From 67b116146eca6ac6563d48df0ac38dd238e7b18d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 Jan 2020 18:12:35 +0100 Subject: [PATCH] pytester: testdir: add makefiles helper This is a sane method to create a set of files, allowing for absolute paths. Ref: https://github.com/pytest-dev/pytest/pull/6578 Ref: https://github.com/pytest-dev/pytest/pull/6579 --- changelog/6603.feature.rst | 1 + src/_pytest/pytester.py | 27 ++++++++++++++++++ testing/test_pytester.py | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 changelog/6603.feature.rst diff --git a/changelog/6603.feature.rst b/changelog/6603.feature.rst new file mode 100644 index 00000000000..1a58437097a --- /dev/null +++ b/changelog/6603.feature.rst @@ -0,0 +1 @@ +Add :py:func:`~_pytest.pytester.Testdir.makefiles` helper to :ref:`testdir`, which allows to more easily create files with absolute paths. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index cfe1b9a6ca5..92beb97280a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -7,6 +7,7 @@ import re import subprocess import sys +import textwrap import time import traceback from fnmatch import fnmatch @@ -672,6 +673,32 @@ def maketxtfile(self, *args, **kwargs): """Shortcut for .makefile() with a .txt extension.""" return self._makefile(".txt", args, kwargs) + def makefiles( + self, files: Dict[str, str], allow_outside_tmpdir=False + ) -> List[Path]: + """Create the given set of files. + + Unlike other helpers like :func:`makepyfile` this allows to specify + absolute paths, which need to be below :attr:`tmpdir` by default + (use `allow_outside_tmpdir` to write arbitrary files). + """ + paths = [] + if allow_outside_tmpdir: + validated_files = tuple((Path(k), v) for k, v in files.items()) + else: + tmpdir_path = Path(self.tmpdir) + validated_files = tuple( + (Path(k).absolute().relative_to(tmpdir_path), v) + for k, v in files.items() + ) + + for fpath, content in validated_files: + path = Path(self.tmpdir).joinpath(fpath) + with open(str(path), "w") as fp: + fp.write(textwrap.dedent(content)) + paths.append(path) + return paths + def syspathinsert(self, path=None): """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 869e35db3e1..9b6a6b09620 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,7 +1,11 @@ +import builtins import os +import re import subprocess import sys import time +from collections import OrderedDict +from contextlib import contextmanager from typing import List import py.path @@ -11,9 +15,11 @@ from _pytest.config import PytestPluginManager from _pytest.main import ExitCode from _pytest.outcomes import Failed +from _pytest.pathlib import Path from _pytest.pytester import CwdSnapshot from _pytest.pytester import HookRecorder from _pytest.pytester import LineMatcher +from _pytest.pytester import MonkeyPatch from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot from _pytest.pytester import Testdir @@ -710,3 +716,53 @@ def test_error2(bad_fixture): result.assert_outcomes(error=2) assert result.parseoutcomes() == {"error": 2} + + +def test_testdir_makefiles(testdir: Testdir, monkeypatch: MonkeyPatch) -> None: + tmpdir = testdir.tmpdir + + abspath = str(tmpdir / "bar") + created_paths = testdir.makefiles(OrderedDict({"foo": "", abspath: ""})) + p1 = created_paths[0] + assert isinstance(p1, Path) + relpath = tmpdir / "foo" + assert str(p1) == str(relpath) + + p2 = created_paths[1] + assert p2.exists() + assert str(p2) == abspath + + assert testdir.makefiles({}) == [] + + # Disallows creation outside of tmpdir by default. + with pytest.raises( + ValueError, + match="'/abspath' does not start with '{}'".format(re.escape(str(tmpdir))), + ): + testdir.makefiles({"shouldnotbecreated": "", "/abspath": ""}) + # Validation before creating anything. + assert not Path("shouldnotbecreated").exists() + + # Support writing arbitrary files on request. + open_calls = [] + orig_open = builtins.open + + @contextmanager + def mocked_open(*args): + open_calls.append(["__enter__", args]) + with orig_open(os.devnull, *args[1:]) as fp: + yield fp + + with monkeypatch.context() as mp: + mp.setattr(builtins, "open", mocked_open) + created_paths = testdir.makefiles({"/abspath": ""}, allow_outside_tmpdir=True) + assert created_paths == [Path("/abspath")] + assert open_calls == [["__enter__", ("/abspath", "w")]] + + # Duplicated files (absolute and relative). + created_paths = testdir.makefiles(OrderedDict({"bar": "1", abspath: "2"})) + with open("bar", "r") as fp: + assert fp.read() == "2" + created_paths = testdir.makefiles(OrderedDict({abspath: "2", "bar": "1"})) + with open("bar", "r") as fp: + assert fp.read() == "1"