Skip to content

Allow broader scoped fixtures to rely on narrower scoped fixtures #797

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

Closed
jaraco opened this issue Jun 22, 2015 · 6 comments
Closed

Allow broader scoped fixtures to rely on narrower scoped fixtures #797

jaraco opened this issue Jun 22, 2015 · 6 comments
Labels
type: question general question, might be closed after 2 weeks of inactivity

Comments

@jaraco
Copy link
Contributor

jaraco commented Jun 22, 2015

I have this scenario: a plugin provides a fixture for a resource:

@pytest.fixture(scope='function')
def cheap_thing():
    return dict(a=1, b=2)

In my application, I want to do some heavy processing on cheap thing and create an expensive thing. Because it's an expensive thing, I want it to be session scoped:

@pytest.fixture(scope='session')
def expensive_thing(cheap_thing):
    return expensive_transformation(cheap_thing)

However, the scoping rules disallow this usage. I can understand how the implementation may have gotten to a place where the scope processing might preclude naturally using a function-scoped fixture in a session-scoped fixture. However, I don't see any reason why the function-scoped fixture couldn't be invoked to be utilized in this way, where the function-scoped fixture is invoked and that instance of the fixture scheduled for teardown at the close of the session scope.

It might be possible to refactor the functionality like so:

@pytest.fixture(scope='session')
def expensive_thing():
    return expensive_transform(cheap_thing())

Except that presumes things about the behavior of cheap_thing. If it were changed to a yield_fixture, for example, that would break the workaround.

Is there something more intrinsic that would preclude the behavior proposed above? Would it add undue complexity?

Thanks for the consideration.

@nicoddemus
Copy link
Member

I think the problem is that it would violate an invariant where fixture objects are the same independently where they were created. For example, it makes sense that both a function and fixture which use tmpdir use the same tmpdir instance:

@pytest.fixture
def setup_with_files(tmpdir):
    tmpdir.join('input.txt').write('...)
    return MySetup()

def test_foo(tmpdir, setup_files):
    setup_files.check()
    contents = tmpdir.join('input.txt').read()   

That's the reason you can't use a narrow-scoped fixture as an input to a session fixture: every test which expects a function-scoped fixture expects a new object, which conflicts directly with the session's purpose of having a single object that lives during the entire testing session.

Hope this helps! 😄

@nicoddemus nicoddemus added the type: question general question, might be closed after 2 weeks of inactivity label Jul 5, 2015
@jaraco
Copy link
Contributor Author

jaraco commented Jul 6, 2015

Oh, that's interesting, and sort-of surprising. I would have expected in that use case that the fixture would have generated two different tmpdir objects from different invocations of the fixture.

I see how it might be convenient to be able to demand the same fixture, but that behavior strikes me as dangerous and implicit. What if one wants a different tmpdir? Consider if a function demands a tmpdir in which a specific set of files generated and tested for existence, but also demands another fixture (e.g. package_cache) which itself uses a tmpdir. The test might fail because of content added by the package_cache would be present, even though the tests expected a clean tmpdir.

Furthermore, it would be possible for any fixture, such as setup_with_files above to include the tmpdir in the MySetup object. e.g.:

@pytest.fixture
def setup_with_files(tmpdir):
    tmpdir.join('input.txt').write('...)
    return MySetup(tmpdir)

def test_foo(setup_files):
    setup_files.check()
    contents = setup_files.tmpdir.join('input.txt').read()

or

@pytest.fixture
def setup_with_files(tmpdir):
    tmpdir.join('input.txt').write('...)
    return tmpdir, MySetup()

def test_foo(setup_files):
    tmpdir, setup_files = setup_files
    setup_files.check()
    contents = tmpdir.join('input.txt').read()

It seems to me there's no clean way to implement the use case I've described in the original post.

Given that the intended functionality is possible without re-using fixtures, and given that the implicit re-use of fixtures is sometimes surprising and prevents the elevation of scope for a fixture, I would strongly consider changing that expectation.

I do note that what I'm suggesting is a potentially drastic change in expectation and probably substantial changes to the implementation.

This leads to a few questions. Is there another workaround for my use case? Am I wrong in thinking fixtures would be more flexible if not implicitly re-used (with minimal loss through explicit re-use)? Would the scope and impact of this kind of change be too great to consider it for a future (incompatible) release?

@nicoddemus
Copy link
Member

I see you point, but I personally disagree. When I discovered that fixtures re-used the same object, I thought "well that's interesting!". 😄

You example makes sense for tmpdir specifically, but in general it does make more sense to reuse the objects: consider my initial example returning MySetup, if other fixture also received a setup_files parameter, it would make sense to receive the same MySetup instance for that other fixture.

Either way, as you noted, this change is too drastic and would certainly break a lot of test suites out there (my own test suites, and I suspect many many others, rely on this behavior). Perhaps we could add this as an option (perhaps passing an optional argument to pytest.fixture), but I'm not entirely sure.

Is there another workaround for my use case?

I think the workaround you proposed is good enough... if one changes the implementation to an yield_fixture as you feared, you just have to check that in expensive_thing to catch that problem quickly.

Am I wrong in thinking fixtures would be more flexible if not implicitly re-used (with minimal loss through explicit re-use)?

I'm of the opinion that it would make fixtures less flexible and more dependent on one another... but that's my opinion, others may of course disagree. 😄

Would the scope and impact of this kind of change be too great to consider it for a future (incompatible) release?

I would say certainly for the 2.X series, as it would severely impact existing test suites, unless implemented in way that is optional like I mentioned above.

@RonnyPfannschmidt
Copy link
Member

for tmpdir there is a simple rough plan already, we want that any scope can have a tmpdir, and each scope would have a own fixture instance

the tmpdir_manager could be used to ask for all tmpdirs materialized in the parent item chain for a item

@nicoddemus
Copy link
Member

Just to add here, in 2.8 we have a tmpdir_factory fixture which can be used by any fixture to generate temporary directories.

@RonnyPfannschmidt
Copy link
Member

Closing this one as done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question general question, might be closed after 2 weeks of inactivity
Projects
None yet
Development

No branches or pull requests

3 participants