Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamically generated fixtures #2424

Closed
majuscule opened this issue May 22, 2017 · 17 comments
Closed

dynamically generated fixtures #2424

majuscule opened this issue May 22, 2017 · 17 comments
Labels
topic: fixtures anything involving fixtures directly or indirectly type: proposal proposal for a new feature, often to gather opinions or design the API around the new feature

Comments

@majuscule
Copy link

majuscule commented May 22, 2017

This is a feature request. I would like to be able to dynamically generate fixtures, similar to parameterized fixtures, but instead of running all associated tests with each fixture, expose each fixture dynamically. The syntax could use work, but i.e.:

@pytest.fixture(scope='module',
                generate=['user', 'admin'],
                params=[User, Admin])
def session(SessionType):
    return SessionType()

def test_authorization(session_user, session_admin):
    with pytest.raises(Exception):
        session_user.do_admin_thing()
     assert session_admin.do_admin_thing()
@nicoddemus nicoddemus added type: proposal proposal for a new feature, often to gather opinions or design the API around the new feature topic: fixtures anything involving fixtures directly or indirectly labels Sep 28, 2017
@nolar
Copy link

nolar commented Oct 1, 2017

This is a rare and too specific use-case to make it into the framework. And it can be achieved easily right now. Here is the code snippet that works as you described:

import pytest

def generate_fixture(someparam):
    @pytest.fixture(scope='module')
    def my_fixture():
        print('my fixture is called with someparam={!r}'.format(someparam))
        return someparam
    return my_fixture

def inject_fixture(name, someparam):
    globals()[name] = generate_fixture(someparam)

inject_fixture('my_user', 100)
inject_fixture('my_admin', 200)

def test_me(my_user, my_admin):
    print(my_user, my_admin)

