Skip to content

Commit f05d65d

Browse files
authored
Merge pull request #1622 from nicoddemus/issue-1619-conftest-assert-rewrite
Issue 1619 conftest assert rewrite
2 parents 2305d32 + 573866b commit f05d65d

File tree

6 files changed

+129
-42
lines changed

6 files changed

+129
-42
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Ronny Pfannschmidt
8585
Ross Lawley
8686
Ryan Wooden
8787
Samuele Pedroni
88+
Stephan Obermann
8889
Tareq Alayan
8990
Tom Viner
9091
Trevor Bekolay

CHANGELOG.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,37 @@
7474
message to raise when no exception occurred.
7575
Thanks `@palaviv`_ for the complete PR (`#1616`_).
7676

77+
* ``conftest.py`` files now benefit from assertion rewriting; previously it
78+
was only available for test modules. Thanks `@flub`_, `@sober7`_ and
79+
`@nicoddemus`_ for the PR (`#1619`_).
80+
81+
*
82+
83+
*
84+
7785
* Fix `#1421`_: Exit tests if a collection error occurs and add
7886
``--continue-on-collection-errors`` option to restore previous behaviour.
7987
Thanks `@olegpidsadnyi`_ and `@omarkohl`_ for the complete PR (`#1628`_).
8088

89+
*
90+
91+
*
92+
8193
.. _@milliams: https://github.com/milliams
8294
.. _@csaftoiu: https://github.com/csaftoiu
95+
.. _@flub: https://github.com/flub
8396
.. _@novas0x2a: https://github.com/novas0x2a
8497
.. _@kalekundert: https://github.com/kalekundert
8598
.. _@tareqalayan: https://github.com/tareqalayan
8699
.. _@ceridwen: https://github.com/ceridwen
87100
.. _@palaviv: https://github.com/palaviv
88101
.. _@omarkohl: https://github.com/omarkohl
89102
.. _@mikofski: https://github.com/mikofski
103+
<<<<<<< HEAD
104+
.. _@sober7: https://github.com/sober7
105+
=======
90106
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
107+
>>>>>>> upstream/features
91108

92109
.. _#1421: https://github.com/pytest-dev/pytest/issues/1421
93110
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426
@@ -101,6 +118,7 @@
101118
.. _#1474: https://github.com/pytest-dev/pytest/pull/1474
102119
.. _#1502: https://github.com/pytest-dev/pytest/pull/1502
103120
.. _#1520: https://github.com/pytest-dev/pytest/pull/1520
121+
.. _#1619: https://github.com/pytest-dev/pytest/issues/1619
104122
.. _#372: https://github.com/pytest-dev/pytest/issues/372
105123
.. _#1544: https://github.com/pytest-dev/pytest/issues/1544
106124
.. _#1616: https://github.com/pytest-dev/pytest/pull/1616

_pytest/assertion/__init__.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import py
55
import os
66
import sys
7+
8+
from _pytest.config import hookimpl
79
from _pytest.monkeypatch import monkeypatch
810
from _pytest.assertion import util
911

@@ -42,9 +44,13 @@ def __init__(self, config, mode):
4244
self.trace = config.trace.root.get("assertion")
4345

4446

