Merge pull request #1811 from nicoddemus/revert-invocation-fixtures

Revert invocation-fixtures code
This commit is contained in:
Florian Bruhin 2016-08-17 20:32:54 +02:00 committed by GitHub
commit c8ab79402c
10 changed files with 31 additions and 425 deletions

View File

@ -143,13 +143,6 @@ time or change existing behaviors in order to make them less surprising/more use
never fail because tuples are always truthy and are usually a mistake never fail because tuples are always truthy and are usually a mistake
(see `#1562`_). Thanks `@kvas-it`_, for the PR. (see `#1562`_). Thanks `@kvas-it`_, for the PR.
* Experimentally introduce new ``"invocation"`` fixture scope. At invocation scope a
fixture function is cached in the same way as the fixture or test function that requests it.
You can now use the builtin ``monkeypatch`` fixture from ``session``-scoped fixtures
where previously you would get an error that you can not use a ``function``-scoped fixture from a
``session``-scoped one.*
Thanks `@nicoddemus`_ for the PR.
* Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``). * Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``).
Thanks to `@anntzer`_ for the PR. Thanks to `@anntzer`_ for the PR.

View File

@ -274,10 +274,8 @@ class FixtureRequest(FuncargnamesCompatAttr):
self.fixturename = None self.fixturename = None
#: Scope string, one of "function", "class", "module", "session" #: Scope string, one of "function", "class", "module", "session"
self.scope = "function" self.scope = "function"
# rename both attributes below because their key has changed; better an attribute error self._fixture_values = {} # argname -> fixture value
# than subtle key misses; also backward incompatibility self._fixture_defs = {} # argname -> FixtureDef
self._fixture_values = {} # (argname, scope) -> fixture value
self._fixture_defs = {} # (argname, scope) -> FixtureDef
fixtureinfo = pyfuncitem._fixtureinfo fixtureinfo = pyfuncitem._fixtureinfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
self._arg2index = {} self._arg2index = {}
@ -294,31 +292,20 @@ class FixtureRequest(FuncargnamesCompatAttr):
return self._getscopeitem(self.scope) return self._getscopeitem(self.scope)
def _getnextfixturedef(self, argname, scope): def _getnextfixturedef(self, argname):
def trygetfixturedefs(argname):
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
fixturedefs = self._arg2fixturedefs.get(argname + ':' + scope, None)
return fixturedefs
fixturedefs = trygetfixturedefs(argname)
if fixturedefs is None: if fixturedefs is None:
# we arrive here because of a a dynamic call to # we arrive here because of a a dynamic call to
# getfixturevalue(argname) usage which was naturally # getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time # not known at parsing/collection time
parentid = self._pyfuncitem.parent.nodeid parentid = self._pyfuncitem.parent.nodeid
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
if fixturedefs:
self._arg2fixturedefs[argname] = fixturedefs self._arg2fixturedefs[argname] = fixturedefs
fixturedefs_by_argname = self._fixturemanager.getfixturedefs_multiple_scopes(argname, parentid)
if fixturedefs_by_argname:
self._arg2fixturedefs.update(fixturedefs_by_argname)
fixturedefs = trygetfixturedefs(argname)
# fixturedefs list is immutable so we maintain a decreasing index # fixturedefs list is immutable so we maintain a decreasing index
index = self._arg2index.get((argname, scope), 0) - 1 index = self._arg2index.get(argname, 0) - 1
if fixturedefs is None or (-index > len(fixturedefs)): if fixturedefs is None or (-index > len(fixturedefs)):
raise FixtureLookupError(argname, self) raise FixtureLookupError(argname, self)
self._arg2index[(argname, scope)] = index self._arg2index[argname] = index
return fixturedefs[index] return fixturedefs[index]
@property @property
@ -458,10 +445,10 @@ class FixtureRequest(FuncargnamesCompatAttr):
def _get_active_fixturedef(self, argname): def _get_active_fixturedef(self, argname):
try: try:
return self._fixture_defs[(argname, self.scope)] return self._fixture_defs[argname]
except KeyError: except KeyError:
try: try:
fixturedef = self._getnextfixturedef(argname, self.scope) fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError: except FixtureLookupError:
if argname == "request": if argname == "request":
class PseudoFixtureDef: class PseudoFixtureDef:
@ -472,8 +459,8 @@ class FixtureRequest(FuncargnamesCompatAttr):
# remove indent to prevent the python3 exception # remove indent to prevent the python3 exception
# from leaking into the call # from leaking into the call
result = self._getfixturevalue(fixturedef) result = self._getfixturevalue(fixturedef)
self._fixture_values[(argname, self.scope)] = result self._fixture_values[argname] = result
self._fixture_defs[(argname, self.scope)] = fixturedef self._fixture_defs[argname] = fixturedef
return fixturedef return fixturedef
def _get_fixturestack(self): def _get_fixturestack(self):
@ -615,16 +602,6 @@ def scopemismatch(currentscope, newscope):
return scopes.index(newscope) > scopes.index(currentscope) return scopes.index(newscope) > scopes.index(currentscope)
def strip_invocation_scope_suffix(name):
"""Remove the invocation-scope suffix from the given name.
Invocation scope fixtures have their scope in the name of the fixture.
For example, "monkeypatch:session". This function strips the suffix
returning the user-frienldy name of the fixture.
"""
return name.split(':')[0]
class FixtureLookupError(LookupError): class FixtureLookupError(LookupError):
""" could not return a requested Fixture (missing or invalid). """ """ could not return a requested Fixture (missing or invalid). """
def __init__(self, argname, request, msg=None): def __init__(self, argname, request, msg=None):
@ -664,7 +641,6 @@ class FixtureLookupError(LookupError):
parentid = self.request._pyfuncitem.parent.nodeid parentid = self.request._pyfuncitem.parent.nodeid
for name, fixturedefs in fm._arg2fixturedefs.items(): for name, fixturedefs in fm._arg2fixturedefs.items():
faclist = list(fm._matchfactories(fixturedefs, parentid)) faclist = list(fm._matchfactories(fixturedefs, parentid))
name = strip_invocation_scope_suffix(name)
if faclist and name not in available: if faclist and name not in available:
available.append(name) available.append(name)
msg = "fixture %r not found" % (self.argname,) msg = "fixture %r not found" % (self.argname,)
@ -847,7 +823,7 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
function will be injected. function will be injected.
:arg scope: the scope for which this fixture is shared, one of :arg scope: the scope for which this fixture is shared, one of
"function" (default), "class", "module", "session" or "invocation". "function" (default), "class", "module" or "session".
:arg params: an optional list of parameters which will cause multiple :arg params: an optional list of parameters which will cause multiple
invocations of the fixture function and all of the tests invocations of the fixture function and all of the tests
@ -1029,11 +1005,6 @@ class FixtureManager:
if fixturedefs: if fixturedefs:
arg2fixturedefs[argname] = fixturedefs arg2fixturedefs[argname] = fixturedefs
merge(fixturedefs[-1].argnames) merge(fixturedefs[-1].argnames)
fixturedefs_by_argname = self.getfixturedefs_multiple_scopes(argname, parentid)
if fixturedefs_by_argname:
arg2fixturedefs.update(fixturedefs_by_argname)
for fixturedefs in fixturedefs_by_argname.values():
merge(fixturedefs[-1].argnames)
return fixturenames_closure, arg2fixturedefs return fixturenames_closure, arg2fixturedefs
def pytest_generate_tests(self, metafunc): def pytest_generate_tests(self, metafunc):
@ -1093,10 +1064,8 @@ class FixtureManager:
'and be decorated with @pytest.fixture:\n%s' % name 'and be decorated with @pytest.fixture:\n%s' % name
assert not name.startswith(self._argprefix), msg assert not name.startswith(self._argprefix), msg
def new_fixture_def(name, scope):
"""Create and registers a new FixtureDef with given name and scope."""
fixture_def = FixtureDef(self, nodeid, name, obj, fixture_def = FixtureDef(self, nodeid, name, obj,
scope, marker.params, marker.scope, marker.params,
unittest=unittest, ids=marker.ids) unittest=unittest, ids=marker.ids)
faclist = self._arg2fixturedefs.setdefault(name, []) faclist = self._arg2fixturedefs.setdefault(name, [])
@ -1112,12 +1081,6 @@ class FixtureManager:
if marker.autouse: if marker.autouse:
autousenames.append(name) autousenames.append(name)
if marker.scope == 'invocation':
for new_scope in scopes:
new_fixture_def(name + ':{0}'.format(new_scope), new_scope)
else:
new_fixture_def(name, marker.scope)
if autousenames: if autousenames:
self._nodeid_and_autousenames.append((nodeid or '', autousenames)) self._nodeid_and_autousenames.append((nodeid or '', autousenames))
@ -1141,23 +1104,3 @@ class FixtureManager:
if nodeid.startswith(fixturedef.baseid): if nodeid.startswith(fixturedef.baseid):
yield fixturedef yield fixturedef
def getfixturedefs_multiple_scopes(self, argname, nodeid):
"""
Gets multiple scoped fixtures which are applicable to the given nodeid. Multiple scoped
fixtures are created by "invocation" scoped fixtures and have argnames in
the form: "<argname>:<scope>" (for example "tmpdir:session").
:return: dict of "argname" -> [FixtureDef].
Arguments similar to ``getfixturedefs``.
"""
prefix = argname + ':'
fixturedefs_by_argname = dict((k, v) for k, v in self._arg2fixturedefs.items()
if k.startswith(prefix))
if fixturedefs_by_argname:
result = {}
for argname, fixturedefs in fixturedefs_by_argname.items():
result[argname] = tuple(self._matchfactories(fixturedefs, nodeid))
return result
else:
return None