The trick is that the module must contain a module-level variable (in the module's globals) with the name of the generated fixture — because pytest scans for the fixture by iterating over dir(holderobj) where holderobj is the module, not by explicitly registering a function on each call to pytest.fuxture(). So, you can have as many dynamically created fixtures with dynamically created names as you wish.

@nicoddemus
Copy link
Member

@nolar that's a nice snippet, thanks!

@nicoddemus
Copy link
Member

@nolar you might consider contribute a post to pytest-tricks with that trick btw 😉

@nicoddemus
Copy link
Member

Closing as it seems the workaround is good enough to solve the original case.

@briancappello
Copy link

briancappello commented Mar 25, 2018

I needed something that could inject fixtures based on the result of another fixture, for which the solution proposed above by @nolar does not work. (My specific use case is a Flask app with its own dependency injection system bolted on top, for which I wanted to be able to inject those services as fixtures)

For anybody else that needs something similar, this is what I came up:

@pytest.fixture(autouse=True)
def inject_services(app, request):
    # get the current fixture or test-function
    item = request._pyfuncitem
    # preemptively inject any arg_names that pytest doesn't know about, but that we do
    fixture_names = getattr(item, "fixturenames", request.fixturenames)
    for arg_name in fixture_names:
        if arg_name not in item.funcargs:
            try:
                request.getfixturevalue(arg_name)
            except FixtureLookupError:
                if arg_name in app.services:
                    item.funcargs[arg_name] = app.services[arg_name]

Seems to work fine, but no guarantees. Logic is based on pytest's FixtureRequest._fillfixtures method.

@nicoddemus
Copy link
Member

Thanks for sharing @briancappello!

@maxandersen
Copy link

anyone found a good way to have the dynamic fixtures have generated doc strings too ?

with the above you get:

my_user
    tests/conftest.py:11: no docstring available
my_admin
    tests/conftest.py:11: no docstring available

would be nice to also somehow control the documentation.

Anyone ?

@asottile
Copy link
Member

@maxandersen #4636 (comment) has an example where docstrings work

@dan-sel
Copy link

dan-sel commented Apr 29, 2021

Closing as it seems the workaround is good enough to solve the original case.

@nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

@nicoddemus
Copy link
Member

@nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

Sorry which API?

@asottile
Copy link
Member

Closing as it seems the workaround is good enough to solve the original case.

@nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

from a python standpoint (and therefore pytest) there's not really a noticeable difference between:

x = 1

and

def f(name, value):
    globals()[name] = value

f('x', 1)

@dan-sel
Copy link

dan-sel commented Apr 29, 2021

Sorry, bad hand-mind coordination.
What I actually wanted to know is whether we can rely on the workaround to work in the future. It seems to be based on an undocumented implementation detail ("pytest scans for the fixture by iterating over dir(holderobj)"). If that is subject to change anytime without prior notice, we (and probably most others) will not be able to use it. And in that case, it would be great to have it as an official feature.

@nicoddemus
Copy link
Member

Hmm it is hard to make that promise, but we can say with confidence that there are no plans at the moment to change that, even in the far future.

If that were to change however, we probably will give this prior notice as we understand this might break existing code.

@GrayedFox
Copy link

I deleted my previous comment since it assumed a problem with parametrization when really the issue is about importing helper methods that generate parametrized fixtures. I've opened a stackoverflow question about it here.

I can confirm that this trick still works for class scoped parametrized fixtures 👍🏾

@GrayedFox
Copy link

GrayedFox commented Feb 16, 2022

Hey just leaving this here in case people find it useful - the reason this trick didn't work when importing the methods from a separate helper file is that helper file has a different global scope, and the hack works by changing the global fixture scope at run time. Here's a working example that generates a parametrized fixture that also uses the bundled request fixture for class introspection:

import inspect
import pytest

def generate_fixture(scope, params):
    @pytest.fixture(scope=scope, params=params)
    def my_fixture(request):
        request.cls.param = request.param
        print(request.param)

    return my_fixture

def inject_fixture(name, scope, params):
    """Dynamically inject a fixture at runtime"""
    # we need the caller's global scope for this hack to work hence the use of the inspect module
    caller_globals = inspect.stack()[1][0].f_globals
    # for an explanation of this trick and why it works go here: https://github.com/pytest-dev/pytest/issues/2424
    caller_globals[name] = generate_fixture(params)

@moshesar
Copy link

moshesar commented Apr 7, 2022

@nolar that's very helpful thank you! Can you suggest a wider approach where multiple test generate the same fixture, but with different params ? I think using globals in this case might cause error where 2 tests can reference to the same fixture name but get unexpected data.

@patha454
Copy link

patha454 commented Jan 31, 2024

Is it possible to generalise the snippets here to be injected from a different module than the tests?

I'm writing a PyTest plugin for backwards compatibility with a legacy, in-house testing framework. I'm attempting to implement a plugin which creates a wrapper fixture, and attaches it to all relevant tests.

The adapted code fail with the fixture not being found, despite being present in the test module's globals()

def pytest_collection_modifyitems(session, config, items: List[Item]):
    for item in items:
        pytest_function: Function = item.getparent(Function)
        def generate_fixture(name: str):
            @pytest.fixture(name=name, scope="module")
            def _fixture():
                print("Setup...")
                yield
                print("Teardown...")
            return _fixture

        def inject_fixture(name: str, module: ModuleType):
            setattr(module, name, generate_fixture(name))

        module: ModuleType = pytest_function.module
        inject_fixture("example_fixture", module)
        pytest_function.fixturenames.append("example_fixture")
  def test_dummy_func():
E       fixture 'example_fixture' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.
# Printing globals from withing a test
------------------- Captured stdout call --------------------
... 'example_fixture': <function pytest_collection_modifyitems.<locals>.generate_fixture.<locals>._fixture at 0x7f97ca10e1f0>
...

I'm aware of the pytest_runtest_teardown and pytest_runtest_setup PyTest hooks for running legacy function scope hooks - but I also need support for legacy module scope hooks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: fixtures anything involving fixtures directly or indirectly type: proposal proposal for a new feature, often to gather opinions or design the API around the new feature
Projects
None yet
Development

No branches or pull requests

10 participants