45-
def pytest_configure(config):
46-
mode = config.getvalue("assertmode")
47-
if config.getvalue("noassert") or config.getvalue("nomagic"):
47+
@hookimpl(tryfirst=True)
48+
def pytest_load_initial_conftests(early_config, parser, args):
49+
ns, ns_unknown_args = parser.parse_known_and_unknown_args(args)
50+
mode = ns.assertmode
51+
no_assert = ns.noassert
52+
no_magic = ns.nomagic
53+
if no_assert or no_magic:
4854
mode = "plain"
4955
if mode == "rewrite":
5056
try:
@@ -57,25 +63,29 @@ def pytest_configure(config):
5763
if (sys.platform.startswith('java') or
5864
sys.version_info[:3] == (2, 6, 0)):
5965
mode = "reinterp"
66+
67+
early_config._assertstate = AssertionState(early_config, mode)
68+
warn_about_missing_assertion(mode, early_config.pluginmanager)
69+
6070
if mode != "plain":
6171
_load_modules(mode)
6272
m = monkeypatch()
63-
config._cleanup.append(m.undo)
73+
early_config._cleanup.append(m.undo)
6474
m.setattr(py.builtin.builtins, 'AssertionError',
6575
reinterpret.AssertionError) # noqa
76+
6677
hook = None
6778
if mode == "rewrite":
68-
hook = rewrite.AssertionRewritingHook() # noqa
79+
hook = rewrite.AssertionRewritingHook(early_config) # noqa
6980
sys.meta_path.insert(0, hook)
70-
warn_about_missing_assertion(mode)
71-
config._assertstate = AssertionState(config, mode)
72-
config._assertstate.hook = hook
73-
config._assertstate.trace("configured with mode set to %r" % (mode,))
81+
82+
early_config._assertstate.hook = hook
83+
early_config._assertstate.trace("configured with mode set to %r" % (mode,))
7484
def undo():
75-
hook = config._assertstate.hook
85+
hook = early_config._assertstate.hook
7686
if hook is not None and hook in sys.meta_path:
7787
sys.meta_path.remove(hook)
78-
config.add_cleanup(undo)
88+
early_config.add_cleanup(undo)
7989

8090

8191
def pytest_collection(session):
@@ -154,7 +164,7 @@ def _load_modules(mode):
154164
from _pytest.assertion import rewrite # noqa
155165

156166

157-
def warn_about_missing_assertion(mode):
167+
def warn_about_missing_assertion(mode, pluginmanager):
158168
try:
159169
assert False
160170
except AssertionError:
@@ -166,10 +176,18 @@ def warn_about_missing_assertion(mode):
166176
else:
167177
specifically = "failing tests may report as passing"
168178

169-
sys.stderr.write("WARNING: " + specifically +
170-
" because assert statements are not executed "
171-
"by the underlying Python interpreter "
172-
"(are you using python -O?)\n")
179+
# temporarily disable capture so we can print our warning
180+
capman = pluginmanager.getplugin('capturemanager')
181+
try:
182+
out, err = capman.suspendcapture()
183+
sys.stderr.write("WARNING: " + specifically +
184+
" because assert statements are not executed "
185+
"by the underlying Python interpreter "
186+
"(are you using python -O?)\n")
187+
finally:
188+
capman.resumecapture()
189+
sys.stdout.write(out)
190+
sys.stderr.write(err)
173191

174192

175193
# Expose this plugin's implementation for the pytest_assertrepr_compare hook

_pytest/assertion/rewrite.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,18 @@
4444
class AssertionRewritingHook(object):
4545
"""PEP302 Import hook which rewrites asserts."""
4646

47-
def __init__(self):
47+
def __init__(self, config):
48+
self.config = config
49+
self.fnpats = config.getini("python_files")
4850
self.session = None
4951
self.modules = {}
5052
self._register_with_pkg_resources()
5153

5254
def set_session(self, session):
53-
self.fnpats = session.config.getini("python_files")
5455
self.session = session
5556

5657
def find_module(self, name, path=None):
57-
if self.session is None:
58-
return None
59-
sess = self.session
60-
state = sess.config._assertstate
58+
state = self.config._assertstate
6159
state.trace("find_module called for: %s" % name)
6260
names = name.rsplit(".", 1)
6361
lastname = names[-1]
@@ -86,24 +84,11 @@ def find_module(self, name, path=None):
8684
return None
8785
else:
8886
fn = os.path.join(pth, name.rpartition(".")[2] + ".py")
87+
8988
fn_pypath = py.path.local(fn)
90-
# Is this a test file?
91-
if not sess.isinitpath(fn):
92-
# We have to be very careful here because imports in this code can
93-
# trigger a cycle.
94-
self.session = None
95-
try:
96-
for pat in self.fnpats:
97-
if fn_pypath.fnmatch(pat):
98-
state.trace("matched test file %r" % (fn,))
99-
break
100-
else:
101-
return None
102-
finally:
103-
self.session = sess
104-
else:
105-
state.trace("matched test file (was specified on cmdline): %r" %
106-
(fn,))
89+
if not self._should_rewrite(fn_pypath, state):
90+
return None
91+
10792
# The requested module looks like a test file, so rewrite it. This is
10893
# the most magical part of the process: load the source, rewrite the
10994
# asserts, and load the rewritten source. We also cache the rewritten
@@ -151,6 +136,32 @@ def find_module(self, name, path=None):
151136
self.modules[name] = co, pyc
152137
return self
153138