View File

@ -10,7 +10,7 @@ import pytest
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
@pytest.fixture(scope='invocation') @pytest.fixture
def monkeypatch(request): def monkeypatch(request):
"""The returned ``monkeypatch`` fixture provides these """The returned ``monkeypatch`` fixture provides these
helper methods to modify objects, dictionaries or os.environ:: helper methods to modify objects, dictionaries or os.environ::
@ -28,8 +28,6 @@ def monkeypatch(request):
test function or fixture has finished. The ``raising`` test function or fixture has finished. The ``raising``
parameter determines if a KeyError or AttributeError parameter determines if a KeyError or AttributeError
will be raised if the set/deletion operation has no target. will be raised if the set/deletion operation has no target.
This fixture is ``invocation``-scoped.
""" """
mpatch = MonkeyPatch() mpatch = MonkeyPatch()
request.addfinalizer(mpatch.undo) request.addfinalizer(mpatch.undo)

View File

@ -1008,7 +1008,6 @@ def showfixtures(config):
def _showfixtures_main(config, session): def _showfixtures_main(config, session):
import _pytest.config import _pytest.config
from _pytest.fixtures import strip_invocation_scope_suffix
session.perform_collect() session.perform_collect()
curdir = py.path.local() curdir = py.path.local()
tw = _pytest.config.create_terminal_writer(config) tw = _pytest.config.create_terminal_writer(config)
@ -1025,16 +1024,13 @@ def _showfixtures_main(config, session):
continue continue
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
loc = getlocation(fixturedef.func, curdir) loc = getlocation(fixturedef.func, curdir)
# invocation-scoped fixtures have argname in the form if (fixturedef.argname, loc) in seen:
# "<name>:<scope>" (for example: "monkeypatch:session").
fixture_argname = strip_invocation_scope_suffix(fixturedef.argname)
if (fixture_argname, loc) in seen:
continue continue
seen.add((fixture_argname, loc)) seen.add((fixturedef.argname, loc))
available.append((len(fixturedef.baseid), available.append((len(fixturedef.baseid),
fixturedef.func.__module__, fixturedef.func.__module__,
curdir.bestrelpath(loc), curdir.bestrelpath(loc),
fixture_argname, fixturedef)) fixturedef.argname, fixturedef))
available.sort() available.sort()
currentmodule = None currentmodule = None

