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

Fixtures without name matching magic #3834

Open
shoyer opened this issue Aug 19, 2018 · 86 comments
Open

Fixtures without name matching magic #3834

shoyer opened this issue Aug 19, 2018 · 86 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

@shoyer
Copy link
Contributor

shoyer commented Aug 19, 2018

I've been happily using pytest for several projects for the past few years.

There's one part about pytest that I still struggle to get behind: The way that fixtures magically match argument names to fixtures -- and apparently I'm not alone in this feeling. I would much rather declare dependencies explicitly in some way using code. I know this would be more verbose, but that's a tradeoff I'm happy to make.

Is it possible to do this in some way with pytest today? If not, would you be open to adding an optional feature for this?

I was thinking perhaps something like the following, using an example adapted from the docs:

import pytest

@pytest.fixture
def smtp_connection():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

@pytest.use_fixture(smtp_connection)
def test_ehlo(connection):
    response, msg = connection.ehlo()
    assert response == 250
    assert 0 # for demo purposes
@nicoddemus
Copy link
Member

Hi @shoyer,

If you don't like the way fixtures are automatically injected, you can be more explicit in a way very similar to the example you provided:

import pytest

@pytest.fixture
def smtp_connection():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

@pytest.mark.usefixtures("stmp_connection")
def test_ehlo(smtp_connection):
    response, msg = connection.ehlo()
    assert response == 250
    assert 0 # for demo purposes

@nicoddemus nicoddemus added the type: question general question, might be closed after 2 weeks of inactivity label Aug 19, 2018
@shoyer
Copy link
Contributor Author

shoyer commented Aug 19, 2018

@nicoddemus that's explicit about using a fixture, but it's still using a string to refer to a name variable. Is it possible to do something like that while referencing smtp_connection as a Python object -- preferably with a different, non-conflicting argument name?

@nicoddemus
Copy link
Member

Probably you can do something like this:

import pytest

def use_fixture(fixfunc):
    def inner(f):
        return pytest.mark.usefixtures(fixfunc.__name__)(f)
    return inner

@pytest.fixture
def fix():
    return 1

@use_fixture(fix)
def test(fix):
    assert fix == 1

But you still need to use the same name in the argument as the fixture, plus you will need to explicitly import the fixture function into the test module in case it is not defined there, and that is not really recommended because it messes with the internal caching of the fixtures.

that's explicit about using a fixture, but it's still using a string to refer to a name variable.

What is the problem with that in your opinion? That's very explicit IMHO: @pytest.mark.usefixtures("fix") explicitly declares that the test function uses a fixture "fix", and that is passed as argument name.

preferably with a different, non-conflicting argument name?

Not sure what is the reason for that, can you explain?

@shoyer
Copy link
Contributor Author

shoyer commented Aug 20, 2018

But you still need to use the same name in the argument as the fixture, plus you will need to explicitly import the fixture function into the test module in case it is not defined there, and that is not really recommended because it messes with the internal caching of the fixtures.

Indeed, but also using fixfunc.__name__ for lookup is a good example of exactly what I was trying to avoid here.

What is the problem with that in your opinion? That's very explicit IMHO: @pytest.mark.usefixtures("fix") explicitly declares that the test function uses a fixture "fix", and that is passed as argument name.

I want to use Python's normal rules for resolving symbols (i.e., standard variable lookup), and I want to keep introspection to the bare minimum needed to run tests. I know pytest uses magic to rewrite assert and decide which tests to run, but in general I like my Python code (even tests) to follow the normal rules of Python code.

Referencing functions by string name instead of object breaks normal code introspection tools (e.g., lint), which can't recognize misspelled names if there's no variable reference. So if I write stmp_connection instead of smtp_connection, nothing catches that mistake until I actually run my tests.

(In my opinion, one of the best things about Python is that dependencies are explicit, unless you use the discouraged import *.)

preferably with a different, non-conflicting argument name?

