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

Session scoped fixture is called twice #3217

Closed
bdjellab opened this issue Feb 13, 2018 · 7 comments
Closed

Session scoped fixture is called twice #3217

bdjellab opened this issue Feb 13, 2018 · 7 comments
Labels
topic: fixtures anything involving fixtures directly or indirectly type: question general question, might be closed after 2 weeks of inactivity

Comments

@bdjellab
Copy link

Hello,

Version: I'm running Ubuntu 14.04 with pytest 3.4.0.

Use case: I am using a session scoped fixture which I import and use in two different test files.

Issue: The fixture is run one time for each file when I expect it to be run only once for the whole session.

Example:

fixtures.py

import pytest

@pytest.fixture(scope="session")
def my_fixture():
    print "Hello"

test1.py

from fixtures import my_fixture

def test_example(my_fixture):
    pass

test2.py

from fixtures import my_fixture

def test_example2(my_fixture):
    pass

Hello is printed twice instead of once.

Workaround: A workaround is to import it in the conftest but since I am using hundred of fixtures, I prefer not to import everything in the conftest.

@pytestbot pytestbot added the topic: fixtures anything involving fixtures directly or indirectly label Feb 13, 2018
@feuillemorte
Copy link
Contributor

feuillemorte commented Feb 14, 2018

Hi!

You don't need to import fixtures

You can use fixtures in test files like

@pytest.fixture()
def print_word():
    print('This is fixture!')
    

def test_one(print_word):
    assert 1

or you can add your fixture file to conftest as plugin:
my_custom_fixtures.py

import pytest


@pytest.fixture(scope='session')
def print_word():
    print('This is fixture!')

conftest.py

pytest_plugins = ['my_custom_fixtures']

test_one.py

def test_one(print_word):
    assert 1

test_two.py

def test_one(print_word):
    assert 1

result:

collected 2 items                                                                                                                                                                                                                                                                                                     

tests/test_one.py This is fixture!
.
tests/test_two.py .

if you have different files with your fixtures you can add it in your conftest like

pytest_plugins = [
    'path.to.fixtures',
    'path.to.another.fixtures',
    'path.to.third.fixtures.file',
    ...
]

but please note this in future:
https://github.com/pytest-dev/pytest/wiki/Deprecation-Roadmap#pytest_plugins-in-non-toplevel-conftests

pytest_plugins in non-toplevel conftests

There is a deep conceptual confusion as conftest.py files themselves are activated/deactivated based on path, but the plugins they depend on aren't.

Not yet officially deprecated.

@bdjellab
Copy link
Author

bdjellab commented Feb 14, 2018

Hi,

Thanks for the info, I knew you could import fixtures in the conftest but I didn't know you could do it as a plugin.

However, do you mean that I must not import fixtures like that?
Am I doing something that's against pytest rules or is it a limitation/bug of pytest?

@feuillemorte
Copy link
Contributor

feuillemorte commented Feb 15, 2018

I might be wrong, but I guess:
When you import fixture pytest creates another base_id for test file. And your scope session starts to work in this file and you have another session fixture fo this file. Check my tests:
structure:

tests
|- conftest.py
|- test_one.py
|- test_two.py
\- test_three.py

files:
conftest.py:

import random
import pytest

@pytest.fixture(scope='session')
def print_word():
    print('This is fixture!')
    return random.randint(1, 100)

test_one.py:

from conftest import print_word

def test_one(print_word):
    print(print_word)
    assert 1

def test_one_2(print_word):
    print(print_word)
    assert 1

test_two.py:

def test_two(print_word):
    print(print_word)
    assert 1


def test_two_2(print_word):
    print(print_word)
    assert 1

test_three.py:

def test_three(print_word):
    print(print_word)
    assert 1


def test_three_2(print_word):
    print(print_word)
    assert 1

start tests by command

pytest tests -s

FixtureDef objects:

test_one.py:  (<FixtureDef name='print_word' scope='session' baseid='tests' >, <FixtureDef name='print_word' scope='session' baseid='tests/test_one.py' >)
test_two.py: (<FixtureDef name='print_word' scope='session' baseid='tests' >,)
test_three.py: (<FixtureDef name='print_word' scope='session' baseid='tests' >,)

