Merge pull request #1711 from nicoddemus/invocation-scoped-fixtures
Invocation scoped fixtures
This commit is contained in:
		
						commit
						ae0798522f
					
				|  | @ -135,9 +135,17 @@ 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. | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| * | * | ||||||
| 
 | 
 | ||||||
| * | * | ||||||
|  |  | ||||||
|  | @ -161,7 +161,7 @@ def capsys(request): | ||||||
|     captured output available via ``capsys.readouterr()`` method calls |     captured output available via ``capsys.readouterr()`` method calls | ||||||
|     which return a ``(out, err)`` tuple. |     which return a ``(out, err)`` tuple. | ||||||
|     """ |     """ | ||||||
|     if "capfd" in request._funcargs: |     if "capfd" in request.fixturenames: | ||||||
|         raise request.raiseerror(error_capsysfderror) |         raise request.raiseerror(error_capsysfderror) | ||||||
|     request.node._capfuncarg = c = CaptureFixture(SysCapture, request) |     request.node._capfuncarg = c = CaptureFixture(SysCapture, request) | ||||||
|     return c |     return c | ||||||
|  | @ -172,7 +172,7 @@ def capfd(request): | ||||||
|     captured output available via ``capfd.readouterr()`` method calls |     captured output available via ``capfd.readouterr()`` method calls | ||||||
|     which return a ``(out, err)`` tuple. |     which return a ``(out, err)`` tuple. | ||||||
|     """ |     """ | ||||||
|     if "capsys" in request._funcargs: |     if "capsys" in request.fixturenames: | ||||||
|         request.raiseerror(error_capsysfderror) |         request.raiseerror(error_capsysfderror) | ||||||
|     if not hasattr(os, 'dup'): |     if not hasattr(os, 'dup'): | ||||||
|         pytest.skip("capfd funcarg needs os.dup") |         pytest.skip("capfd funcarg needs os.dup") | ||||||
|  |  | ||||||
|  | @ -260,8 +260,6 @@ class FuncFixtureInfo: | ||||||
|         self.name2fixturedefs = name2fixturedefs |         self.name2fixturedefs = name2fixturedefs | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class FixtureRequest(FuncargnamesCompatAttr): | class FixtureRequest(FuncargnamesCompatAttr): | ||||||
|     """ A request for a fixture from a test or fixture function. |     """ A request for a fixture from a test or fixture function. | ||||||
| 
 | 
 | ||||||
|  | @ -276,34 +274,51 @@ 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" | ||||||
|         self._funcargs  = {} |         # rename both attributes below because their key has changed; better an attribute error | ||||||
|         self._fixturedefs = {} |         # than subtle key misses; also backward incompatibility | ||||||
|  |         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 = {} | ||||||
|         self.fixturenames = fixtureinfo.names_closure |  | ||||||
|         self._fixturemanager = pyfuncitem.session._fixturemanager |         self._fixturemanager = pyfuncitem.session._fixturemanager | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def fixturenames(self): | ||||||
|  |         # backward incompatible note: now a readonly property | ||||||
|  |         return list(self._pyfuncitem._fixtureinfo.names_closure) | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def node(self): |     def node(self): | ||||||
|         """ underlying collection node (depends on current request scope)""" |         """ underlying collection node (depends on current request scope)""" | ||||||
|         return self._getscopeitem(self.scope) |         return self._getscopeitem(self.scope) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _getnextfixturedef(self, argname): |     def _getnextfixturedef(self, argname, scope): | ||||||
|         fixturedefs = self._arg2fixturedefs.get(argname, None) |         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) | ||||||
|         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 | ||||||
|             fixturedefs = self._fixturemanager.getfixturedefs( |             parentid = self._pyfuncitem.parent.nodeid | ||||||
|                             argname, self._pyfuncitem.parent.nodeid) |             fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) | ||||||
|             self._arg2fixturedefs[argname] = fixturedefs |             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) | ||||||
|         # 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, 0) - 1 |         index = self._arg2index.get((argname, scope), 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] = index |         self._arg2index[(argname, scope)] = index | ||||||
|         return fixturedefs[index] |         return fixturedefs[index] | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | @ -442,10 +457,10 @@ class FixtureRequest(FuncargnamesCompatAttr): | ||||||
| 
 | 
 | ||||||