Not sure what is the reason for that, can you explain?

As one simple example, if you run this example through pylint, you get the following warning:

W0621: Redefining name 'smtp_connection' from outer scope (line 7) (redefined-outer-name)

At Google, we actually strictly enforce running pylint on all Python code. If I wanted to disable this message, I would need to write a comment # pylint: disable=redefined-outer-name.

Now, pylint is known for being overly-aggressive in some cases, but I do think that in general it's best not mask variable names from outer scopes. There's basically no reason why this is ever necessary if not using pytest, and if done unintentionally, this can easily lead to bugs.


Another example of what doing this explicitly could look like, with keyword arguments:

@pytest.mark.usefixtures(conn=smtp_connection)
def test_ehlo(conn):
    ...

Using a fixture imported from another module also becomes very straightforward, e.g.,

@pytest.mark.usefixtures(conn=other_module.smtp_connection)
def test_ehlo(conn):
    ...

@nicoddemus
Copy link
Member

Hi @shoyer sorry for taking so long to get back at this.

I'm not sure it has a place in the core though, as it would lead to a different way of doing things, and is solving not really an existing problem but a personal preference which is shared by some people but not by the majority of pytest users (at least that's what I think, no hard data here).

But all you are proposing is probably possible to write as a plugin. I suggest you give this a try (be sure to post here if you need help) and see how that feels in real code.

What do you think?

@RonnyPfannschmidt
Copy link
Member

personally i have been thinking about such a mechanism as well, i just dont have the time to work on even a base for it

@burnpanck
Copy link

burnpanck commented Sep 11, 2018

Let me add to this that the name-matching does lead to real-world problems: I just spent an hour trying to get my pytest-sanic tests to working, not realising that it's test_client fixture clashes with the test_client fixture of pytest-aiohttp.

@tuukkamustonen
Copy link

This would be really nice. I hate dropping # pylint: disable=redefined-outer-name lines to my test files because pylint will nag about every test with a fixture otherwise.

The original suggestion by @shoyer looks good enough, imo. Anyway, here are some extra thoughts:

# not supported by python(?):

def test_ehlo(@pytest.inject(smtp_connection) connection):
    ...

# dunno if this would be doable, either:

@pytest.fixture
class SmtpConnection:
    def __call__(self):
        import smtplib
        return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

@pytest.inject
def test_ehlo(conn: SmtpConnection):
    ...

@nicoddemus
Copy link
Member

This would be really nice. I hate dropping # pylint: disable=redefined-outer-name lines to my test files because pylint will nag about every test with a fixture otherwise.

Just to mention that fixtures have a name parameter to avoid this problem:

@pytest.fixture(name='smtp_connection')
def smtp_connection_():
    ...

def test_ehlo(smtp_connection):
    ...

@tuukkamustonen
Copy link

tuukkamustonen commented Oct 11, 2018

Oh, that's a workaround I could use. Good to know.

Though the benefit in being able to do something like what I suggested above would be that when you (have to) import fixtures, they are then actually referenced in the tests file, and your import will not get marked as unused (and removed if you auto-organize imports). Sure I could:

from foo import my_fixture

PYTEST_IMPORTS = my_fixture

But then PYTEST_IMPORTS will be marked as unused :P (or would importing even work together with name=?)

@Zac-HD Zac-HD 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 and removed type: question general question, might be closed after 2 weeks of inactivity labels Nov 14, 2018
@countvajhula
Copy link

I'll register my support for this issue as well. When users first see tests written in pytest, it is mystifying how the arguments to these functions get populated. At least having the option to be explicit about it in a robust way would be nice.

Another possible benefit is the ability to give a different name to the same fixture in different tests. Or maybe more specifically, the ability to give the same name to different fixtures in different tests. Not sure if this is already possible, but with an explicit mechanism like the one @shoyer is talking about, it could look something like this:

