diff --git a/CHANGELOG b/CHANGELOG index 1edca48e7..1d4581c16 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,19 +9,9 @@ Changes between 2.2.4 and 2.3.0.dev it is constructed correctly from the original current working dir. - fix "python setup.py test" example to cause a proper "errno" return - fix issue165 - fix broken doc links and mention stackoverflow for FAQ -- fix issue139 - merge FuncargRequest and Item API such that - funcarg-functionality is now directly available on the "item" - object passed to the various pytest_runtest hooks. This allows more - sensitive behaviour of e.g. the pytest-django plugin which previously - had no full access to all instantiated funcargs. - This internal API re-organisation is a fully backward compatible - change: existing factories accepting a "request" object will - get a Function "item" object which carries the same API. In fact, - the FuncargRequest API (or rather then a ResourceRequestAPI) - could be available for all collection and item nodes but this is - left for later consideration because it would render the documentation - invalid and the "funcarg" naming sounds odd in context of - directory, file, class, etc. nodes. +- fix issue139 - introduce @pytest.factory which allows direct scoping + and parametrization of funcarg factories. Introduce new @pytest.setup + marker to allow the writing of setup functions which accept funcargs. - catch unicode-issues when writing failure representations to terminal to prevent the whole session from crashing - fix xfail/skip confusion: a skip-mark or an imperative pytest.skip diff --git a/_pytest/python.py b/_pytest/python.py index f5ce59662..b9096c59a 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -912,31 +912,51 @@ class Function(FunctionMixin, pytest.Item): def __hash__(self): return hash((self.parent, self.name)) +scope2props = dict(session=()) +scope2props["module"] = ("fspath", "module") +scope2props["class"] = scope2props["module"] + ("cls",) +scope2props["instance"] = scope2props["class"] + ("instance", ) +scope2props["function"] = scope2props["instance"] + ("function", "keywords") + +def scopeproperty(name=None, doc=None): + def decoratescope(func): + scopename = name or func.__name__ + def provide(self): + if func.__name__ in scope2props[self.scope]: + return func(self) + raise AttributeError("%s not available in %s-scoped context" % ( + scopename, self.scope)) + return property(provide, None, None, func.__doc__) + return decoratescope + +def pytest_funcarg__request(__request__): + return __request__ + +#def pytest_funcarg__testcontext(__request__): +# return __request__ class FuncargRequest: - """ (old-style) A request for function arguments from a test function. + """ A request for function arguments from a test or setup function. - Note that there is an optional ``param`` attribute in case - there was an invocation to metafunc.addcall(param=...). - If no such call was done in a ``pytest_generate_tests`` - hook, the attribute will not be present. Note that - as of pytest-2.3 you probably rather want to use the - testcontext object and mark your factory with a ``@pytest.factory`` - marker. + A request object gives access to attributes of the requesting + test context. It has an optional ``param`` attribute in case + of parametrization. """ def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem if hasattr(pyfuncitem, '_requestparam'): self.param = pyfuncitem._requestparam + #: Scope string, one of "function", "cls", "module", "session" + self.scope = "function" self.getparent = pyfuncitem.getparent self._funcargs = self._pyfuncitem.funcargs.copy() + self._funcargs["__request__"] = self self._name2factory = {} self.funcargmanager = pyfuncitem.session.funcargmanager self._currentarg = None self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid - self.scope = "function" self._factorystack = [] def _getfaclist(self, argname): @@ -956,21 +976,42 @@ class FuncargRequest: self.parentid) return facdeflist - def raiseerror(self, msg): - """ raise a FuncargLookupError with the given message. """ - raise self.funcargmanager.FuncargLookupError(self.function, msg) - @property + def config(self): + """ the pytest config object associated with this request. """ + return self._pyfuncitem.config + + + @scopeproperty() def function(self): - """ function object of the test invocation. """ + """ test function object if the request has a per-function scope. """ return self._pyfuncitem.obj + @scopeproperty("class") + def cls(self): + """ class (can be None) where the test function was collected. """ + clscol = self._pyfuncitem.getparent(pytest.Class) + if clscol: + return clscol.obj + + @scopeproperty() + def instance(self): + """ instance (can be None) on which test function was collected. """ + return py.builtin._getimself(self.function) + + @scopeproperty() + def module(self): + """ python module object where the test function was collected. """ + return self._pyfuncitem.getparent(pytest.Module).obj + + @scopeproperty() + def fspath(self): + """ the file system path of the test module which collected this test. """ + return self._pyfuncitem.fspath + @property def keywords(self): - """ keywords of the test function item. - - .. versionadded:: 2.0 - """ + """ keywords of the test function item. """ return self._pyfuncitem.keywords @property @@ -978,41 +1019,23 @@ class FuncargRequest: """ pytest session object. """ return self._pyfuncitem.session - @property - def module(self): - """ module where the test function was collected. """ - return self._pyfuncitem.getparent(pytest.Module).obj - @property - def cls(self): - """ class (can be None) where the test function was collected. """ - clscol = self._pyfuncitem.getparent(pytest.Class) - if clscol: - return clscol.obj - @property - def instance(self): - """ instance (can be None) on which test function was collected. """ - return py.builtin._getimself(self.function) - @property - def config(self): - """ the pytest config object associated with this request. """ - return self._pyfuncitem.config + def addfinalizer(self, finalizer): + """add finalizer/teardown function to be called after the + last test within the requesting test context finished + execution. """ + self._addfinalizer(finalizer, scope=self.scope) - @property - def fspath(self): - """ the file system path of the test module which collected this test. """ - return self._pyfuncitem.fspath - - def _fillfuncargs(self): - if self.funcargnames: - assert not getattr(self._pyfuncitem, '_args', None), ( - "yielded functions cannot have funcargs") - while self.funcargnames: - argname = self.funcargnames.pop(0) - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = \ - self.getfuncargvalue(argname) + def _addfinalizer(self, finalizer, scope): + if scope != "function" and hasattr(self, "param"): + # parametrized resources are sorted by param + # so we rather store finalizers per (argname, param) + colitem = (self._currentarg, self.param) + else: + colitem = self._getscopeitem(scope) + self._pyfuncitem.session._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) def applymarker(self, marker): """ Apply a marker to a single test function invocation. @@ -1026,11 +1049,33 @@ class FuncargRequest: raise ValueError("%r is not a py.test.mark.* object") self._pyfuncitem.keywords[marker.markname] = marker + def raiseerror(self, msg): + """ raise a FuncargLookupError with the given message. """ + raise self.funcargmanager.FuncargLookupError(self.function, msg) + + + def _fillfuncargs(self): + if self.funcargnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + while self.funcargnames: + argname = self.funcargnames.pop(0) + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = \ + self.getfuncargvalue(argname) + + + def _callsetup(self): + self.funcargmanager.ensure_setupcalls(self) + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): - """ Return a testing resource managed by ``setup`` & + """ (deprecated) Return a testing resource managed by ``setup`` & ``teardown`` calls. ``scope`` and ``extrakey`` determine when the ``teardown`` function will be called so that subsequent calls to - ``setup`` would recreate the resource. + ``setup`` would recreate the resource. With pytest-2.3 you + do not need ``cached_setup()`` as you can directly declare a scope + on a funcarg factory and register a finalizer through + ``request.addfinalizer()``. :arg teardown: function receiving a previously setup resource. :arg setup: a no-argument function creating a resource. @@ -1061,16 +1106,19 @@ class FuncargRequest: return val - def _callsetup(self): - self.funcargmanager.ensure_setupcalls(self) def getfuncargvalue(self, argname): - """ Retrieve a function argument by name for this test + """ (deprecated) Retrieve a function argument by name for this test function invocation. This allows one function argument factory to call another function argument factory. If there are two funcarg factories for the same test function argument the first factory may use ``getfuncargvalue`` to call the second one and do something additional with the resource. + + **Note**, however, that starting with + pytest-2.3 it is easier and better to directly state the needed + funcarg in the factory signature. This will also work seemlessly + with parametrization and the new resource setup optimizations. """ try: return self._funcargs[argname] @@ -1091,12 +1139,7 @@ class FuncargRequest: factory_kwargs = {} def fillfactoryargs(): for newname in newnames: - if newname == "testcontext": - val = TestContextResource(self) - elif newname == "request" and not factorydef.new: - val = self - else: - val = self.getfuncargvalue(newname) + val = self.getfuncargvalue(newname) factory_kwargs[newname] = val node = self._pyfuncitem @@ -1161,21 +1204,6 @@ class FuncargRequest: return self._pyfuncitem.getparent(pytest.Module) raise ValueError("unknown finalization scope %r" %(scope,)) - def addfinalizer(self, finalizer): - """add finalizer function to be called after test function - finished execution. """ - self._addfinalizer(finalizer, scope=self.scope) - - def _addfinalizer(self, finalizer, scope): - if scope != "function" and hasattr(self, "param"): - # parametrized resources are sorted by param - # so we rather store finalizers per (argname, param) - colitem = (self._currentarg, self.param) - else: - colitem = self._getscopeitem(scope) - self._pyfuncitem.session._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem) - def __repr__(self): return "" %(self._pyfuncitem) @@ -1396,17 +1424,13 @@ class FuncargManager: if setupcall.active: continue request._factorystack.append(setupcall) + mp = monkeypatch() try: - testcontext = TestContextSetup(request, setupcall) + #mp.setattr(request, "_setupcall", setupcall, raising=False) + mp.setattr(request, "scope", setupcall.scope) kwargs = {} for name in setupcall.funcargnames: - try: - kwargs[name] = request.getfuncargvalue(name) - except FuncargLookupError: - if name == "testcontext": - kwargs[name] = testcontext - else: - raise + kwargs[name] = request.getfuncargvalue(name) scope = setupcall.scope or "function" scol = setupcall.scopeitem = request._getscopeitem(scope) self.session._setupstate.addfinalizer(setupcall.finish, scol) @@ -1414,6 +1438,7 @@ class FuncargManager: self.addargfinalizer(setupcall.finish, argname) setupcall.execute(kwargs) finally: + mp.undo() request._factorystack.remove(setupcall) def addargfinalizer(self, finalizer, argname): @@ -1427,69 +1452,6 @@ class FuncargManager: except ValueError: pass -scope2props = dict(session=()) -scope2props["module"] = ("fspath", "module") -scope2props["class"] = scope2props["module"] + ("cls",) -scope2props["function"] = scope2props["class"] + ("function", "keywords") - -def scopeprop(attr, name=None, doc=None): - if doc is None: - doc = ("%s of underlying test context, may not exist " - "if the testcontext has a higher scope" % (attr,)) - name = name or attr - def get(self): - if name in scope2props[self.scope]: - return getattr(self._request, name) - raise AttributeError("%s not available in %s-scoped context" % ( - name, self.scope)) - return property(get, doc=doc) - -def rprop(attr, doc=None): - if doc is None: - doc = "%s of underlying test context" % attr - return property(lambda x: getattr(x._request, attr), doc=doc) - -class TestContext(object): - """ Basic objects of the current testing context. """ - def __init__(self, request, scope): - self._request = request - self.scope = scope - - # no getfuncargvalue(), cached_setup, applymarker helpers here - # on purpose - - config = rprop("config", "pytest config object.") - session = rprop("session", "pytest session object.") - - function = scopeprop("function") - module = scopeprop("module") - cls = scopeprop("class", "cls") - instance = scopeprop("instance") - fspath = scopeprop("fspath") - #keywords = scopeprop("keywords") - -class TestContextSetup(TestContext): - def __init__(self, request, setupcall): - self._setupcall = setupcall - self._finalizers = [] - super(TestContextSetup, self).__init__(request, setupcall.scope) - - def addfinalizer(self, finalizer): - """ Add a finalizer to be called after the last test in the - test context executes. """ - self._setupcall.addfinalizer(finalizer) - -class TestContextResource(TestContext): - param = rprop("param") - - def __init__(self, request): - super(TestContextResource, self).__init__(request, request.scope) - - def addfinalizer(self, finalizer): - """ Add a finalizer to be called after the last test in the - test context executes. """ - self._request.addfinalizer(finalizer) - class SetupCall: """ a container/helper for managing calls to setup functions. """ diff --git a/doc/en/example/costlysetup/conftest.py b/doc/en/example/costlysetup/conftest.py index 4f456fae3..33ae48f39 100644 --- a/doc/en/example/costlysetup/conftest.py +++ b/doc/en/example/costlysetup/conftest.py @@ -2,9 +2,9 @@ import pytest @pytest.factory("session") -def setup(testcontext): +def setup(request): setup = CostlySetup() - testcontext.addfinalizer(setup.finalize) + request.addfinalizer(setup.finalize) return setup class CostlySetup: diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index af5377668..13f442354 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -6,13 +6,13 @@ import py, pytest pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8'] @pytest.factory(params=pythonlist) -def python1(testcontext, tmpdir): +def python1(request, tmpdir): picklefile = tmpdir.join("data.pickle") - return Python(testcontext.param, picklefile) + return Python(request.param, picklefile) @pytest.factory(params=pythonlist) -def python2(testcontext, python1): - return Python(testcontext.param, python1.picklefile) +def python2(request, python1): + return Python(request.param, python1.picklefile) class Python: def __init__(self, version, picklefile): diff --git a/doc/en/funcarg_compare.txt b/doc/en/funcarg_compare.txt index 7ca90619b..f7a092c73 100644 --- a/doc/en/funcarg_compare.txt +++ b/doc/en/funcarg_compare.txt @@ -66,15 +66,15 @@ Direct scoping of funcarg factories Instead of calling cached_setup(), you can use the :ref:`@pytest.factory <@pytest.factory>` decorator and directly state the scope:: @pytest.factory(scope="session") - def db(testcontext): + def db(request): # factory will only be invoked once per session - db = DataBase() - testcontext.addfinalizer(db.destroy) # destroy when session is finished + request.addfinalizer(db.destroy) # destroy when session is finished return db This factory implementation does not need to call ``cached_setup()`` anymore because it will only be invoked once per session. Moreover, the -``testcontext.addfinalizer()`` registers a finalizer according to the specified +``request.addfinalizer()`` registers a finalizer according to the specified resource scope on which the factory function is operating. @@ -88,29 +88,29 @@ parametrization, i.e. calling a test multiple times with different value sets. pytest-2.3 introduces a decorator for use on the factory itself:: @pytest.factory(params=["mysql", "pg"]) - def db(testcontext): - ... # use testcontext.param + def db(request): + ... # use request.param Here the factory will be invoked twice (with the respective "mysql" -and "pg" values set as ``testcontext.param`` attributes) and and all of +and "pg" values set as ``request.param`` attributes) and and all of the tests requiring "db" will run twice as well. The "mysql" and "pg" values will also be used for reporting the test-invocation variants. This new way of parametrizing funcarg factories should in many cases allow to re-use already written factories because effectively -``testcontext.param`` are already the parametrization attribute for test +``request.param`` are already the parametrization attribute for test functions/classes were parametrized via :py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls. Of course it's perfectly fine to combine parametrization and scoping:: @pytest.factory(scope="session", params=["mysql", "pg"]) - def db(testcontext): - if testcontext.param == "mysql": + def db(request): + if request.param == "mysql": db = MySQL() - elif testcontext.param == "pg": + elif request.param == "pg": db = PG() - testcontext.addfinalizer(db.destroy) # destroy when session is finished + request.addfinalizer(db.destroy) # destroy when session is finished return db This would execute all tests requiring the per-session "db" resource twice, @@ -126,7 +126,7 @@ denotes the name under which the resource can be accessed as a function argument:: @pytest.factory() - def db(testcontext): + def db(request): ... The name under which the funcarg resource can be requested is ``db``. diff --git a/doc/en/funcargs.txt b/doc/en/funcargs.txt index e77b69565..546b18e0b 100644 --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -9,44 +9,45 @@ funcargs: resource injection and parametrization .. note:: - pytest-2.3 introduces major refinements to the original funcarg - mechanism introduced to pytest-2.0. While the old way - remains fully supported, it is recommended to use the refined - mechanisms. See also the `compatibility notes`_ and the detailed - :ref:`reasoning for the new funcarg and setup functions `. + pytest-2.3 introduces major refinements to the test setup and funcarg + mechanisms introduced to pytest-2.0. All pre-2.3 usages remain + supported and several use cases, among them scoping and parametrization + of funcarg resources, are now easier to accomplish. For more background, + see `compatibility notes`_ and the detailed :ref:`reasoning for the new + funcarg and setup functions `. .. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection Introduction ==================== -py.test supports the injection of objects into test and setup functions +pytest supports the injection of test resources into test and setup functions and flexibly control their life cycle in relation to the overall test -execution. Moreover, you can run a test function multiple times -injecting different objects. +execution. Moreover, tests can get executed multiple times if you have +different variants of test resources to test with. The basic mechanism for injecting objects is called the *funcarg -mechanism* because objects are **injected** when a test or setup -function states it as an **argument**. The injected argument is -created by a call to a registered **funcarg factory**. This approach is -an example of `Dependency Injection`_ and helps to de-couple test code -from the setup of required objects: at test writing time you do not need -to care for the details of where and how your required resources are -constructed, if they are shared on a per-class, module or session basis, -or if your test function is invoked multiple times with differently -configured resource instances. +mechanism* because objects are injected when a test or setup +**function** states it as an **argument**. The injected argument +is created by a call to a registered **funcarg factory** for each argument +name. This mechanism is an example of `Dependency Injection`_ +and helps to de-couple test code from the setup of required +objects: at test writing time you do not need to care for the details of +where and how your required test resources are constructed, if they are +shared on a per-class, module or session basis, or if your test function +is invoked multiple times with differently configured resource +instances. + +Funcarg dependency injection allows to organise test resources +in a modular explicit way so that test functions state their needs +in their signature. pytest additionally offers powerful xunit-style +:ref:`setup functions ` for the cases where you need +to create implicit test state that is not passed explicitely to test functions. When a test function is invoked multiple times with different arguments we -speak of **parametrized testing**. This is useful if you want to test -e.g. against different database backends or want to write a parametrized -test function, checking that certain inputs lead to certain outputs. -You can parametrize funcarg factories, parametrize test function -arguments or even implement your own parametrization scheme through a -plugin hook. - -pytest additionally offers powerful xunit-style :ref:`setup functions ` for the cases where you need to create implicit test state -that is not passed explicitely to test functions. +speak of **parametrized testing**. You can use it e. g. to repeatedly run test +functions against different database backends or to check that certain +inputs lead to certain outputs. Concretely, there are three main means of funcarg management: @@ -54,7 +55,7 @@ Concretely, there are three main means of funcarg management: their scoping and parametrization. Factories can themselves receive resources through their function arguments, easing the setup of `interdependent resources`_. Factories can use - the special `testcontext`_ object to access details from where + the special `request`_ object to access details from where the factory or setup function is called and for registering finalizers. * a `@pytest.mark.parametrize`_ marker for executing test functions @@ -103,8 +104,8 @@ factory function. Running the tests looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev11 + plugins: xdist, bugzilla, cache, oejskit, cli, timeout, pep8, cov collecting ... collected 1 items test_simplefactory.py F @@ -119,7 +120,7 @@ factory function. Running the tests looks like this:: E assert 42 == 17 test_simplefactory.py:8: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.01 seconds ========================= This shows that the test function was called with a ``myfuncarg`` argument value of ``42`` and the assert fails as expected. Here is @@ -186,7 +187,7 @@ test session:: import smtplib @pytest.factory(scope="session") - def smtp(testcontext): + def smtp(request): return smtplib.SMTP("merlinux.eu") The name of the factory is ``smtp`` (the factory function name) @@ -214,7 +215,7 @@ inspect what is going on and can now run the tests:: ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -226,7 +227,7 @@ inspect what is going on and can now run the tests:: test_module.py:5: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -235,7 +236,7 @@ inspect what is going on and can now run the tests:: E assert 0 test_module.py:10: AssertionError - 2 failed in 0.20 seconds + 2 failed in 0.26 seconds you see the two ``assert 0`` failing and can also see that the same (session-scoped) object was passed into the two test functions. @@ -247,7 +248,7 @@ Parametrizing a session-shared funcarg resource Extending the previous example, we can flag the factory to create two ``smtp`` values which will cause all tests using it to run twice with two different values. The factory function gets -access to each parameter through the special `testcontext`_ object:: +access to each parameter through the special `request`_ object:: # content of conftest.py import pytest @@ -255,11 +256,11 @@ access to each parameter through the special `testcontext`_ object:: @pytest.factory(scope="session", params=["merlinux.eu", "mail.python.org"]) - def smtp(testcontext): - return smtplib.SMTP(testcontext.param) + def smtp(request): + return smtplib.SMTP(request.param) The main change is the definition of a ``params`` list in the -``factory``-marker and the ``testcontext.param`` access within the +``factory``-marker and the ``request.param`` access within the factory function. No test function code needs to change. So let's just do another run:: @@ -269,7 +270,7 @@ So let's just do another run:: ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -281,7 +282,7 @@ So let's just do another run:: test_module.py:5: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -292,7 +293,7 @@ So let's just do another run:: test_module.py:10: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -303,7 +304,7 @@ So let's just do another run:: test_module.py:4: AssertionError ________________________ test_noop[mail.python.org] ________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -312,7 +313,7 @@ So let's just do another run:: E assert 0 test_module.py:10: AssertionError - 4 failed in 6.00 seconds + 4 failed in 6.17 seconds We get four failures because we are running the two tests twice with different ``smtp`` instantiations as defined on the factory. @@ -324,7 +325,7 @@ Adding a finalizer to the parametrized resource Further extending the ``smtp`` example, we now want to properly close a smtp server connection after the last test using it -has been run. We can do this by calling the ``testcontext.addfinalizer()`` +has been run. We can do this by calling the ``request.addfinalizer()`` helper:: # content of conftest.py @@ -333,12 +334,12 @@ helper:: @pytest.factory(scope="session", params=["merlinux.eu", "mail.python.org"]) - def smtp(testcontext): - smtp = smtplib.SMTP(testcontext.param) + def smtp(request): + smtp = smtplib.SMTP(request.param) def fin(): print ("finalizing %s" % smtp) smtp.close() - testcontext.addfinalizer(fin) + request.addfinalizer(fin) return smtp We also add a print call and then run py.test without the default @@ -347,9 +348,9 @@ output capturing and disabled traceback reporting:: $ py.test -s -q --tb=no collecting ... collected 4 items FFFF - 4 failed in 5.62 seconds - finalizing - finalizing + 4 failed in 6.40 seconds + finalizing + finalizing We see that the two ``smtp`` instances are finalized appropriately. @@ -360,8 +361,8 @@ You can also look at what tests pytest collects without running them:: $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev11 + plugins: xdist, bugzilla, cache, oejskit, cli, timeout, pep8, cov collecting ... collected 4 items @@ -369,7 +370,7 @@ You can also look at what tests pytest collects without running them:: - ============================= in 0.02 seconds ============================= + ============================= in 0.01 seconds ============================= Note that pytest orders your test run by resource usage, minimizing the number of active resources at any given time. @@ -404,15 +405,15 @@ the ``smtp`` resource by listing it as an input parameter. Let's run this:: $ py.test -v test_appsetup.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-414/.cache - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev11 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-423/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, timeout, pep8, cov collecting ... collected 2 items test_appsetup.py:12: test_exists[merlinux.eu] PASSED test_appsetup.py:12: test_exists[mail.python.org] PASSED - ========================= 2 passed in 5.37 seconds ========================= + ========================= 2 passed in 6.82 seconds ========================= Due to the parametrization of ``smtp`` the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -445,17 +446,17 @@ to show the flow of calls:: import pytest @pytest.factory(scope="module", params=["mod1", "mod2"]) - def modarg(testcontext): - param = testcontext.param + def modarg(request): + param = request.param print "create", param def fin(): print "fin", param - testcontext.addfinalizer(fin) + request.addfinalizer(fin) return param @pytest.factory(scope="function", params=[1,2]) - def otherarg(testcontext): - return testcontext.param + def otherarg(request): + return request.param def test_0(otherarg): print " test0", otherarg @@ -468,9 +469,9 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ py.test -v -s test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-414/.cache - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev11 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-423/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, timeout, pep8, cov collecting ... collected 8 items test_module.py:16: test_0[1] PASSED @@ -501,26 +502,29 @@ a re-ordering of test execution. The finalizer for the ``mod1`` parametrized resource was executed before the ``mod2`` resource was setup. .. currentmodule:: _pytest.python -.. _`testcontext`: +.. _`request`: -``testcontext``: interacting with test context ---------------------------------------------------- +``request``: interacting with test invocation context +------------------------------------------------------- -The ``testcontext`` object may be used by `@pytest.factory`_ or -:ref:`@pytest.setup ` marked functions. It contains -information relating to the test context within which the marked -function executes. Moreover, you can call -``testcontext.addfinalizer(myfinalizer)`` in order to trigger a call to -``myfinalizer`` after the last test in the test context has executed. -If passed to a parametrized factory ``testcontext.param`` will contain a -parameter (one value out of the ``params`` list specified with the -`@pytest.factory`_ marker). +The ``request`` object may be received by `@pytest.factory`_ or +:ref:`@pytest.setup ` marked functions and provides methods -.. autoclass:: _pytest.python.TestContext() +* to inspect attributes of the requesting test context, such as + ``function``, ``cls``, ``module``, ``session`` and the pytest + ``config`` object. A request object passed to a parametrized factory + will also carry a ``request.param`` object (A parametrized factory and + all of its dependent tests will be called with each of the factory-specified + ``params``). + +* to add finalizers/teardowns to be invoked when the last + test of the requesting test context executes + + +.. autoclass:: _pytest.python.FuncargRequest() :members: - .. _`test generators`: .. _`parametrizing-tests`: .. _`parametrized test functions`: @@ -566,7 +570,6 @@ two ``(input, output)`` arguments of ``test_eval`` function so the latter will be run three times:: $ py.test -q - collecting ... collected 13 items ....F........ ================================= FAILURES ================================= @@ -585,7 +588,7 @@ will be run three times:: E + where 54 = eval('6*9') test_expectation.py:8: AssertionError - 1 failed, 12 passed in 5.76 seconds + 1 failed, 12 passed in 6.41 seconds As expected only one pair of input/output values fails the simple test function. As usual you can see the ``input`` and ``output`` values in the traceback. @@ -662,11 +665,11 @@ This means that we only run two tests if no option is passed:: $ py.test -q test_compute.py collecting ... collected 2 items .. - 2 passed in 0.02 seconds + 2 passed in 0.01 seconds And we run five tests if we add the ``--all`` option:: - $ py.test -q --all + $ py.test -q --all test_compute.py collecting ... collected 5 items ....F ================================= FAILURES ================================= @@ -679,7 +682,7 @@ And we run five tests if we add the ``--all`` option:: E assert 4 < 4 test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.03 seconds + 1 failed, 4 passed in 0.02 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -748,13 +751,4 @@ the mechanism was extended and refined: use remains fully supported and existing code using it should run unmodified. -.. _request: - -The request object passed to old-style factories ------------------------------------------------------------------ - -Old-style funcarg factory definitions can receive a :py:class:`~_pytest.python.FuncargRequest` object which -provides methods to manage caching and finalization in the context of the -test invocation as well as several attributes of the the underlying test item. - diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index e884dd414..2c69a4adc 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -331,9 +331,6 @@ test execution: Reference of objects involved in hooks =========================================================== -.. autoclass:: _pytest.python.FuncargRequest() - :members: - .. autoclass:: _pytest.config.Config() :members: diff --git a/doc/en/setup.txt b/doc/en/setup.txt index 418b91ac8..7a78e7750 100644 --- a/doc/en/setup.txt +++ b/doc/en/setup.txt @@ -33,7 +33,7 @@ you can do with the old-style and much more. Specifically setup functions: - fully interoperate with parametrized resources, - can be defined in a plugin or :ref:`conftest.py ` file and get called on a per-session, per-module, per-class or per-function basis, -- can access the :ref:`testcontext ` for which the setup is called, +- can access the :ref:`request ` for which the setup is called, - can precisely control teardown by registering one or multiple teardown functions as soon as they have performed some actions which need undoing, eliminating the no need for a separate @@ -140,8 +140,8 @@ a per-module setup function. We use a :ref:`resource factory return GlobalResource() @pytest.setup(scope="module") - def setresource(testcontext, globresource): - testcontext.module.globresource = globresource + def setresource(request, globresource): + request.module.globresource = globresource Now any test module can access ``globresource`` as a module global:: @@ -178,16 +178,16 @@ factory and also add a finalizer:: self.param = param @pytest.factory(scope="session", params=[1,2]) - def globresource(testcontext): - g = GlobalResource(testcontext.param) + def globresource(request): + g = GlobalResource(request.param) def fin(): print "finalizing", g - testcontext.addfinalizer(fin) + request.addfinalizer(fin) return g @pytest.setup(scope="module") - def setresource(testcontext, globresource): - testcontext.module.globresource = globresource + def setresource(request, globresource): + request.module.globresource = globresource And then re-run our test module:: diff --git a/testing/test_python.py b/testing/test_python.py index 9960fe700..168ab23b7 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1590,8 +1590,8 @@ class TestRequestAPI: result = testdir.makeconftest(""" import pytest @pytest.setup() - def mysetup(testcontext): - testcontext.uses_funcarg("db") + def mysetup(request): + request.uses_funcarg("db") """) result = testdir.runpytest() assert result.ret == 0 @@ -1647,9 +1647,9 @@ class TestFuncargFactory: import pytest l = [] @pytest.factory(params=[1,2]) - def arg1(testcontext): + def arg1(request): l.append(1) - return testcontext.param + return request.param @pytest.factory() def arg2(arg1): @@ -1758,7 +1758,7 @@ class TestSetupDiscovery: testdir.makeconftest(""" import pytest @pytest.setup() - def perfunction(testcontext, tmpdir): + def perfunction(request, tmpdir): pass @pytest.factory() @@ -1782,10 +1782,10 @@ class TestSetupDiscovery: setupcalls, allnames = fm.getsetuplist(item.nodeid) assert len(setupcalls) == 2 assert setupcalls[0].func.__name__ == "perfunction" - assert "testcontext" in setupcalls[0].funcargnames + assert "request" in setupcalls[0].funcargnames assert "tmpdir" in setupcalls[0].funcargnames assert setupcalls[1].func.__name__ == "perfunction2" - assert "testcontext" not in setupcalls[1].funcargnames + assert "request" not in setupcalls[1].funcargnames assert "arg1" in setupcalls[1].funcargnames assert "tmpdir" not in setupcalls[1].funcargnames #assert "tmpdir" in setupcalls[1].depfuncargs @@ -1842,8 +1842,8 @@ class TestSetupManagement: import pytest l = [] @pytest.factory(params=[1,2]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param @pytest.setup() def something(arg): @@ -1868,12 +1868,12 @@ class TestSetupManagement: l = [] @pytest.factory(scope="session", params=[1,2]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param @pytest.setup(scope="function") - def append(testcontext, arg): - if testcontext.function.__name__ == "test_some": + def append(request, arg): + if request.function.__name__ == "test_some": l.append(arg) def test_some(): @@ -1894,18 +1894,18 @@ class TestSetupManagement: l = [] @pytest.factory(scope="function", params=[1,2]) - def farg(testcontext): - return testcontext.param + def farg(request): + return request.param @pytest.factory(scope="class", params=list("ab")) - def carg(testcontext): - return testcontext.param + def carg(request): + return request.param - @pytest.setup(scope="class") - def append(testcontext, farg, carg): + @pytest.setup(scope="function") + def append(request, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) - testcontext.addfinalizer(fin) + request.addfinalizer(fin) """) testdir.makepyfile(""" import pytest @@ -1950,8 +1950,8 @@ class TestFuncargMarker: testdir.makepyfile(""" import pytest @pytest.factory(params=["a", "b", "c"]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param l = [] def test_param(arg): l.append(arg) @@ -2011,10 +2011,10 @@ class TestFuncargMarker: finalized = [] created = [] @pytest.factory(scope="module") - def arg(testcontext): + def arg(request): created.append(1) - assert testcontext.scope == "module" - testcontext.addfinalizer(lambda: finalized.append(1)) + assert request.scope == "module" + request.addfinalizer(lambda: finalized.append(1)) def pytest_funcarg__created(request): return len(created) def pytest_funcarg__finalized(request): @@ -2050,14 +2050,14 @@ class TestFuncargMarker: finalized = [] created = [] @pytest.factory(scope="function") - def arg(testcontext): + def arg(request): pass """) testdir.makepyfile( test_mod1=""" import pytest @pytest.factory(scope="session") - def arg(testcontext): + def arg(request): %s def test_1(arg): pass @@ -2091,8 +2091,8 @@ class TestFuncargMarker: testdir.makepyfile(""" import pytest @pytest.factory(scope="module", params=["a", "b", "c"]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param l = [] def test_param(arg): l.append(arg) @@ -2109,7 +2109,7 @@ class TestFuncargMarker: testdir.makeconftest(""" import pytest @pytest.factory(scope="function") - def arg(testcontext): + def arg(request): pass """) testdir.makepyfile(""" @@ -2131,8 +2131,8 @@ class TestFuncargMarker: import pytest @pytest.factory(scope="module", params=[1, 2]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param l = [] def test_1(arg): @@ -2198,18 +2198,18 @@ class TestFuncargMarker: l = [] @pytest.factory(scope="function", params=[1,2]) - def farg(testcontext): - return testcontext.param + def farg(request): + return request.param @pytest.factory(scope="class", params=list("ab")) - def carg(testcontext): - return testcontext.param + def carg(request): + return request.param - @pytest.setup(scope="class") - def append(testcontext, farg, carg): + @pytest.setup(scope="function") + def append(request, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) - testcontext.addfinalizer(fin) + request.addfinalizer(fin) """) testdir.makepyfile(""" import pytest @@ -2244,18 +2244,18 @@ class TestFuncargMarker: import pytest @pytest.factory(scope="function", params=[1, 2]) - def arg(testcontext): - param = testcontext.param - testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) + def arg(request): + param = request.param + request.addfinalizer(lambda: l.append("fin:%s" % param)) l.append("create:%s" % param) - return testcontext.param + return request.param @pytest.factory(scope="module", params=["mod1", "mod2"]) - def modarg(testcontext): - param = testcontext.param - testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) + def modarg(request): + param = request.param + request.addfinalizer(lambda: l.append("fin:%s" % param)) l.append("create:%s" % param) - return testcontext.param + return request.param l = [] def test_1(arg): @@ -2288,11 +2288,11 @@ class TestFuncargMarker: import pytest @pytest.factory(scope="module", params=[1, 2]) - def arg(testcontext): - testcontext.config.l = l # to access from outer - x = testcontext.param - testcontext.addfinalizer(lambda: l.append("fin%s" % x)) - return testcontext.param + def arg(request): + request.config.l = l # to access from outer + x = request.param + request.addfinalizer(lambda: l.append("fin%s" % x)) + return request.param l = [] def test_1(arg): @@ -2317,10 +2317,10 @@ class TestFuncargMarker: import pytest @pytest.factory(scope="function", params=[1, 2]) - def arg(testcontext): - x = testcontext.param - testcontext.addfinalizer(lambda: l.append("fin%s" % x)) - return testcontext.param + def arg(request): + x = request.param + request.addfinalizer(lambda: l.append("fin%s" % x)) + return request.param l = [] def test_1(arg): @@ -2339,12 +2339,12 @@ class TestFuncargMarker: import pytest @pytest.factory(scope="module", params=[1, 2]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param @pytest.setup(scope="module") - def mysetup(testcontext, arg): - testcontext.addfinalizer(lambda: l.append("fin%s" % arg)) + def mysetup(request, arg): + request.addfinalizer(lambda: l.append("fin%s" % arg)) l.append("setup%s" % arg) l = [] @@ -2376,32 +2376,32 @@ class TestTestContextScopeAccess: testdir.makepyfile(""" import pytest @pytest.setup(scope=%r) - def myscoped(testcontext): + def myscoped(request): for x in %r: - assert hasattr(testcontext, x) + assert hasattr(request, x) for x in %r: pytest.raises(AttributeError, lambda: - getattr(testcontext, x)) - assert testcontext.session - assert testcontext.config + getattr(request, x)) + assert request.session + assert request.config def test_func(): pass """ %(scope, ok.split(), error.split())) - reprec = testdir.inline_run() + reprec = testdir.inline_run("-l") reprec.assertoutcome(passed=1) def test_funcarg(self, testdir, scope, ok, error): testdir.makepyfile(""" import pytest @pytest.factory(scope=%r) - def arg(testcontext): + def arg(request): for x in %r: - assert hasattr(testcontext, x) + assert hasattr(request, x) for x in %r: pytest.raises(AttributeError, lambda: - getattr(testcontext, x)) - assert testcontext.session - assert testcontext.config + getattr(request, x)) + assert request.session + assert request.config def test_func(arg): pass """ %(scope, ok.split(), error.split())) @@ -2414,7 +2414,7 @@ class TestErrors: testdir.makepyfile(""" import pytest @pytest.factory() - def gen(request): + def gen(qwe123): return 1 def test_something(gen): pass @@ -2422,8 +2422,8 @@ class TestErrors: result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines([ - "*def gen(request):*", - "*no factory*request*", + "*def gen(qwe123):*", + "*no factory*qwe123*", "*1 error*", ]) @@ -2431,7 +2431,7 @@ class TestErrors: testdir.makepyfile(""" import pytest @pytest.setup() - def gen(request): + def gen(qwe123): return 1 def test_something(): pass @@ -2439,14 +2439,14 @@ class TestErrors: result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines([ - "*def gen(request):*", - "*no factory*request*", + "*def gen(qwe123):*", + "*no factory*qwe123*", "*1 error*", ]) class TestTestContextVarious: - def test_newstyle_no_request(self, testdir): + def test_newstyle_with_request(self, testdir): testdir.makepyfile(""" import pytest @pytest.factory() @@ -2455,21 +2455,19 @@ class TestTestContextVarious: def test_1(arg): pass """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*no factory found*request*", - ]) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) def test_setupcontext_no_param(self, testdir): testdir.makepyfile(""" import pytest @pytest.factory(params=[1,2]) - def arg(testcontext): - return testcontext.param + def arg(request): + return request.param @pytest.setup() - def mysetup(testcontext, arg): - assert not hasattr(testcontext, "param") + def mysetup(request, arg): + assert not hasattr(request, "param") def test_1(arg): assert arg in (1,2) """) @@ -2505,3 +2503,16 @@ def test_setupdecorator_and_xunit(testdir): """) reprec = testdir.inline_run() reprec.assertoutcome(passed=3) + +def test_request_can_be_overridden(testdir): + testdir.makepyfile(""" + import pytest + @pytest.factory() + def request(request): + request.a = 1 + return request + def test_request(request): + assert request.a == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) diff --git a/tox.ini b/tox.ini index d9dd8d8ed..1e8db2c16 100644 --- a/tox.ini +++ b/tox.ini @@ -62,6 +62,9 @@ commands= [testenv:py32] deps=py>=1.4.0 +[testenv:py33] +deps=py>=1.4.0 + [testenv:jython] changedir=testing commands=