Hi all, particularly Floris and Carl,

i have finally arrived at the V2 resource-API draft based on the very valuable
feedback you gave to the first version.  The document implements a largely
changed approach, see the "Changes from V1 to V2" at the beginning, and
focuses on usage-level documentation instead of internal details.

I have also uploaded this doc as HTML which makes it a bit more colorful
to read, and also contains some cross-references:

    http://pytest.org/dev/resources.html

Please find the the source txt-file also attached for your
inline-commenting usage.  Before i target the actual (substantial)
refactoring, i'd actually be very grateful for some more of your time
and comments on this new version.

I believe that the new resource parametrization facilities are a major
step forward - they should allow test writers to much more seldomly having
to resort to pytest_* hooks, and make access and working with parametrized 
resources straight forward, irrespective of previous xUnit/pytest background.
Plugin writers, of course, may still use the hooks for good value.

best & thanks,
holger

V2: Creating and working with parametrized test resources
===============================================================

Abstract: pytest-2.X provides generalized scoping and parametrization
of resource setup.  It does so by introducing new scoping and parametrization
capabilities directly to to funcarg factories and by enhancing
xUnit-style setup_X methods to directly accept funcarg resources.
Moreover, new xUnit setup_directory() and setup_session() methods allow
fixture code (and resource usage) at previously unavailable scopes.
Pre-existing test suites and plugins written to work for previous pytest
versions shall run unmodified.

This V2 draft is based on incorporating feedback provided by Floris Bruynooghe, 
Carl Meyer and Ronny Pfannschmidt. It remains as draft documentation, pending 
further refinements and changes according to implementation or backward 
compatibility issues. The main changes to V1 are:

* changed approach: now based on improving ``pytest_funcarg__``
  factories and extending setup_X methods to directly accept
  funcarg resources, also including a new per-directory
  "setup_directory()" and setup_session() function for respectively
  scoped setup.
* the "funcarg" versus "resource" naming issue is disregarded in favour
  of keeping with "funcargs" and talking about "funcarg resources" 
  to ease a later possible renaming (whose value is questionable)
* The register_factory/getresource methods are moved to an
  implementation section for now, drawing a clear boundary between
  usage-level docs and impl-level ones.
* use "2.X" as the version for introduction (might be 2.3, else 2.4)

.. currentmodule:: _pytest


Shortcomings of the previous pytest_funcarg__ mechanism
---------------------------------------------------------

The previous funcarg mechanism calls a factory each time a
funcarg for a test function is requested.  If a factory wants
t re-use a resource across different scopes, it often used 
the ``request.cached_setup()`` helper to manage caching of 
resources.  Here is a basic example how we could implement 
a per-session Database object::

    # content of conftest.py 
    class Database:
        def __init__(self):
            print ("database instance created")
        def destroy(self):
            print ("database instance destroyed")

    def pytest_funcarg__db(request):
        return request.cached_setup(setup=DataBase, 
                                    teardown=lambda db: db.destroy,
                                    scope="session")

There are some problems with this approach:

1. Scoping resource creation is not straight forward, instead one must
   understand the intricate cached_setup() method mechanics.

2. parametrizing the "db" resource is not straight forward: 
   you need to apply a "parametrize" decorator or implement a
   :py:func:`~hookspec.pytest_generate_tests` hook 
   calling :py:func:`~python.Metafunc.parametrize` which
   performs parametrization at the places where the resource 
   is used.  Moreover, you need to modify the factory to use an 
   ``extrakey`` parameter containing ``request.param`` to the 
   :py:func:`~python.Request.cached_setup` call.

3. the current implementation is inefficient: it performs factory discovery
   each time a "db" argument is required.  This discovery wrongly happens at 
   setup-time.

4. there is no way how you can use funcarg factories, let alone 
   parametrization, when your tests use the xUnit setup_X approach.

5. there is no way to specify a per-directory scope for caching.