Let's say we have these fixtures for an application that processes log files (this is a barebones example just to illustrate the point):

@pytest.fixture
def simple_logfile():
    return ("2019-08-16 10:35:05 connection established\n"
             "2019-08-16 11:07:16 connection dropped\n")

@pytest.fixture
def empty_logfile():
    return ""

With the current behavior, we could write tests like this:

def test_read_logfile(simple_logfile):
    result = read_logfile(simple_logfile)
    assert result == True

def test_read_empty_logfile(empty_logfile):
    result = read_logfile(empty_logfile)
    assert result == True

In the proposed behavior, it could look like this:

@pytest.mark.usefixtures(logfile=simple_logfile)
def test_read_logfile(logfile):
    result = read_logfile(logfile)
    assert result == True

@pytest.mark.usefixtures(logfile=empty_logfile)
def test_read_empty_logfile(logfile):
    result = read_logfile(logfile)
    assert result == True

The explicit approach allows the generic aspects of the test code to remain generic -- in this example, the test only cares about reading a logfile and asserting that it works. The fact that we have selected this file to be an empty one to test an edge case is already captured in the name of the function, and avoiding the need for different variable names seems desirable since the code is the same.

In fact, doing it this way gives us yet another advantage -- the possibility of parametrizing over fixtures using pytest.mark.parametrize. For instance, the proposed behavior could now look like this:

@pytest.mark.parametrize(
    "logfile", (simple_logfile, empty_logfile),
)
def test_read_logfile(logfile):
    result = read_logfile(logfile)
    assert result == True

... which covers both tests in a single parameterized test.

It looks like there is currently already a way to parametrize fixtures themselves. That's a great feature as well, but I think the ability to parameterize the test rather than the fixture is a different thing which could complement the current functionality and lend additional flexibility.

@jadkik
Copy link

jadkik commented Feb 3, 2020

I think the ability to parameterize the test rather than the fixture is a different thing which could complement the current functionality and lend additional flexibility.

FYI there is already a way to do this in a nice-ish way:

import pytest


@pytest.fixture
def simple_logfile():
    return 'simple_logfile contents'


@pytest.fixture
def empty_logfile():
    return 'empty_logfile contents'


@pytest.fixture(name='logfile')
def dynamic_fixture(request):
    return request.getfixturevalue(request.param)


@pytest.mark.parametrize('logfile', ['simple_logfile', 'empty_logfile'], indirect=True)
def test_something(logfile):
    print()
    print('test_something', repr(logfile))

Downside being that you can't do indirect parameterization of simple_logfile from test_something, and it doesn't show up when you run pytest with --setup-plan (it does when you do --setup-only though.

I can still see the benefit in this proposal, though: if usefixtures accepts keyword arguments with names or actual fixture functions, it could be a change that doesn't break the current behaviour.

@ms-lolo
Copy link

ms-lolo commented Aug 17, 2020

Just wanted to drop my support for this request as well. The current fixtures functionality acts as a global registry of magical variable names and globals are confusing and error prone for the reasons already outlined a few times above. When I'm creating new fixtures I should not need to try and think of a globally unique name for it as long as I've implemented it in my globally unique package name.

It's a little disappointing to try and convince folks on the team to write maintainable/testable code and then run into the testing framework itself not following software engineering best practices. We only need to write this code once but we have to read it, understand it, and maintain it for years. We can afford to type a few extra lines of code to make sure it works and is immediately clear to anyone that might run into it in the future.

@nicoddemus
Copy link
Member

Hi @ms-lolo,

It's a little disappointing to try and convince folks on the team to write maintainable/testable code and then run into the testing framework itself not following software engineering best practices. We only need to write this code once but we have to read it, understand it, and maintain it for years. We can afford to type a few extra lines of code to make sure it works and is immediately clear to anyone that might run into it in the future.

I sympathize with that sentiment, but personally I think the issue boils down more to introducing another way to use fixtures, which might lead to even more confusion when the classic way and this new way proposed here are mixed.

As was stated before, I believe it is possible to implement a different mechanism as a plugin, but adding this directly to the core without first seeing it being used in the wild might not be the best approach.

@shoyer
Copy link
Contributor Author

shoyer commented Aug 17, 2020

I sympathize with that sentiment, but personally I think the issue boils down more to introducing another way to use fixtures, which might lead to even more confusion when the classic way and this new way proposed here are mixed.

If we agree that an explicit API is better, we might consider deprecating the current global name matching entirely, and eventually only supporting the new way (or at least suggesting it as the preferred option for new code).

@ms-lolo
Copy link

ms-lolo commented Aug 17, 2020

Totally understand wanting to avoid having multiple ways of doing this. And using a plugin to demonstrate a cleaner approach sounds like a great idea that I'm going to try and find some time to investigate, although I have no experience with the pytest internals, so any guidance would be appreciated!

I'm not sure it has a place in the core though, as it would lead to a different way of doing things, and is solving not really an existing problem but a personal preference which is shared by some people but not by the majority of pytest users (at least that's what I think, no hard data here).