View File

@ -865,13 +865,6 @@ All test methods in this TestClass will use the transaction fixture while
other test classes or functions in the module will not use it unless other test classes or functions in the module will not use it unless
they also add a ``transact`` reference. they also add a ``transact`` reference.
invocation-scoped fixtures
--------------------------
pytest 3.0 introduced a new advanced scope for fixtures: ``"invocation"``. Fixtures marked with
this scope can be requested from any other scope, providing a version of the fixture for that scope.
See more in :ref:`invocation_scoped_fixture`.
Shifting (visibility of) fixture functions Shifting (visibility of) fixture functions
---------------------------------------------------- ----------------------------------------------------

View File

@ -1,63 +0,0 @@
.. _invocation_scoped_fixture:
Invocation-scoped fixtures
==========================
.. versionadded:: 3.0
.. note::
This feature is experimental, so if decided that it brings too much problems
or considered too complicated it might be removed in pytest ``3.1``.
Fixtures can be defined with ``invocation`` scope, meaning that the fixture
can be requested by fixtures from any scope, but when they do they assume
the same scope as the fixture requesting it.
An ``invocation``-scoped fixture can be requested from different scopes
in the same test session, in which case each scope will have its own copy.
Example
-------
Consider a fixture which manages external process execution:
this fixture provides auxiliary methods for tests and fixtures to start external
processes while making sure the
processes terminate at the appropriate time. Because it makes sense
to start a webserver for the entire session and also to execute a numerical
simulation for a single test function, the ``process_manager``
fixture can be declared as ``invocation``, so each scope gets its own
value and can manage processes which will live for the duration of the scope.
.. code-block:: python
import pytest
@pytest.fixture(scope='invocation')
def process_manager():
"""
Return a ProcessManager instance which can be used to start
long-lived processes and ensures they are terminated at the
appropriate scope.
"""
m = ProcessManager()
yield m
m.shutdown_all()
@pytest.fixture(scope='session')
def server(process_manager):
process_manager.start(sys.executable, 'server.py')
@pytest.fixture(scope='function')
def start_simulation(process_manager):
import functools
return functools.partial(process_manager.start,
sys.executable, 'simulator.py')
In the above code, simulators started using ``start_simulation`` will be
terminated when the test function exits, while the server will be kept
active for the entire simulation run, being terminated when the test session
finishes.

