Skip to content

Commit 76884c7

Browse files
authored
Merge pull request #4146 from Tadaboody/give_hints_when_an_assertion_value_is_None_instead_of_a_boolean_3191
[#3191] Give hints when an assertion value is None instead of a boolean
2 parents 5db46d2 + 5ebacc4 commit 76884c7

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

changelog/3191.feature.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
A warning is now issued when assertions are made for ``None``.
2+
3+
This is a common source of confusion among new users, which write::
4+
5+
assert mocked_object.assert_called_with(3, 4, 5, key='value')
6+
7+
When they should write::
8+
9+
mocked_object.assert_called_with(3, 4, 5, key='value')
10+
11+
Because the ``assert_called_with`` method of mock objects already executes an assertion.
12+
13+
This warning will not be issued when ``None`` is explicitly checked. An assertion like::
14+
15+
assert variable is None
16+
17+
will not issue the warning.

src/_pytest/assertion/rewrite.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ def ast_Call(a, b, c):
5151
return ast.Call(a, b, c, None, None)
5252

5353

54+
def ast_Call_helper(func_name, *args, **kwargs):
55+
"""
56+
func_name: str
57+
args: Iterable[ast.expr]
58+
kwargs: Dict[str,ast.expr]
59+
"""
60+
return ast.Call(
61+
ast.Name(func_name, ast.Load()),
62+
list(args),
63+
[ast.keyword(key, val) for key, val in kwargs.items()],
64+
)
65+
66+
5467
class AssertionRewritingHook(object):
5568
"""PEP302 Import hook which rewrites asserts."""
5669

@@ -828,6 +841,13 @@ def visit_Assert(self, assert_):
828841
self.push_format_context()
829842
# Rewrite assert into a bunch of statements.
830843
top_condition, explanation = self.visit(assert_.test)
844+
# If in a test module, check if directly asserting None, in order to warn [Issue #3191]
845+
if self.module_path is not None:
846+
self.statements.append(
847+
self.warn_about_none_ast(
848+
top_condition, module_path=self.module_path, lineno=assert_.lineno
849+
)
850+
)
831851
# Create failure message.
832852
body = self.on_failure
833853
negation = ast.UnaryOp(ast.Not(), top_condition)
@@ -858,6 +878,33 @@ def visit_Assert(self, assert_):
858878
set_location(stmt, assert_.lineno, assert_.col_offset)
859879
return self.statements
860880

881+
def warn_about_none_ast(self, node, module_path, lineno):
882+
"""
883+
Returns an AST issuing a warning if the value of node is `None`.
884+
This is used to warn the user when asserting a function that asserts
885+
internally already.
886+
See issue #3191 for more details.
887+
"""
888+
889+
# Using parse because it is different between py2 and py3.
890+
AST_NONE = ast.parse("None").body[0].value
891+
val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE])
892+
send_warning = ast.parse(
893+
"""
894+
from _pytest.warning_types import PytestWarning
895+
from warnings import warn_explicit
896+
warn_explicit(
897+
PytestWarning('asserting the value None, please use "assert is None"'),
898+
category=None,
899+
filename={filename!r},
900+
lineno={lineno},
901+
)
902+
""".format(
903+
filename=module_path.strpath, lineno=lineno
904+
)
905+
).body
906+
return ast.If(val_is_none, send_warning, [])
907+
861908
def visit_Name(self, name):
862909
# Display the repr of the name if it's a local variable or
863910
# _should_repr_global_name() thinks it's acceptable.

testing/test_warnings.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,63 @@ def test():
623623
else:
624624
assert change_default in ("ini", "cmdline")
625625
result.stdout.fnmatch_lines(["* 1 passed in *"])
626+
627+
628+
class TestAssertionWarnings:
629+
@staticmethod
630+
def assert_result_warns(result, msg):
631+
result.stdout.fnmatch_lines(["*PytestWarning: %s*" % msg])
632+
633+
def test_tuple_warning(self, testdir):
634+
testdir.makepyfile(
635+
"""
636+
def test_foo():
637+
assert (1,2)
638+
"""
639+
)
640+
result = testdir.runpytest()
641+
self.assert_result_warns(
642+
result, "assertion is always true, perhaps remove parentheses?"
643+
)
644+
645+
@staticmethod
646+
def create_file(testdir, return_none):
647+
testdir.makepyfile(
648+
"""
649+
def foo(return_none):
650+
if return_none:
651+
return None
652+
else:
653+
return False
654+
655+
def test_foo():
656+
assert foo({return_none})
657+
""".format(
658+
return_none=return_none
659+
)
660+
)
661+
662+
def test_none_function_warns(self, testdir):
663+
self.create_file(testdir, True)
664+
result = testdir.runpytest()
665+
self.assert_result_warns(
666+
result, 'asserting the value None, please use "assert is None"'
667+
)
668+
669+
def test_assert_is_none_no_warn(self, testdir):
670+
testdir.makepyfile(
671+
"""
672+
def foo():
673+
return None
674+
675+
def test_foo():
676+
assert foo() is None
677+
"""
678+
)
679+
result = testdir.runpytest()
680+
result.stdout.fnmatch_lines(["*1 passed in*"])
681+
682+
def test_false_function_no_warn(self, testdir):
683+
self.create_file(testdir, False)
684+
result = testdir.runpytest()
685+
result.stdout.fnmatch_lines(["*1 failed in*"])

0 commit comments

Comments
 (0)