I was mostly leaving a comment to add a data point about this comment :) I disagree with the statement that this is a personal preference but we don't need to go down that rabbit hole without even a proof of concept alternative to compare to.

@countvajhula
Copy link

@jadkik Nice, though another downside is that, judging by your example, we'd need to write a wrapping dynamic fixture for every set of fixtures we might want to parametrize over. And since we would be creating this abstraction behind yet another global name, this does not escape the namespace issues that @ms-lolo brings up. By separating the fixture definition from its name, we gain the ability to abstract over any set of fixtures behind a lexically-scoped name in each test, without having to pre-designate (via a wrapper function like dynamic_fixture) which ones those are.

@The-Compiler
Copy link
Member

The-Compiler commented Aug 18, 2020

FWIW maybe we could turn to ward for inspiration here. It uses default argument values to make fixtures more explicit (and namespaced) without having much more boilerplate:

from ward import fixture, test

@fixture
def user():
    return User(id=1, name="sam")

@test("fetch_user_by_id should return the expected User object")
def _(expected_user=user):
    fetched_user = fetch_user_by_id(id=expected_user.id)
    assert fetched_user == expected_user

I don't particularly like the @test decorator/naming thingy myself, but having namespaced fixtures which can be imported and used by specifying them as argument value seems pretty nice to me.

It's still "name matching magic", but at least a bit more explicit and, most importantly, namespaced.

Of course the million dollar question is how to implement this with the already quite complex fixture code, especially in a way that's backwards compatible. So far I haven't had the time (or mind-space) to see if it'd even be realistic at all.

@RonnyPfannschmidt
Copy link
Member

anything in the domain of adoption/modifying the mechanisms in which fixture lookup works is something for after we de-cruft and de-tangle it

from past experience its pretty much doomed before we simplify/expliticify it

my take is that we can't hope to do better until we make it reasonably sane to choose the dependency injection system and to integrate a external one with pytest fixtures as a base

personally i really want to enable better lookup, but that comes after de-tangleing what grew due to the history that started with the pytest_funcarg__somename hooks that wouldn't cache

@graingert
Copy link
Member

graingert commented Aug 18, 2020

now that pytest is Python3.5+ only, using an Annotations based DI is possible: https://fastapi.tiangolo.com/tutorial/dependencies/

@nicoddemus
Copy link
Member

nicoddemus commented Aug 19, 2020

@shoyer

If we agree that an explicit API is better, we might consider deprecating the current global name matching entirely

That's an idea, and probably the way to go for certain features, but not for fixture IMO: it would break every pytest suite out there which uses fixtures. This kind of breakage would kill the project IMO.

@ms-lolo