In the following sections, API extensions are presented to solve 
each of these problems. 


Direct scoping of funcarg factories
--------------------------------------------------------

Instead of calling cached_setup(), you can decorate your factory
to state its scope::

    @pytest.mark.factory_scope("session")
    def pytest_funcarg__db(request):
        # factory will only be invoked once per session - 
        db = DataBase()
        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 
``request.addfinalizer()`` registers a finalizer according to the specified
factory scope on which this factory is operating.  With this new
scoping, the still existing ``cached_setup()`` should be much less used
but will remain for compatibility reasons and for the case where you
still want to have your factory get called on a per-item basis.


Direct parametrization of funcarg factories 
----------------------------------------------------------

Previously, funcarg factories could not directly cause parametrization.
You needed to specify a ``@parametrize`` or implement a 
``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test 
multiple times
with different value sets.  pytest-2.X introduces a decorator for use
on the factory itself::

    @pytest.mark.factory_parametrize(["mysql", "pg"])
    def pytest_funcarg__db(request):
        ...

Here the factory will be invoked twice (with the respective "mysql" 
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 directly parametrizing funcarg factories should
in many cases allow to use already written factories because effectively
``@factory_parametrize`` sets the same ``request.param`` that a 
:py:func:`~_pytest.python.Metafunc.parametrize()` call causes to be set.

Of course it's perfectly to combine parametrization and scoping::

    @pytest.mark.factory_parametrize(["mysql", "pg"])
    @pytest.mark.factory_scope("session")
    def pytest_funcarg__db(request):
        if request.param == "mysql":
            db = MySQL()
        elif request.param == "pg":
            db = PG()
        request.addfinalizer(db.destroy)  # destroy when session is finished
        return db

This would execute all tests requiring the per-session "db" resource twice,
receiving the values respectively created by the two invocations to the
``db`` factory.


Using funcarg resources in xUnit setup methods
------------------------------------------------------------

For a long time, pytest has recommended the usage of funcarg resource
factories as a primary means for managing resources in your test run.
It is a better approach than the jUnit-based approach in many cases, even 
more with the new pytest-2.X features, because the funcarg resource factory
provides a single place to determine scoping and parametrization.  Your tests 
do not need to encode setup/teardown details in every test file's 
setup_module/class/method.  

However, the jUnit methods originally introduced by pytest to Python,
remain popoular with nose and unittest-based test suites.  And in any
case, there are large existing test suites using this paradigm.  pytest-2.X
recognizes this fact and now offers direct integration with funcarg resources.  
Here is a basic example for getting a per-module tmpdir::

    def setup_module(mod, tmpdir):
        mod.tmpdir = tmpdir

This will make pytest's funcarg resource mechanism to create a value of
"tmpdir" which can then be used through the module as a global.

Note that the new extension to setup_X methods also works in case a 
resource is parametrized. Let's consider an setup_class example using
our "db" resource::

    class TestClass:
        def setup_class(cls, db):
            cls.db = db
            # perform some extra things on db
            # so that test methods can work with it

With pytest-2.X the setup* methods will be discovered at collection-time,
allowing to seemlessly integrate this approach with parametrization.
Note again, that the setup_class itself does not itself need to 
be aware of the fact that "db" might be a mysql/PG database.
Note that if the specified resource is provided only as a per-testfunction
resource, collection will already report a ScopingMismatch error.

support for setup_session and setup_directory
------------------------------------------------------

pytest for a long time offered a pytest_configure and a pytest_sessionstart
hook which are often used to setup global resources.  This suffers from
several problems:

1. in distributed testing the master process would setup test resources
   that are never needed because it only co-ordinates the test run
   activities of the slave processes.  

2. if you only perform a collection (with "--collectonly") 
   resource-setup will still be executed.  

3. If a pytest_sessionstart is contained in some subdirectories
   conftest.py file, it will not be called.  This stems from the
   fact that this hook is actually used for reporting, in particular
   the test-header with platform/custom information.

4. there is no direct way how you can restrict setup to a directory scope.

It follows that these hooks are not a good place to implement session
or directory-based setup.  pytest-2.X offers new "setup_X" methods, accepting
funcargs, which you can put into conftest.py or plugins files::
    
    # content of conftest.py
    def setup_session(db):
        ... use db resource or do some initial global init stuff
        ... before any test is run.
        
    def setup_directory(db):
        # called when the first test in the directory tree is about to execute

For obvious reasons, the used funcarg-resources be provided on a per-directory
or per-session basis.

the "directory" caching scope
--------------------------------------------

All API accepting a scope (:py:func:`cached_setup()`  nd
@mark.factory_scope currently) now also accepts a "directory"
specification.  This allows to restrict/cache resource values on a
per-directory level.

funcarg and setup discovery now happens at collection time
---------------------------------------------------------------------

pytest-2.X takes care to discover resource factories and setup_X methods
at collection time.  This is more efficient especially for large test suites. 
Moreover, a call to "py.test --collectonly" should be able to show
a lot of setup-information and thus presents a nice method to get an
overview of resource management in your project.

Implementation level 
===================================================================

To implement the above new features, pytest-2.X grows some new hooks and
methods.  At the time of writing V2 and without actually implementing
it, it is not clear how much of this new internal API will also be
exposed and advertised e. g. for plugin writers. 


the "request" object incorporates scope-specific behaviour
------------------------------------------------------------------

funcarg factories receive a request object to help with implementing
finalization and inspection of the requesting-context.  If there is
no scoping is in effect, nothing much will change of the API behaviour.
However, with scoping the request object represents the according context.
Let's consider this example::

    @pytest.mark.factory_scope("class")
    def pytest_funcarg__db(request):
        # ...
        request.getfuncargvalue(...)
        #
        request.addfinalizer(db)

Due to the class-scope, the request object will:

- provide a ``None`` value for the ``request.function`` attribute. 
- default to per-class finalization with the addfinalizer() call.
- raise a ScopeMismatchError if a more broadly scoped factory
  wants to use a more tighly scoped factory (e.g. per-function)

In fact, the request object is likely going to provide a "node" 
attribute, denoting the current collection node on which it internally
operates.  (Prior to pytest-2.3 there already was an internal
_pyfuncitem).

As these are rather intuitive extensions, not much friction is expected 
for test/plugin writers using the new scoping and parametrization mechanism. 
It's, however, a serious internal effort to reorganize the pytest 
implementation.


node.register_factory/getresource() methods
--------------------------------------------------------

In order to implement factory- and setup-method discovery at
collection time, a new node API will be introduced to allow
for factory registration and a getresource() call to obtain
created values.  The exact details of this API remain subject
to experimentation. The basic idea is to introduce two new
methods to the Session class which is already available on all nodes
through the ``node.session`` attribute::

    class Session:
        def register_resource_factory(self, name, factory_or_list, scope):
            """ register a resource factory for the given name.

            :param name: Name of the resource.
            :factory_or_list: a function or a list of functions creating
                              one or multiple resource values.
            :param scope: a node instance. The factory will be only visisble
                          available for all descendant nodes.
                          specify the "session" instance for global availability
            """

        def getresource(self, name, node):
            """ get a named resource for the give node.

            This method looks up a matching funcarg resource factory 
            and calls it.
            """

.. todo::

    XXX While this new API (or some variant of it) may suffices to implement
    all of the described new usage-level features, it remains unclear how the
    existing "@parametrize" or "metafunc.parametrize()" calls will map to it.
    These parametrize-approaches tie resource parametrization to the 
    function/funcargs-usage rather than to the factories. 


.. todo::

    Will this hook be called  

_______________________________________________
py-dev mailing list
py-dev@codespeak.net
http://codespeak.net/mailman/listinfo/py-dev

Reply via email to