diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4fb0c07ce..47a77a3e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 (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``). Thanks to `@anntzer`_ for the PR. diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 383e41bad..e7f5b6d05 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -274,10 +274,8 @@ class FixtureRequest(FuncargnamesCompatAttr): self.fixturename = None #: Scope string, one of "function", "class", "module", "session" self.scope = "function" - # rename both attributes below because their key has changed; better an attribute error - # than subtle key misses; also backward incompatibility - self._fixture_values = {} # (argname, scope) -> fixture value - self._fixture_defs = {} # (argname, scope) -> FixtureDef + self._fixture_values = {} # argname -> fixture value + self._fixture_defs = {} # argname -> FixtureDef fixtureinfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} @@ -294,31 +292,20 @@ class FixtureRequest(FuncargnamesCompatAttr): return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname, scope): - def trygetfixturedefs(argname): - fixturedefs = self._arg2fixturedefs.get(argname, None) - if fixturedefs is None: - fixturedefs = self._arg2fixturedefs.get(argname + ':' + scope, None) - return fixturedefs - - fixturedefs = trygetfixturedefs(argname) + def _getnextfixturedef(self, argname): + fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: # we arrive here because of a a dynamic call to # getfixturevalue(argname) usage which was naturally # not known at parsing/collection time parentid = self._pyfuncitem.parent.nodeid fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - if 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) + self._arg2fixturedefs[argname] = fixturedefs # 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)): raise FixtureLookupError(argname, self) - self._arg2index[(argname, scope)] = index + self._arg2index[argname] = index return fixturedefs[index] @property @@ -458,10 +445,10 @@ class FixtureRequest(FuncargnamesCompatAttr): def _get_active_fixturedef(self, argname): try: - return self._fixture_defs[(argname, self.scope)] + return self._fixture_defs[argname] except KeyError: try: - fixturedef = self._getnextfixturedef(argname, self.scope) + fixturedef = self._getnextfixturedef(argname) except FixtureLookupError: if argname == "request": class PseudoFixtureDef: @@ -472,8 +459,8 @@ class FixtureRequest(FuncargnamesCompatAttr): # remove indent to prevent the python3 exception # from leaking into the call result = self._getfixturevalue(fixturedef) - self._fixture_values[(argname, self.scope)] = result - self._fixture_defs[(argname, self.scope)] = fixturedef + self._fixture_values[argname] = result + self._fixture_defs[argname] = fixturedef return fixturedef def _get_fixturestack(self): @@ -615,16 +602,6 @@ def scopemismatch(currentscope, newscope): 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): """ could not return a requested Fixture (missing or invalid). """ def __init__(self, argname, request, msg=None): @@ -664,7 +641,6 @@ class FixtureLookupError(LookupError): parentid = self.request._pyfuncitem.parent.nodeid for name, fixturedefs in fm._arg2fixturedefs.items(): faclist = list(fm._matchfactories(fixturedefs, parentid)) - name = strip_invocation_scope_suffix(name) if faclist and name not in available: available.append(name) 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. :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 invocations of the fixture function and all of the tests @@ -1029,11 +1005,6 @@ class FixtureManager: if fixturedefs: arg2fixturedefs[argname] = fixturedefs 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 def pytest_generate_tests(self, metafunc): @@ -1093,30 +1064,22 @@ class FixtureManager: 'and be decorated with @pytest.fixture:\n%s' % name 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, - scope, marker.params, - unittest=unittest, ids=marker.ids) + fixture_def = FixtureDef(self, nodeid, name, obj, + marker.scope, marker.params, + unittest=unittest, ids=marker.ids) - faclist = self._arg2fixturedefs.setdefault(name, []) - if fixture_def.has_location: - faclist.append(fixture_def) - else: - # fixturedefs with no location are at the front - # so this inserts the current fixturedef after the - # existing fixturedefs from external plugins but - # before the fixturedefs provided in conftests. - i = len([f for f in faclist if not f.has_location]) - faclist.insert(i, fixture_def) - if marker.autouse: - autousenames.append(name) - - if marker.scope == 'invocation': - for new_scope in scopes: - new_fixture_def(name + ':{0}'.format(new_scope), new_scope) + faclist = self._arg2fixturedefs.setdefault(name, []) + if fixture_def.has_location: + faclist.append(fixture_def) else: - new_fixture_def(name, marker.scope) + # fixturedefs with no location are at the front + # so this inserts the current fixturedef after the + # existing fixturedefs from external plugins but + # before the fixturedefs provided in conftests. + i = len([f for f in faclist if not f.has_location]) + faclist.insert(i, fixture_def) + if marker.autouse: + autousenames.append(name) if autousenames: self._nodeid_and_autousenames.append((nodeid or '', autousenames)) @@ -1141,23 +1104,3 @@ class FixtureManager: if nodeid.startswith(fixturedef.baseid): 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: ":" (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 diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 3c61d6f5b..852e72bed 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -10,7 +10,7 @@ import pytest RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") -@pytest.fixture(scope='invocation') +@pytest.fixture def monkeypatch(request): """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -28,8 +28,6 @@ def monkeypatch(request): test function or fixture has finished. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - - This fixture is ``invocation``-scoped. """ mpatch = MonkeyPatch() request.addfinalizer(mpatch.undo) diff --git a/_pytest/python.py b/_pytest/python.py index 4d2155aee..860c7da8c 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1008,7 +1008,6 @@ def showfixtures(config): def _showfixtures_main(config, session): import _pytest.config - from _pytest.fixtures import strip_invocation_scope_suffix session.perform_collect() curdir = py.path.local() tw = _pytest.config.create_terminal_writer(config) @@ -1025,16 +1024,13 @@ def _showfixtures_main(config, session): continue for fixturedef in fixturedefs: loc = getlocation(fixturedef.func, curdir) - # invocation-scoped fixtures have argname in the form - # ":" (for example: "monkeypatch:session"). - fixture_argname = strip_invocation_scope_suffix(fixturedef.argname) - if (fixture_argname, loc) in seen: + if (fixturedef.argname, loc) in seen: continue - seen.add((fixture_argname, loc)) + seen.add((fixturedef.argname, loc)) available.append((len(fixturedef.baseid), fixturedef.func.__module__, curdir.bestrelpath(loc), - fixture_argname, fixturedef)) + fixturedef.argname, fixturedef)) available.sort() currentmodule = None diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 95d2357c7..07146e982 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -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 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 ---------------------------------------------------- diff --git a/doc/en/invocation-fixture.rst b/doc/en/invocation-fixture.rst deleted file mode 100644 index 80f00adf7..000000000 --- a/doc/en/invocation-fixture.rst +++ /dev/null @@ -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. - diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index cc6b94958..229e1d982 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -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/ -As of pytest-3.0, the ``monkeypatch`` fixture is :ref:`invocation-scoped ` -meaning it can be requested from fixtures of any scope. Simple example: monkeypatching functions --------------------------------------------------- diff --git a/testing/python/fixture.py b/testing/python/fixture.py index d1f507a7f..800fcc81a 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -433,8 +433,7 @@ class TestFillFixtures: "*1 error*", ]) 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): # on python2 sys.excinfo would leak into fixture executions @@ -2743,13 +2742,6 @@ class TestShowFixtures: 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']) class TestContextManagerFixtureFuncs: diff --git a/testing/python/invocation_scope.py b/testing/python/invocation_scope.py deleted file mode 100644 index 81801067d..000000000 --- a/testing/python/invocation_scope.py +++ /dev/null @@ -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 '' - - @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 == '' - """) - 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 == '' - """) - 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 *') - - diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 0c152a8b6..3fcd20f32 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -331,33 +331,3 @@ def test_issue1338_name_resolving(): 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*'])