|     def _get_active_fixturedef(self, argname): |     def _get_active_fixturedef(self, argname): | ||||||
|         try: |         try: | ||||||
|             return self._fixturedefs[argname] |             return self._fixture_defs[(argname, self.scope)] | ||||||
|         except KeyError: |         except KeyError: | ||||||
|             try: |             try: | ||||||
|                 fixturedef = self._getnextfixturedef(argname) |                 fixturedef = self._getnextfixturedef(argname, self.scope) | ||||||
|             except FixtureLookupError: |             except FixtureLookupError: | ||||||
|                 if argname == "request": |                 if argname == "request": | ||||||
|                     class PseudoFixtureDef: |                     class PseudoFixtureDef: | ||||||
|  | @ -456,8 +471,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._funcargs[argname] = result |         self._fixture_values[(argname, self.scope)] = result | ||||||
|         self._fixturedefs[argname] = fixturedef |         self._fixture_defs[(argname, self.scope)] = fixturedef | ||||||
|         return fixturedef |         return fixturedef | ||||||
| 
 | 
 | ||||||
|     def _get_fixturestack(self): |     def _get_fixturestack(self): | ||||||
|  | @ -578,11 +593,10 @@ class SubRequest(FixtureRequest): | ||||||
|         self._fixturedef = fixturedef |         self._fixturedef = fixturedef | ||||||
|         self.addfinalizer = fixturedef.addfinalizer |         self.addfinalizer = fixturedef.addfinalizer | ||||||
|         self._pyfuncitem = request._pyfuncitem |         self._pyfuncitem = request._pyfuncitem | ||||||
|         self._funcargs  = request._funcargs |         self._fixture_values  = request._fixture_values | ||||||
|         self._fixturedefs = request._fixturedefs |         self._fixture_defs = request._fixture_defs | ||||||
|         self._arg2fixturedefs = request._arg2fixturedefs |         self._arg2fixturedefs = request._arg2fixturedefs | ||||||
|         self._arg2index = request._arg2index |         self._arg2index = request._arg2index | ||||||
|         self.fixturenames = request.fixturenames |  | ||||||
|         self._fixturemanager = request._fixturemanager |         self._fixturemanager = request._fixturemanager | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|  | @ -622,7 +636,7 @@ class FixtureLookupError(LookupError): | ||||||
|             fspath, lineno = getfslineno(function) |             fspath, lineno = getfslineno(function) | ||||||
|             try: |             try: | ||||||
|                 lines, _ = inspect.getsourcelines(get_real_func(function)) |                 lines, _ = inspect.getsourcelines(get_real_func(function)) | ||||||
|             except (IOError, IndexError): |             except (IOError, IndexError, TypeError): | ||||||
|                 error_msg = "file %s, line %s: source code not available" |                 error_msg = "file %s, line %s: source code not available" | ||||||
|                 addline(error_msg % (fspath, lineno+1)) |                 addline(error_msg % (fspath, lineno+1)) | ||||||
|             else: |             else: | ||||||
|  | @ -636,9 +650,9 @@ class FixtureLookupError(LookupError): | ||||||
|         if msg is None: |         if msg is None: | ||||||
|             fm = self.request._fixturemanager |             fm = self.request._fixturemanager | ||||||
|             available = [] |             available = [] | ||||||
|             for name, fixturedef in fm._arg2fixturedefs.items(): |             parentid = self.request._pyfuncitem.parent.nodeid | ||||||
|                 parentid = self.request._pyfuncitem.parent.nodeid |             for name, fixturedefs in fm._arg2fixturedefs.items(): | ||||||
|                 faclist = list(fm._matchfactories(fixturedef, parentid)) |                 faclist = list(fm._matchfactories(fixturedefs, parentid)) | ||||||
|                 if faclist: |                 if faclist: | ||||||
|                     available.append(name) |                     available.append(name) | ||||||
|             msg = "fixture %r not found" % (self.argname,) |             msg = "fixture %r not found" % (self.argname,) | ||||||
|  | @ -749,7 +763,7 @@ class FixtureDef: | ||||||
|             assert not hasattr(self, "cached_result") |             assert not hasattr(self, "cached_result") | ||||||
| 
 | 
 | ||||||