Totally understand wanting to avoid having multiple ways of doing this. And using a plugin to demonstrate a cleaner approach sounds like a great idea that I'm going to try and find some time to investigate, although I have no experience with the pytest internals, so any guidance would be appreciated!

It depends on the mechanism actually... there are some proposals in this thread, but importing and using the actual names in the test function definition, bypassing the current global lookup, might not be possible through a simple plugin, I'm afraid.

@RonnyPfannschmidt

my take is that we can't hope to do better until we make it reasonably sane to choose the dependency injection system and to integrate a external one with pytest fixtures as a base

Indeed, but it seems this would take many hours of someone dedicated to this task to tackle.

now that pytest is Python3.5+ only, using an Annotations based DI is possible: fastapi.tiangolo.com/tutorial/dependencies

Indeed, and that's similar to what ward does, as @The-Compiler commented.


My take overall:

  • Enabling more explicit fixture lookup would be great
  • Currently the pytest internals don't allow for that
  • Backward compatibility is a hard requirement

So this is a very complicated issue. 😓

@burnpanck
Copy link

What is the current status of this?

@nicoddemus
Copy link
Member

The current status is basically: not something that will be implemented in the core directly, but which someone can experiment using a plugin. Not sure if any progress has been done on the latter.

Btw is this useful to leave open?

@burnpanck
Copy link

I think it is. There are quite few people out there who would love such a feature (including prominent people such as @shoyer). This issue can serve as a meeting point to attempt pushing each other to move this a bit further ;-).

Magic name matching is a constant hurdle for new contributors not familiar with any given code-base: If a fixture is missing, there is absolutely no indication in the code where it is supposed to be loaded from - is it a local conftest.py, a missing pytest plugin or on some other obscure path. IMHO it just feels against the principles of python (e.g. 12. "In the face of ambiguity, refuse the temptation to guess." or 19. "Namespaces are one honking great idea -- let's do more of those!").

I guess the problem is that one usually gets bitten by this while working with foreign packages rather than one's own.

@nicoddemus
Copy link
Member

Thanks @burnpanck.

Even if closed, the issue is still searchable, and people can further comment. My experience is that by leaving it open we are signalling that this is something we plan to integrate into the core eventually, and people then ask the status on it because (understandably) don't want to go through dozens of posts to understand the status.

But I don't think it is a big deal to leave it open too.

@RonnyPfannschmidt
Copy link
Member

Personally, ever since typing and type annotation is part of the language I was hoping to experiment with this,

Unfortunately it's not clear if/when we can even create a base for this

@adriangb
Copy link

adriangb commented Dec 6, 2021

Hi folks, I've been working on a dependency injection system (repo, docs) that has an API inspired by FastAPI and scoping/caching rules (somewhat) inspired by Pytest. @graingert thought it might be applicable to this issue, so I wrote up a quick prototype:

from typing import Annotated, AsyncGenerator, List

import pytest

from di.pytest import Depends, inject


class Log(List[str]):
    pass


def dep(log: Log) -> int:
    log.append("dep")
    return 1


async def generator_dep(log: Log, inner_val: Annotated[int, Depends(dep)]) -> AsyncGenerator[int, None]:
    log.append("generator_dep setup")
    yield inner_val + 1
    log.append("generator_dep teardown")


@pytest.mark.anyio
@inject
async def test_inject(log: Log, v: Annotated[int, Depends(generator_dep)]) -> None:
    assert v == 2
    assert log == ["dep", "generator_dep setup"]  # teardown hasn't run yet

My WIP branch for this is here: adriangb/di#43

Before I do any more work on it, I wanted to post here to see if there'd be any interest / conceptual input into what an integration like this should look like. I have some thoughts on rough edges but I'll hold off on them for now.

@grahamgower
Copy link

I've been trying to annotate all my tests that use a fixture with pytest.mark.usefixtures (I don't write my own fixtures, because I dislike the pattern so much). Would it be possible to have a configuration option to disable magic name matching without an explicit pytest.mark.usefixtures?