View File

@ -14,8 +14,6 @@ and a discussion of its motivation.
.. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/
As of pytest-3.0, the ``monkeypatch`` fixture is :ref:`invocation-scoped <invocation_scoped_fixture>`
meaning it can be requested from fixtures of any scope.
Simple example: monkeypatching functions Simple example: monkeypatching functions
--------------------------------------------------- ---------------------------------------------------

View File

@ -433,8 +433,7 @@ class TestFillFixtures:
"*1 error*", "*1 error*",
]) ])
assert "INTERNAL" not in result.stdout.str() assert "INTERNAL" not in result.stdout.str()
# invocation-scoped fixture should appear with their friendly name only
assert 'monkeypatch:session' not in result.stdout.str()
def test_fixture_excinfo_leak(self, testdir): def test_fixture_excinfo_leak(self, testdir):
# on python2 sys.excinfo would leak into fixture executions # on python2 sys.excinfo would leak into fixture executions
@ -2743,13 +2742,6 @@ class TestShowFixtures:
Hi from test module Hi from test module
''') ''')
def test_show_invocation_fixtures(self, testdir):
"""py.test --fixtures should display invocation-scoped fixtures once.
"""
result = testdir.runpytest("--fixtures")
result.stdout.fnmatch_lines('''monkeypatch''')
assert 'monkeypatch:session' not in result.stdout.str()
@pytest.mark.parametrize('flavor', ['fixture', 'yield_fixture']) @pytest.mark.parametrize('flavor', ['fixture', 'yield_fixture'])
class TestContextManagerFixtureFuncs: class TestContextManagerFixtureFuncs:

View File