139+
def _should_rewrite(self, fn_pypath, state):
140+
# always rewrite conftest files
141+
fn = str(fn_pypath)
142+
if fn_pypath.basename == 'conftest.py':
143+
state.trace("rewriting conftest file: %r" % (fn,))
144+
return True
145+
elif self.session is not None:
146+
if self.session.isinitpath(fn):
147+
state.trace("matched test file (was specified on cmdline): %r" %
148+
(fn,))
149+
return True
150+
else:
151+
# modules not passed explicitly on the command line are only
152+
# rewritten if they match the naming convention for test files
153+
session = self.session # avoid a cycle here
154+
self.session = None
155+
try:
156+
for pat in self.fnpats:
157+
if fn_pypath.fnmatch(pat):
158+
state.trace("matched test file %r" % (fn,))
159+
return True
160+
finally:
161+
self.session = session
162+
del session
163+
return False
164+
154165
def load_module(self, name):
155166
# If there is an existing module object named 'fullname' in
156167
# sys.modules, the loader must use that existing module. (Otherwise,

testing/test_assertrewrite.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,40 @@ def test_foo(self):
694694
result = testdir.runpytest()
695695
result.stdout.fnmatch_lines('*1 passed*')
696696

697+
@pytest.mark.parametrize('initial_conftest', [True, False])
698+
@pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp'])
699+
def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode):
700+
"""Test that conftest files are using assertion rewrite on import.
701+
(#1619)
702+
"""
703+
testdir.tmpdir.join('foo/tests').ensure(dir=1)
704+
conftest_path = 'conftest.py' if initial_conftest else 'foo/conftest.py'
705+
contents = {
706+
conftest_path: """
707+
import pytest
708+
@pytest.fixture
709+
def check_first():
710+
def check(values, value):
711+
assert values.pop(0) == value
712+
return check
713+
""",
714+
'foo/tests/test_foo.py': """
715+
def test(check_first):
716+
check_first([10, 30], 30)
717+
"""
718+
}
719+
testdir.makepyfile(**contents)
720+
result = testdir.runpytest_subprocess('--assert=%s' % mode)
721+
if mode == 'plain':
722+
expected = 'E AssertionError'
723+
elif mode == 'rewrite':
724+
expected = '*assert 10 == 30*'
725+
elif mode == 'reinterp':
726+
expected = '*AssertionError:*was re-run*'
727+
else:
728+
assert 0
729+
result.stdout.fnmatch_lines([expected])
730+
697731

698732
def test_issue731(testdir):
699733
testdir.makepyfile("""

testing/test_config.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,14 @@ def pytest_load_initial_conftests(self):
485485
pm.register(m)
486486
hc = pm.hook.pytest_load_initial_conftests
487487
l = hc._nonwrappers + hc._wrappers
488-
assert l[-1].function.__module__ == "_pytest.capture"
489-
assert l[-2].function == m.pytest_load_initial_conftests
490-
assert l[-3].function.__module__ == "_pytest.config"
488+
expected = [
489+
"_pytest.config",
490+
'test_config',
491+
'_pytest.assertion',
492+
'_pytest.capture',
493+
]
494+
assert [x.function.__module__ for x in l] == expected
495+
491496

492497
class TestWarning:
493498
def test_warn_config(self, testdir):

0 commit comments

Comments
 (0)