@RonnyPfannschmidt
Copy link
Member

@grahamgower that particular one is out of scope for this issue and currently extremely unlikely as currently pytest has only name matching

@adriangb that approach looks interesting, but unless there is a way to use annotations instead of magic default values, this doest sit too well with me

with the current tech debt in pytest, im also under the impression, that unless we slowly externalize our own system first, we are unlikely to have success with integrating a new system in a way that wont break immense amounts of test-suites

in particular parameterization, dependencies and setup reduction reordering are going to be very challenging topics

i absolutely want pytest to see a better DI system, however we have to evolve the codebase into using one

if there was a api we could in parts start to use for better management of setupstate, value storage, interacting with fixture definition,
i beleive that would be a much better starting point than the outer facade of a di system

as pytest has a already existing internal DI system thats by no means easy to replace/reshape

@adriangb
Copy link

adriangb commented Dec 10, 2021

@adriangb that approach looks interesting, but unless there is a way to use annotations instead of magic default values, this doest sit too well with me

could you clarify what you mean? We also support v: Annotated[int, Depends(generator_dep)], and when you're injecting a class you don't even need that (log: Log). This is covered here. And it's completely customizable, so I guess you could even implement matching on names if you wanted to 🤷

My idea was to interface with the existing DI system. You'd be able to opt in to the above proposed DI system by (installing the dependency) and then adding the @inject decorator (or something like that). The test function itself would still be able to accept pytest fixtures, but sub-dependencies would not (so limited interoperability). And yeah you'd probably have to rewrite a lot of your code to really take advantage of it, but that would still be the case if pytest came out with a feature that allowed you to match on fixture objects/values instead of names. Maybe with a pytest plugin it could be more seamless (register pytest fixtures with the di container?), but I haven't looked into that.

@RonnyPfannschmidt
Copy link
Member

@adriangb i like that annotation approach, for pytest i would probalby have a helper to make those annotations spell nicely

its pretty strange that mypy/pyrright "interpret" strange default values, but don't handle annotations nicely

i suspect there will be some extra need for plugins (just like attrs, pydantic & co)

@adriangb
Copy link

adriangb commented Dec 10, 2021

i would probalby have a helper to make those annotations spell nicely

Do you mean like an Annotated alias?

its pretty strange that mypy/pyrright "interpret" strange default values, but don't handle annotations nicely

to be fair to those tools, they only interpret those default values because we hack them to do so (same thing FastAPI does more or less)

i suspect there will be some extra need for plugins (just like attrs, pydantic & co)

a plugin for MyPy or for pytest?

@graingert
Copy link
Member

@RonnyPfannschmidt

but don't handle annotations nicely

what do you mean by that?

@RonnyPfannschmidt
Copy link
Member

@graingert thats related to my misunderstanding about having the Dependent global function just ignore the return value for type checking

as it turns out, the "support" is just a tactical type ignore, no actual support is there

@rtaycher
Copy link

rtaycher commented Nov 3, 2022

@nicoddemus

your myfixtures decorator fixes the problem I have with pytest, making it more explicit.
Sadly it seems to not work anymore due to the deprecation and removal of directly calling fixtures

Fixture "my_fruit" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.

https://docs.pytest.org/en/7.1.x/deprecations.html?highlight=calling#calling-fixtures-directly (
Removed in version 4.0)

Are there any ways to make it work again with branch 7.2.*?

@nicoddemus
Copy link
Member

I just tried the example from #3834 (comment) and it works for me.

That error only happens when someone uses the @fixture decorator, which the example does not.

One workaround is to extract the fixture code you have into a normal function, and call that from the fixture and the myfixtures code.

@rtaycher
Copy link

rtaycher commented Nov 3, 2022

Yes I'm sorry if I confused you.
I used the fruit basket example from the pytest documentation which has a fixture decorator.

