-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#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 #4146
Conversation
testing/test_warnings.py
Outdated
assert (1,2) | ||
""" | ||
) | ||
with pytest.warns(pytest.PytestWarning): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 == 3 Becomes:
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 None To 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right! I saw that later with other tests in the same module, I'll get to it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line seems to be out of place.
Great changelog entry btw!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left over?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup. should I keep the docstring at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to use filename=module_path.strpath
here instead: on Python 2 and unicode filenames the str()
call will blow up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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):
changelog
folder, with a name like<ISSUE NUMBER>.<TYPE>.rst
. See changelog/README.rst for details.master
branch for bug fixes, documentation updates and trivial changes.features
branch 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:
AUTHORS
in alphabetical order;Closes #3191