@ -1,214 +0,0 @@
def test_invocation_request(testdir):
"""
Simple test case with session and module scopes requesting an
invocation-scoped fixture.
"""
testdir.makeconftest("""
import pytest
@pytest.fixture(scope='invocation')
def my_name(request):
if request.scope == 'function':
return request.function.__name__
elif request.scope == 'module':
return request.module.__name__
elif request.scope == 'session':
return '<session>'
@pytest.fixture(scope='session')
def session_name(my_name):
return my_name
@pytest.fixture(scope='module')
def module_name(my_name):
return my_name
""")
testdir.makepyfile(test_module_foo="""
def test_foo(my_name, module_name, session_name):
assert my_name == 'test_foo'
assert module_name == 'test_module_foo'
assert session_name == '<session>'
""")
testdir.makepyfile(test_module_bar="""
def test_bar(my_name, module_name, session_name):
assert my_name == 'test_bar'
assert module_name == 'test_module_bar'
assert session_name == '<session>'
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['*2 passed*'])
def test_override_invocation_scoped(testdir):
"""Test that it's possible to override invocation-scoped fixtures."""
testdir.makeconftest("""
import pytest
@pytest.fixture(scope='invocation')
def magic_value(request):
if request.scope == 'function':
return 1
elif request.scope == 'module':
return 100
@pytest.fixture(scope='module')
def module_magic_value(magic_value):
return magic_value * 2
""")
testdir.makepyfile(test_module_override="""
import pytest
@pytest.fixture(scope='module')
def magic_value():
return 42
def test_override(magic_value, module_magic_value):
assert magic_value == 42
assert module_magic_value == 42 * 2
""")
testdir.makepyfile(test_normal="""
def test_normal(magic_value, module_magic_value):
assert magic_value == 1
assert module_magic_value == 200
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['*2 passed*'])
class TestAcceptance:
"""
Complete acceptance test for a invocation-scoped fixture.
"""
def test_acceptance(self, testdir):
"""
Tests a "stack" fixture which provides a separate list to each scope which uses it.
Some notes:
- For each scope, define 2 fixtures of the same scope which use the "stack" fixture,
to ensure they get the same "stack" instance for that scope.
- Creates multiple test files, which tests on each modifying and checking fixtures to
ensure things are working properly.
"""
testdir.makeconftest("""
import pytest
@pytest.fixture(scope='invocation')
def stack():
return []
@pytest.fixture(scope='session')
def session1_fix(stack):
stack.append('session1_fix')
return stack
@pytest.fixture(scope='session')
def session2_fix(stack):
stack.append('session2_fix')
return stack
@pytest.fixture(scope='module')
def module1_fix(stack):
stack.append('module1_fix')
return stack
@pytest.fixture(scope='module')
def module2_fix(stack):
stack.append('module2_fix')
return stack
@pytest.fixture(scope='class')
def class1_fix(stack):
stack.append('class1_fix')
return stack
@pytest.fixture(scope='class')
def class2_fix(stack):
stack.append('class2_fix')
return stack
""")
testdir.makepyfile(test_0="""
import pytest
@pytest.fixture
def func_stack(stack):
return stack
def test_scoped_instances(session1_fix, session2_fix, module1_fix, module2_fix,
class1_fix, class2_fix, stack, func_stack):
assert session1_fix is session2_fix
assert module1_fix is module2_fix
assert class1_fix is class2_fix
assert stack is func_stack
assert session1_fix is not module2_fix
assert module2_fix is not class1_fix
assert class1_fix is not stack
""")
testdir.makepyfile(test_1="""
def test_func_1(request, session1_fix, session2_fix, module1_fix, module2_fix, stack):
assert stack == []
assert session1_fix == ['session1_fix', 'session2_fix']
session1_fix.append('test_1::test_func_1')
assert module1_fix == ['module1_fix', 'module2_fix']
module1_fix.append('test_1::test_func_1')
class Test:
def test_1(self, request, session1_fix, module1_fix, class1_fix, class2_fix, stack):
assert stack == []
assert session1_fix == ['session1_fix', 'session2_fix', 'test_1::test_func_1']
session1_fix.append('test_1::Test::test_1')
assert module1_fix == ['module1_fix', 'module2_fix', 'test_1::test_func_1']
module1_fix.append('test_1::test_func_1')
assert class1_fix == ['class1_fix', 'class2_fix']
class1_fix.append('test_1::Test::test_1')
def test_2(self, request, class1_fix, class2_fix):
assert class1_fix == ['class1_fix', 'class2_fix', 'test_1::Test::test_1']
class1_fix.append('Test.test_2')
def test_func_2(request, session1_fix, session2_fix, module1_fix, class1_fix, class2_fix, stack):
assert stack == []
assert session1_fix == ['session1_fix', 'session2_fix', 'test_1::test_func_1',
'test_1::Test::test_1']
session1_fix.append('test_1::test_func_2')
assert module1_fix == ['module1_fix', 'module2_fix', 'test_1::test_func_1', 'test_1::test_func_1']
assert class1_fix == ['class1_fix', 'class2_fix']
""")
testdir.makepyfile(test_2="""
import pytest
@pytest.fixture(scope='session')
def another_session_stack(stack):
stack.append('other_session_stack')
return stack
def test_func_2(request, another_session_stack, module1_fix, stack):
assert stack == []
assert another_session_stack == [
'session1_fix',
'session2_fix',
'test_1::test_func_1',
'test_1::Test::test_1',
'test_1::test_func_2',
'other_session_stack',
]
assert module1_fix == ['module1_fix']
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines('* 6 passed in *')

View File

@ -331,33 +331,3 @@ def test_issue1338_name_resolving():
monkeypatch.undo() monkeypatch.undo()
def test_invocation_scoped_monkeypatch(testdir):
testdir.makeconftest("""
import pytest
import sys
@pytest.fixture(scope='module')
def stamp_sys(monkeypatch):
monkeypatch.setattr(sys, 'module_stamped', True, raising=False)
""")
testdir.makepyfile(test_inv_mokeypatch_1="""
import sys
def test_stamp_1(monkeypatch, stamp_sys):
assert sys.module_stamped
monkeypatch.setattr(sys, 'function_stamped', True, raising=False)
assert sys.function_stamped
def test_stamp_2(monkeypatch):
assert sys.module_stamped
assert not hasattr(sys, 'function_stamped')
""")
testdir.makepyfile(test_inv_mokeypatch_2="""
import sys
def test_no_stamps():
assert not hasattr(sys, 'module_stamped')
assert not hasattr(sys, 'function_stamped')
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['*3 passed*'])