My understanding is that the fixture decorator makes it into a "proper" fixture and all the third party pytest plugins use such fixtures. I was hoping for a workaround for pytests name based matching which I worry will confuse people on my team. Maybe I misunderstood what you were trying to say in that comment.

Is there a way to do make a decorator similar to what you wrote possibly with getfixturevalue or something else that works on fixture decorated functions or am I just totally misunderstanding how the fixtures work and that isn't currently possible without a lot of effort?

@rtaycher
Copy link

rtaycher commented Nov 8, 2022

This seems to work as a useful workaround

def make_test_with_fixtures_from_defaults(fn):
    def default_func_names(func):
        # HT: https://stackoverflow.com/a/12627202/259130
        signature = inspect.signature(func)
        return [
            v.default.__name__
            for k, v in signature.parameters.items()
            if v.default is not inspect.Parameter.empty
        ]

    fixture_names = default_func_names(fn)

    z = textwrap.dedent(f'''\
    def test_func({', '.join(fixture_names)}):
        return fn({', '.join(fixture_names)})
    ''')
    scope = dict(fn=fn, fixture_names=fixture_names)
    exec(z, scope)
    test_func = scope['test_func']
    test_func.__name__ = fn.__name__
    return test_func

seems to work. Assuming you don't call the function by keyword argument. Does that every happen? Or are there any other corner cases or plugins this might have trouble with?

from _pytest.tmpdir import tmp_path
#...
@make_test_with_fixtures_from_defaults
def test_create_file(tmp=tmp_path):
    d = tmp / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text('lol')
    assert p.read_text() == 'lol'
    assert len(list(tmp.iterdir())) == 1

works as expected and passes(so do other examples I tried)

I know a lot of people don't like exec but it works. I could probably do something to set the signature with https://smarie.github.io/python-makefun/ instead but I think makefun might also use exec. Maybe https://github.com/dfee/forge?

@nicoddemus
Copy link
Member

Interesting approach. If you turn that into a package, I'm sure others would be interested in trying it out.

@rtaycher
Copy link

rtaycher commented Nov 8, 2022

I think this version of the workaround should support both default argument and decorator provided fixtures like ward does

def make_test_with_fixtures(**fixture_kwargs):
    def inner(fn):
        signature = inspect.signature(fn)
        keyword_names_to_fixtures = {
            k: fixture_kwargs.get(k, None) or v.default
            for k, v in signature.parameters.items()
        }
        assert all(v is not inspect.Parameter.empty
                   for v in keyword_names_to_fixtures.values()), (
            'every parameter should have a matching fixture function '
            'provided in either the decorator or default function')
        keyword_names_to_fixture_names = {k: f.__name__ for (k, f) in keyword_names_to_fixtures.items()}
        fixture_names = keyword_names_to_fixture_names.values()

        z = textwrap.dedent(f'''\
        def test_func({', '.join(fixture_names)}):
            return fn({', '.join(kname+'='+fname for (kname, fname) in keyword_names_to_fixture_names.items())})
        ''')
        scope = dict(fn=fn)
        exec(z, scope)
        test_func = scope['test_func']
        test_func.__name__ = fn.__name__
        return test_func
    return inner

I tested it out and both


@make_test_with_fixtures()
def test_bar1(f1=fix_w_yield1, f2=fix_w_yield2, tmp=tmp_path):
    print("test_bar")
    print(f'{tmp}')
    assert tmp.exists()

@make_test_with_fixtures(f1=fix_w_yield1, f2=fix_w_yield2, tmp=tmp_path)
def test_bar2(f1, f2, tmp):
    print("test_bar")
    print(f'{tmp}')
    assert tmp.exists()

test_bar1 and test_bar2 seem to work.

I'll try to put it in a package when I have time. Maybe this weekend

I want to see it it possible avoid exec first(possibly via the python-forge package).

@nicoddemus
Copy link
Member

I want to see it it possible avoid exec first(possibly via the python-forge package).

