28

The tmpdir fixture in py.test uses the function scope and thus isn't available in a fixture with a broader scope such as session. However, this would be useful for some cases such as setting up a temporary PostgreSQL server (which of course shouldn't be recreated for each test).

Is there any clean way to get a temporary folder for a broader scope that does not involve writing my own fixture and accessing internal APIs of py.test?

ThiefMaster
  • 310,957
  • 84
  • 592
  • 636

4 Answers4

38

Since pytest release 2.8 and above the session-scoped tmpdir_factory fixture is available. See the example below from the documentation.

# contents of conftest.py
import pytest

@pytest.fixture(scope='session')
def image_file(tmpdir_factory):
    img = compute_expensive_image()
    fn = tmpdir_factory.mktemp('data').join('img.png')
    img.save(str(fn))
    return fn

# contents of test_image.py
def test_histogram(image_file):
    img = load_image(image_file)
    # compute and test histogram
Mehraban
  • 3,164
  • 4
  • 37
  • 60
itsafire
  • 5,607
  • 3
  • 37
  • 48
  • 1
    To use the base temporary directory directly, instead of creating a sub-directory 'data', use the undocumented: `tmp_path_factory.getbasetemp()`. – matt.baker May 26 '22 at 10:27
14

Unfortunately there is currently no way (2014) of doing this nicely. In the future py.test will introduce a new "any" scope or something similar for this, but that's the future.

Right now you have to do this manually yourself. However as you note you lose quite a few nice features: symlinks in /tmp to the last test, auto cleanup after a few test runs, sensibly named directories etc. If the directory is not too expensive I usually combine a session and function scoped fixture in the following way:

@pytest.fixture(scope='session')
def session_dir(request):
    temp_dir = py.path.local(tempfile.mkdtemp())
    request.addfinalizer(lambda: folder.remove(rec=1))
    # Any extra setup here
    return temp_dir

@pytest.fixture
def temp_dir(session_dir, tmpdir):
    session_dir.copy(tmpdir)
    return tmpdir

This creates a temporary directory which gets cleaned up after a test run, however for each test which actually needs it (by requesting temp_dir) gets a copy which is saved with the tmpdir semantics.

If tests actually need to share state via this directory then the finalizer of temp_dir would have to copy things back to the session_dir. This is however not a very good idea since it makes the tests reliant on the execution order and would also cause problems when using pytest-xdist.

Gulzar
  • 23,452
  • 27
  • 113
  • 201
flub
  • 5,953
  • 27
  • 24
  • Would we run into any trouble if we simply copied the built-in tmpdir to a new plugin name, and scope it as we like? – ajwood Jul 28 '15 at 13:58
  • Probably, IIRC the name of the tmpdir depends on the function scope and that might start to cause trouble. But I haven't checked, try it if you like! – flub Jul 31 '15 at 11:11
  • 2
    Since 2.8, this is no longer the case. See https://stackoverflow.com/a/38050261/384617. – David Pärsson Jun 13 '17 at 11:10
1

I add a finalizer when I want to delete all temporary folders created in session.

_tmp_factory = None
@pytest.fixture(scope="session")
def tmp_factory(request, tmpdir_factory):
    global _tmp_factory
    if _tmp_factory is None:
        _tmp_factory = tmpdir_factory
        request.addfinalizer(cleanup)
    return _tmp_factory

def cleanup():
    root = _tmp_factory.getbasetemp().strpath
    print "Cleaning all temporary folders from %s" % root
    shutil.rmtree(root)

def test_deleting_temp(tmp_factory):
    root_a = tmp_factory.mktemp('A')
    root_a.join('foo.txt').write('hello world A')

    root_b = tmp_factory.mktemp('B')
    root_b.join('bar.txt').write('hello world B')

    for root, _, files in os.walk(tmp_factory.getbasetemp().strpath):
        for name in files:
            print(os.path.join(root, name))

The output should be like:

/tmp/pytest-of-agp/pytest-0/.lock
/tmp/pytest-of-agp/pytest-0/A0/foo.txt
/tmp/pytest-of-agp/pytest-0/B0/bar.txt
Cleaning all temporary folders from /tmp/pytest-of-agp/pytest-0
asterio gonzalez
  • 1,056
  • 12
  • 12
0

Here's another approach. It looks like pytest doesn't remove temporary directories after test runs. The following is a regular function-scoped fixture.

# conftest.py
TMPDIRS = list()

@pytest.fixture
def tmpdir_session(tmpdir):
    """A tmpdir fixture for the session scope. Persists throughout the session."""
    if not TMPDIRS:
        TMPDIRS.append(tmpdir)
    return TMPDIRS[0]

And to have persistent temporary directories across modules instead of the whole pytest session:

# conftest.py
TMPDIRS = dict()

@pytest.fixture
def tmpdir_module(request, tmpdir):
    """A tmpdir fixture for the module scope. Persists throughout the  module."""
    return TMPDIRS.setdefault(request.module.__name__, tmpdir)

Edit: Here's another solution that doesn't involve global variables. pytest 1.8.0 introduced a tmpdir_factory fixture that we can use:

@pytest.fixture(scope='module')
def tmpdir_module(request, tmpdir_factory):
    """A tmpdir fixture for the module scope. Persists throughout the module."""
    return tmpdir_factory.mktemp(request.module.__name__)


@pytest.fixture(scope='session')
def tmpdir_session(request, tmpdir_factory):
    """A tmpdir fixture for the session scope. Persists throughout the pytest session."""
    return tmpdir_factory.mktemp(request.session.name)
Robpol86
  • 1,594
  • 21
  • 18
  • Why don't you use `scope='session'` and `scope='module'` instead of messing around with global variables? – ThiefMaster Aug 08 '15 at 10:02
  • About the not removing part, it's semi-true: It keeps the last 3 temp dirs around, which is mentioned somewhere in the docs, i.e. it's a feature. – ThiefMaster Aug 08 '15 at 10:03
  • You can't use scope='session' or 'module' when you're using the tmpdir builtin fixture. You get an exception telling you you're trying to use a function-scoped fixture within a session/module scope fixture. Hence this workaround. – Robpol86 Aug 08 '15 at 18:39
  • Ah right... now that you mention it I remember running into the same problem. I'd probably try to run the original `tempdir` code in a custom fixture... – ThiefMaster Aug 08 '15 at 22:26