[#3191] Give hints when an assertion value is None instead of a boolean #4146
[#3191] Give hints when an assertion value is None instead of a boolean #4146nicoddemus merged 13 commits intopytest-dev:featuresfrom Tadaboody:give_hints_when_an_assertion_value_is_None_instead_of_a_boolean_3191
Conversation
testing/test_warnings.py
Outdated
| assert (1,2) | ||
| """ | ||
| ) | ||
| with pytest.warns(pytest.PytestWarning): |
There was a problem hiding this comment.
This will never happen, as runpytest_subprocess() runs pytest in a separate process, which won't trigger a PytestWarning. You should check stdout instead (and no need to run in a separate process, you can use just runpytest().
There was a problem hiding this comment.
Gotcha, Will change. I inferred this by snooping around other tests and got it wrong.
Where in the documentation are all these methods specified?
I think that a section in the contributing guide describing the inner use APIs or linking to a separate "On testing tests" guide will go a long way
There was a problem hiding this comment.
You are right, created #4151 to track this. 👍
Here are the full docs to testdir, although they could use some improvement.
Codecov Report
@@ Coverage Diff @@
## features #4146 +/- ##
============================================
+ Coverage 95.75% 95.75% +<.01%
============================================
Files 111 111
Lines 24867 24897 +30
Branches 2455 2457 +2
============================================
+ Hits 23812 23841 +29
Misses 746 746
- Partials 309 310 +1
Continue to review full report at Codecov.
|
testing/test_warnings.py
Outdated
| """ | ||
| ) | ||
| result = testdir.runpytest() | ||
| assert self.result_warns(result) |
There was a problem hiding this comment.
I suggest to use stdout.fnmatch_lines instead:
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*PytestWarning*"])|
@nicoddemus so we because we look at function returns we need to emit the warning at test runtime, where exactly does that happen? I'm getting a little bit lost in the project structure - is there somewhere in the docs I can look to understand a bit more under the hood? |
|
Hi @Tadaboody, Sorry for taking so long to get back to this.
I think the solution is to change how we write the asserts to emit a warning if the expression evaluated is For example, take a look at pytest-ast-back-to-python, it lets you see the exact rewritten assert code generated by pytest: def test_simple():
a = 1
b = 2
assert a + b == 3Becomes: We need to change that generated code into something like this: #3479 by @Sup3rGeo is an example which changes the generated code and might be a good reference. @RonnyPfannschmidt might have some suggestions here as well. |
|
@Tadaboody so basic the main place to play with is the pytest/src/_pytest/assertion/rewrite.py Line 799 in 72d98a7 I suggest that you play changing the ast statements and then checking the resulting python code (probably using pytest-ast-back-to-python as @nicoddemus pointed out, although I was doing this manually using |
Edit: This got fixed itself after I woke up. Is this what people call a bed bug?Thanks for the help @nicoddemus @Sup3rGeo I think I finally got What I'm supposed to do! # stub.py
def test_foo():
assert NoneTo try and see what the ast looks like now I keep on getting From what I gather, Full traceback: rm -rf __pycache__/ && py.test --show-ast-as-python stub.py
Traceback (most recent call last):
File "<frozen importlib._bootstrap>", line 888, in _find_spec
AttributeError: 'AssertionRewritingHook' object has no attribute 'find_spec'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/tomer/Forks/pytest/venv/bin/py.test", line 11, in <module>
load_entry_point('pytest', 'console_scripts', 'py.test')()
File "/Users/tomer/Forks/pytest/src/_pytest/config/__init__.py", line 49, in main
config = _prepareconfig(args, plugins)
File "/Users/tomer/Forks/pytest/src/_pytest/config/__init__.py", line 186, in _prepareconfig
pluginmanager=pluginmanager, args=args
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/hooks.py", line 258, in __call__
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/manager.py", line 67, in _hookexec
return self._inner_hookexec(hook, methods, kwargs)
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/manager.py", line 61, in <lambda>
firstresult=hook.spec_opts.get('firstresult'),
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/callers.py", line 196, in _multicall
gen.send(outcome)
File "/Users/tomer/Forks/pytest/src/_pytest/helpconfig.py", line 89, in pytest_cmdline_parse
config = outcome.get_result()
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/callers.py", line 76, in get_result
raise ex[1].with_traceback(ex[2])
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/callers.py", line 180, in _multicall
res = hook_impl.function(*args)
File "/Users/tomer/Forks/pytest/src/_pytest/config/__init__.py", line 656, in pytest_cmdline_parse
self.parse(args)
File "/Users/tomer/Forks/pytest/src/_pytest/config/__init__.py", line 828, in parse
self._preparse(args, addopts=addopts)
File "/Users/tomer/Forks/pytest/src/_pytest/config/__init__.py", line 780, in _preparse
self.pluginmanager.load_setuptools_entrypoints("pytest11")
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pluggy/manager.py", line 253, in load_setuptools_entrypoints
plugin = ep.load()
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2332, in load
return self.resolve()
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2338, in resolve
module = __import__(self.module_name, fromlist=['__name__'], level=0)
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 656, in _load_unlocked
File "<frozen importlib._bootstrap>", line 626, in _load_backward_compatible
File "/Users/tomer/Forks/pytest/src/_pytest/assertion/rewrite.py", line 304, in load_module
six.exec_(co, mod.__dict__)
File "/Users/tomer/Forks/pytest/venv/lib/python3.6/site-packages/pytest_ast_back_to_python.py", line 7, in <module>
import codegen
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 951, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 890, in _find_spec
File "<frozen importlib._bootstrap>", line 864, in _find_spec_legacy
File "/Users/tomer/Forks/pytest/src/_pytest/assertion/rewrite.py", line 172, in find_module
source_stat, co = _rewrite_test(self.config, fn_pypath)
File "/Users/tomer/Forks/pytest/src/_pytest/assertion/rewrite.py", line 422, in _rewrite_test
co = compile(tree, fn.strpath, "exec", dont_inherit=True)
TypeError: required field "lineno" missing from expr |
|
Should I rebase to fix the dirty history or will this be squashed anyway? |
|
@Tadaboody not sure if I can help you without really going deep into your code. Yes in theory you should not have to worry about I see you got already some helpers and delegated the warning for some functions. I don't know if you did it like this, but I would start manually adding one valid ast statement at a time from the stock code + running/testing, so when things break like this you can pinpoint the code causing problems more easily. |
|
@Sup3rGeo That's more or less what I did, maybe next time I'll do it slower. |
src/_pytest/assertion/rewrite.py
Outdated
| warn_explicit( | ||
| PytestWarning('assertion the value None, Please use "assert is None"'), | ||
| category=None, | ||
| filename='{filename}', |
There was a problem hiding this comment.
I get this error on Windows:
File "c:\users\bruno\pytest\src\_pytest\assertion\rewrite.py", line 843, in visit_Assert
top_condition, module_path=self.module_path, lineno=assert_.lineno
File "c:\users\bruno\pytest\src\_pytest\assertion\rewrite.py", line 904, in warn_about_none_ast
filename=str(module_path), lineno=lineno
File "C:\Users\bruno\AppData\Local\Programs\Python\Python36-32\lib\ast.py", line 35, in parse
return compile(source, filename, mode, PyCF_ONLY_AST)
File "<unknown>", line 7
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \uXXXX escape
The problem is using str(module_path) here, because this will generate a string with \ characters and potential invalid escapes.
We should use this instead:
warn_explicit(
PytestWarning('assertion the value None, please use "assert is None"'),
category=None,
filename={filename!r},
lineno={lineno},
)
""".format(
filename=module_path, lineno=lineno
There was a problem hiding this comment.
Ooh I was wondering how to fix that! Python string formatting is truly a pit with no end
There was a problem hiding this comment.
Heh!
Also, I just realized that you should use filename=module_path.strpath, otherwise we will get the py.path.local representation in the code, which is not what we want.
testing/test_warnings.py
Outdated
| result = testdir.runpytest() | ||
| self.assert_result_warns(result) | ||
|
|
||
| @pytest.mark.xfail(strict=True) |
There was a problem hiding this comment.
Instead of using xfail, please use a proper assertion that we are not receiving any warnings, something like:
result.stdout.fnmatch_lines(["*1 passed in*"])Because warnings will generate a different summary ("1 passed, 1 warnings in"), which would fail the test. Same for test_false_function_no_warn below.
There was a problem hiding this comment.
Right! I saw that later with other tests in the same module, I'll get to it
nicoddemus
left a comment
There was a problem hiding this comment.
We are almost there. 😁
Please take a look at my comments.
|
Oops I also broke linting when I edited the changelog using the GH web UI, sorry about that! 🙇 |
|
It looks like I broke something with test collections? I'm not quite sure what the test I broke is supposed to do |
|
@Tadaboody |
As requested by review. :ok_hand: Address code review for tests
🐛Fix warn ast bugs 🐛Fix inner-ast imports by using importFrom Alternetavly ast_call_helper could be retooled to use ast.attribute(...)
Maybe there should be a warning about that too?
in py2 it's a ast.Name where in py3 it's a ast.NamedConstant Fixes namespace by using import from
Edited the changelog for extra clarity, and to fire off auto-formatting
Oddly enough, keeping `filename='{filename!r}'` caused an error while
collecting tests, but getting rid of the single ticks fixed it
Hopefully closes #3191
|
🎉 Passed! thanks @blueyed. I squashed a bit but looking again I can squash some even more if needed. Waiting for a re-review by @nicoddemus |
nicoddemus
left a comment
There was a problem hiding this comment.
I think a few more things to sort out before we can merge this. 😁
changelog/3191.feature.rst
Outdated
| This warning will not be issued when ``None`` is explicitly checked | ||
| assert none_returning_fun() is None | ||
|
|
||
| will not issue the warning |
There was a problem hiding this comment.
This line seems to be out of place.
Great changelog entry btw!
There was a problem hiding this comment.
That's because you wrote it - 26d27df
I think I added that line to trigger the pre-commit auto format not the best plan in hindsight
There was a problem hiding this comment.
I reworded this section, hopefully it's clearer. If you still think it's out of place I'll delete it
There was a problem hiding this comment.
Ahh forgot about that, well at least I'm consistent then. It would be amusing if I looked at it and request a lot of changes. 😆
src/_pytest/assertion/rewrite.py
Outdated
| warnings.warn_explicit( | ||
| PytestWarning('assertion the value None, Please use "assert is None"'), | ||
| category=None, | ||
| # filename=str(self.module_path), |
There was a problem hiding this comment.
yup. should I keep the docstring at all?
There was a problem hiding this comment.
A good point, let's change the docstring to a short description then, it will probably be better. 👍
src/_pytest/assertion/rewrite.py
Outdated
| lineno={lineno}, | ||
| ) | ||
| """.format( | ||
| filename=str(module_path), lineno=lineno |
There was a problem hiding this comment.
We need to use filename=module_path.strpath here instead: on Python 2 and unicode filenames the str() call will blow up.
nicoddemus
left a comment
There was a problem hiding this comment.
Awesome work @Tadaboody, thanks!
|
now I remember why I didn't keep |
|
Yeah I think it's reasonable to skip those warnings when |
|
Great stuff. |
|
If this turns out to be green and my changes are OK, please squash this into a single commit, and force-push (we could squash-merge it but that is disabled). |
Thanks for submitting a PR, your contribution is really appreciated!
Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is
just a guideline):
changelogfolder, with a name like<ISSUE NUMBER>.<TYPE>.rst. See changelog/README.rst for details.masterbranch for bug fixes, documentation updates and trivial changes.featuresbranch for new features and removals/deprecations.Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
AUTHORSin alphabetical order;Closes #3191