Note that is an implementation detail, which you can change at will later. 👍

@rtaycher
Copy link

rtaycher commented Nov 9, 2022

It's pretty barebones but people might find it useful to reference fixtures by reference.

https://github.com/rtaycher/pytest-fixture-ref
pip install git+https://github.com/rtaycher/pytest-fixture-ref.git
people should feel free to make suggestions or PRs or suggest name changes
I'll try to publish it on pypi before the weekend

pip install git+https://github.com/rtaycher/pytest-fixture-ref.git

from pytest_fixture_ref import make_test_with_fixtures

@make_test_with_fixtures()
def test_bar1(f1=fix_w_yield1, f2=fix_w_yield2, tmp=tmp_path):
    print("test_bar")
    print(f'{tmp}')
    assert tmp.exists()


@make_test_with_fixtures(f1=fix_w_yield1, f2=fix_w_yield2, tmp=tmp_path)
def test_bar2(f1, f2, tmp):
    print("test_bar")
    print(f'{tmp}')
    assert tmp.exists()

@rtaycher
Copy link

rtaycher commented Nov 9, 2022

@nicoddemus is there a common way to import pytest plugins?
from _pytest.tmpdir import tmp_path works for tmp_path but I couldn't find some of the others.
What about fixtures in various pytest plugins?

@nicoddemus
Copy link
Member

There is no standard for that, each plugin decides which API to expose; so in your case from pytest_fixture_ref import make_test_with_fixtures is fine. 👍

Btw, if you have Twitter, feel free to tweet about that project and I'll be glad to retweet (also we can ask the pytest-dev official twitter to retweet too).

@ms-lolo
Copy link

ms-lolo commented Nov 9, 2022

@rtaycher can I suggest splitting up the two versions of fixtures into their own decorators? I'm opposed to the version that inspects the functions' default values and I think it would be useful to have that decision be clearer when we write our tests. A lot of development on large code bases happens by copying nearby code and having them as separate decorators makes it clearer that, in this code base, we chose style A over style B.

@rtaycher
Copy link

rtaycher commented Nov 9, 2022

@rtaycher can I suggest splitting up the two versions of fixtures into their own decorators? I'm opposed to the version that inspects the functions' default values and I think it would be useful to have that decision be clearer when we write our tests. A lot of development on large code bases happens by copying nearby code and having them as separate decorators makes it clearer that, in this code base, we chose style A over style B.

@ms-lolo Could you open an issue with some suggested names? I tend to bikeshed maybe @fixtures_from_default_value and @fixtures_from_ref? Both sound not quite right to me

@rtaycher
Copy link

rtaycher commented Nov 17, 2022

@nicoddemus https://twitter.com/rtaycher1987/status/1593199919892156418
@ms-lolo I split them into using_fixtures_from_defaults/using_fixtures_from_kwargs since I realized this would work not just for tests but for fixtures that take other fixtures

sorry it took so long. Figuring out the CI/publish setup for the boilerplate I used took a while(hit a number of odd library issues, plus the autochangelog is really annoying)

It's now on pypi so people can just use pip install pytest-fixture-ref

@nicoddemus
Copy link
Member

Nice, thanks @rtaycher!

I also recommend that suggestions/requests to be made in https://github.com/rtaycher/pytest-fixture-ref/issues instead of in this thread, as that is the appropriate place. 👍

@YakovL
Copy link

YakovL commented Apr 30, 2024

A couple of notes:

  1. This also hurts DX if one uses Pylance as it also complains like Pylint; it also lacks completions

  2. The workaround

    @pytest.fixture(name='session_manager')
    def get_session_manager():
        return GameSessionsManager()
    
    def test_create_session(session_manager: GameSessionsManager):
        ...
    

    is fragile: since the GameSessionsManager type is casted to the argument, once implementation of get_session_manager is changed (say, returns GameSessionsManager | None), things can get broken.

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