|         ihook = self._fixturemanager.session.ihook |         ihook = self._fixturemanager.session.ihook | ||||||
|         ihook.pytest_fixture_setup(fixturedef=self, request=request) |         return ihook.pytest_fixture_setup(fixturedef=self, request=request) | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return ("<FixtureDef name=%r scope=%r baseid=%r >" % |         return ("<FixtureDef name=%r scope=%r baseid=%r >" % | ||||||
|  | @ -984,10 +998,12 @@ class FixtureManager: | ||||||
| 
 | 
 | ||||||
|         parentid = parentnode.nodeid |         parentid = parentnode.nodeid | ||||||
|         fixturenames_closure = self._getautousenames(parentid) |         fixturenames_closure = self._getautousenames(parentid) | ||||||
|  | 
 | ||||||
|         def merge(otherlist): |         def merge(otherlist): | ||||||
|             for arg in otherlist: |             for arg in otherlist: | ||||||
|                 if arg not in fixturenames_closure: |                 if arg not in fixturenames_closure: | ||||||
|                     fixturenames_closure.append(arg) |                     fixturenames_closure.append(arg) | ||||||
|  | 
 | ||||||
|         merge(fixturenames) |         merge(fixturenames) | ||||||
|         arg2fixturedefs = {} |         arg2fixturedefs = {} | ||||||
|         lastlen = -1 |         lastlen = -1 | ||||||
|  | @ -1000,6 +1016,11 @@ 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): | ||||||
|  | @ -1018,7 +1039,7 @@ class FixtureManager: | ||||||
|                                              indirect=True, scope=fixturedef.scope, |                                              indirect=True, scope=fixturedef.scope, | ||||||
|                                              ids=fixturedef.ids) |                                              ids=fixturedef.ids) | ||||||
|             else: |             else: | ||||||
|                 continue # will raise FixtureLookupError at setup time |                 continue  # will raise FixtureLookupError at setup time | ||||||
| 
 | 
 | ||||||
|     def pytest_collection_modifyitems(self, items): |     def pytest_collection_modifyitems(self, items): | ||||||
|         # separate parametrized setups |         # separate parametrized setups | ||||||
|  | @ -1057,25 +1078,43 @@ class FixtureManager: | ||||||
|                 msg = 'fixtures cannot have "pytest_funcarg__" prefix ' \ |                 msg = 'fixtures cannot have "pytest_funcarg__" prefix ' \ | ||||||
|                       '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 | ||||||
|             fixturedef = FixtureDef(self, nodeid, name, obj, | 
 | ||||||
|                                     marker.scope, marker.params, |             def new_fixture_def(name, scope): | ||||||
|                                     unittest=unittest, ids=marker.ids) |                 """Create and registers a new FixtureDef with given name and scope.""" | ||||||
|             faclist = self._arg2fixturedefs.setdefault(name, []) |                 fixture_def = FixtureDef(self, nodeid, name, obj, | ||||||
|             if fixturedef.has_location: |                                          scope, marker.params, | ||||||
|                 faclist.append(fixturedef) |                                          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) | ||||||
|             else: |             else: | ||||||
|                 # fixturedefs with no location are at the front |                 new_fixture_def(name, marker.scope) | ||||||
|                 # 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, fixturedef) |  | ||||||
|             if marker.autouse: |  | ||||||
|                 autousenames.append(name) |  | ||||||
|         if autousenames: |         if autousenames: | ||||||
|             self._nodeid_and_autousenames.append((nodeid or '', autousenames)) |             self._nodeid_and_autousenames.append((nodeid or '', autousenames)) | ||||||
| 
 | 
 | ||||||