Output and results:

tests/test_one.py
This is fixture!
40
.
40
.
tests/test_three.py 
This is fixture!
91
.
91
.
tests/test_two.py
91
.
91
.

I don't think that it's a bug, but you can ask more core team (@nicoddemus , @The-Compiler , @RonnyPfannschmidt ) for details or check this code:

pytest/_pytest/fixtures.py

Lines 1016 to 1044 in e7bcc85

def getfixtureclosure(self, fixturenames, parentnode):
# collect the closure of all fixtures , starting with the given
# fixturenames as the initial set. As we have to visit all
# factory definitions anyway, we also return a arg2fixturedefs
# mapping so that the caller can reuse it and does not have
# to re-discover fixturedefs again for each fixturename
# (discovering matching fixtures for a given name/node is expensive)
parentid = parentnode.nodeid
fixturenames_closure = self._getautousenames(parentid)
def merge(otherlist):
for arg in otherlist:
if arg not in fixturenames_closure:
fixturenames_closure.append(arg)
merge(fixturenames)
arg2fixturedefs = {}
lastlen = -1
while lastlen != len(fixturenames_closure):
lastlen = len(fixturenames_closure)
for argname in fixturenames_closure:
if argname in arg2fixturedefs:
continue
fixturedefs = self.getfixturedefs(argname, parentid)
if fixturedefs:
arg2fixturedefs[argname] = fixturedefs
merge(fixturedefs[-1].argnames)
return fixturenames_closure, arg2fixturedefs

@RonnyPfannschmidt
Copy link
Member

bascially - fixtures come from plugins, or the test module - if each module import the fixture, then pytest considers each imported fixture as own separate fixture of the test module

so pytest basically sees 2 fixtures with the same name, one in the one test module, the other in the other test module

the most simple way to consolidate is a conftest, followed by having a pytest plugin

@RonnyPfannschmidt RonnyPfannschmidt added the type: question general question, might be closed after 2 weeks of inactivity label Feb 15, 2018
@bdjellab
Copy link
Author

Okay thanks !

It seemed strange to me because - correct me if I'm wrong - importing the same function in two different files actually gives the exact same object in memory.

I will import all my session scoped fixtures in the conftest then.

@RonnyPfannschmidt
Copy link
Member

correct, thats a good solution 👍

in general we consider it good practice put fixtures that are shared across modules/folders into conftests for consistency

i closing this issue as the problem seems to be solved, if you feel there is more to solve please dont hesitate to call out

@erwinkinn
Copy link

May the force be with you, folks! I've smashed helluva day to understand what the heck is happening with those scopes!
Cannot believe pytest works this way. Whatever framework specific rules they must be a subset of the rules of Python.
I couldn't expect I import the same name from module and get different objects in terms of pytest.
Thank you!

rohinb2 added a commit to run-house/runhouse that referenced this issue Nov 30, 2023
When setting up the fixture list within a class, we were doing something along the lines of:

```
class TestClass:
    UNIT = {"cluster": [local_cluster]}
```

This requires an import of `local_cluster` from conftest. Turns out, this causes session scoped fixtures to be recomputed (pytest-dev/pytest#3217). It's bad practice to import from conftest in the first place. So, I changed the fixtures that run `getfixturevalue` to just use the string argument, and we can avoid importing, and rely on pytest to set up the fixtures correctly. Now we set up fixtures like so:

```
class TestClass:
    UNIT = {"cluster": ["local_cluster"]}
```

Moreover... if a fixture is defined in a nested `conftest.py`, and imported in the global `conftest.py`, then there can be conflicts. The way tests work is that they'll look heirarchically upward in conftest.py files, and then grab the first fixture they find that matches the name. Even if the global `conftest.py` imports from a nested `conftest.py` file, it is considered a different fixture object (unintuitive, yes). This will sometimes lead to session scoped fixtures being initialized twice in the same session. More info on nested conftest.py files [here](pytest-dev/pytest#1931).
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: question general question, might be closed after 2 weeks of inactivity
Projects
None yet
Development

No branches or pull requests

5 participants