|     def getfixturedefs(self, argname, nodeid): |     def getfixturedefs(self, argname, nodeid): | ||||||
|  |         """ | ||||||
|  |         Gets a list of fixtures which are applicable to the given node id. | ||||||
|  | 
 | ||||||
|  |         :param str argname: name of the fixture to search for | ||||||
|  |         :param str nodeid: full node id of the requesting test. | ||||||
|  |         :return: list[FixtureDef] | ||||||
|  |         """ | ||||||
|         try: |         try: | ||||||
|             fixturedefs = self._arg2fixturedefs[argname] |             fixturedefs = self._arg2fixturedefs[argname] | ||||||
|         except KeyError: |         except KeyError: | ||||||
|  | @ -1087,3 +1126,24 @@ class FixtureManager: | ||||||
|         for fixturedef in fixturedefs: |         for fixturedef in fixturedefs: | ||||||
|             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 | ||||||
|  |  | ||||||
|  | @ -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 | @pytest.fixture(scope='invocation') | ||||||
| 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:: | ||||||
|  | @ -25,9 +25,11 @@ def monkeypatch(request): | ||||||
|         monkeypatch.chdir(path) |         monkeypatch.chdir(path) | ||||||
| 
 | 
 | ||||||
|     All modifications will be undone after the requesting |     All modifications will be undone after the requesting | ||||||
|     test function 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) | ||||||
|  | @ -97,7 +99,8 @@ notset = Notset() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MonkeyPatch: | class MonkeyPatch: | ||||||
|     """ Object keeping a record of setattr/item/env/syspath changes. """ |     """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self._setattr = [] |         self._setattr = [] | ||||||
|  |  | ||||||
|  | @ -1506,4 +1506,3 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): | ||||||
|         super(Function, self).setup() |         super(Function, self).setup() | ||||||
|         fixtures.fillfixtures(self) |         fixtures.fillfixtures(self) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -603,7 +603,7 @@ first execute with one instance and then finalizers are called | ||||||
| before the next fixture instance is created.  Among other things, | before the next fixture instance is created.  Among other things, | ||||||
| this eases testing of applications which create and use global state. | this eases testing of applications which create and use global state. | ||||||
| 
 | 
 | ||||||
| The following example uses two parametrized funcargs, one of which is | The following example uses two parametrized fixture, one of which is | ||||||
| scoped on a per-module basis, and all the functions perform ``print`` calls | scoped on a per-module basis, and all the functions perform ``print`` calls | ||||||
| to show the setup/teardown flow:: | to show the setup/teardown flow:: | ||||||
| 
 | 
 | ||||||
|  | @ -863,6 +863,14 @@ 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 | ||||||
| ---------------------------------------------------- | ---------------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | .. _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. | ||||||
|  | 
 | ||||||
|  | @ -6,7 +6,7 @@ Monkeypatching/mocking modules and environments | ||||||
| 
 | 
 | ||||||
| Sometimes tests need to invoke functionality which depends | Sometimes tests need to invoke functionality which depends | ||||||
| on global settings or which invokes code which cannot be easily | on global settings or which invokes code which cannot be easily | ||||||
| tested such as network access.  The ``monkeypatch`` function argument | tested such as network access.  The ``monkeypatch`` fixture | ||||||
| helps you to safely set/delete an attribute, dictionary item or | helps you to safely set/delete an attribute, dictionary item or | ||||||
| environment variable or to modify ``sys.path`` for importing. | environment variable or to modify ``sys.path`` for importing. | ||||||
| See the `monkeypatch blog post`_ for some introduction material | See the `monkeypatch blog post`_ for some introduction material | ||||||
|  | @ -14,6 +14,9 @@ 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 | ||||||
| --------------------------------------------------- | --------------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  | @ -53,27 +56,31 @@ This autouse fixture will be executed for each test function and it | ||||||
| will delete the method ``request.session.Session.request``  | will delete the method ``request.session.Session.request``  | ||||||
| so that any attempts within tests to create http requests will fail. | so that any attempts within tests to create http requests will fail. | ||||||
| 
 | 
 | ||||||
| example: setting an attribute on some class | example: setting an environment variable for the test session | ||||||
| ------------------------------------------------------ | ------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| If you need to patch out ``os.getcwd()`` to return an artificial | If you would like for an environment variable to be | ||||||
| value:: | configured for the entire test session, you can add this to your | ||||||
|  | top-level ``conftest.py`` file: | ||||||
| 
 | 
 | ||||||
|     def test_some_interaction(monkeypatch): | .. code-block:: python | ||||||
|         monkeypatch.setattr("os.getcwd", lambda: "/") |  | ||||||
| 
 | 
 | ||||||
| which is equivalent to the long form:: |     # content of conftest.py | ||||||
|  |     @pytest.fixture(scope='session', autouse=True) | ||||||
|  |     def enable_debugging(monkeypatch): | ||||||
|  |         monkeypatch.setenv("DEBUGGING_VERBOSITY", "4") | ||||||
| 
 | 
 | ||||||
|     def test_some_interaction(monkeypatch): | This auto-use fixture will set the ``DEBUGGING_VERBOSITY`` environment variable for | ||||||
|         import os | the entire test session. | ||||||
|         monkeypatch.setattr(os, "getcwd", lambda: "/") | 
 | ||||||
|      | Note that the ability to use a ``monkeypatch`` fixture from a ``session``-scoped | ||||||
|  | fixture was added in pytest-3.0. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Method reference of the monkeypatch function argument | Method reference of the monkeypatch fixture | ||||||
| ----------------------------------------------------- | ------------------------------------------- | ||||||
| 
 | 
 | ||||||
| .. autoclass:: monkeypatch | .. autoclass:: MonkeyPatch | ||||||
|     :members: setattr, replace, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo |     :members: setattr, replace, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo | ||||||
| 
 | 
 | ||||||
| ``monkeypatch.setattr/delattr/delitem/delenv()`` all | ``monkeypatch.setattr/delattr/delitem/delenv()`` all | ||||||
|  |  | ||||||
|  | @ -0,0 +1,214 @@ | ||||||
|  | 
 | ||||||
|  | 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 *') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @ -421,6 +421,25 @@ class TestCaptureFixture: | ||||||
|             "*capsys*capfd*same*time*", |             "*capsys*capfd*same*time*", | ||||||
|             "*2 error*"]) |             "*2 error*"]) | ||||||
| 
 | 
 | ||||||
|  |     def test_capturing_getfixturevalue(self, testdir): | ||||||
|  |         """Test that asking for "capfd" and "capsys" using request.getfixturevalue | ||||||
|  |         in the same test is an error. | ||||||
|  |         """ | ||||||
|  |         testdir.makepyfile(""" | ||||||
|  |             def test_one(capsys, request): | ||||||
|  |                 request.getfixturevalue("capfd") | ||||||
|  |             def test_two(capfd, request): | ||||||
|  |                 request.getfixturevalue("capsys") | ||||||
|  |         """) | ||||||
|  |         result = testdir.runpytest() | ||||||
|  |         result.stdout.fnmatch_lines([ | ||||||
|  |             "*test_one*", | ||||||
|  |             "*capsys*capfd*same*time*", | ||||||
|  |             "*test_two*", | ||||||
|  |             "*capsys*capfd*same*time*", | ||||||
|  |             "*2 failed in*", | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|     @pytest.mark.parametrize("method", ["sys", "fd"]) |     @pytest.mark.parametrize("method", ["sys", "fd"]) | ||||||
|     def test_capture_is_represented_on_failure_issue128(self, testdir, method): |     def test_capture_is_represented_on_failure_issue128(self, testdir, method): | ||||||
|         p = testdir.makepyfile(""" |         p = testdir.makepyfile(""" | ||||||
|  |  | ||||||
|  | @ -328,4 +328,36 @@ def test_issue1338_name_resolving(): | ||||||
|     try: |     try: | ||||||
|          monkeypatch.delattr('requests.sessions.Session.request') |          monkeypatch.delattr('requests.sessions.Session.request') | ||||||
|     finally: |     finally: | ||||||
|         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*']) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue