From 65fb513b9e63e019c70f5462a319c3b86384359c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Fri, 3 Mar 2023 01:00:30 +0100 Subject: [PATCH 1/4] Add PYTEST_TMPDIR_FILE_MASK environment variable --- AUTHORS | 1 + changelog/10738.feature.rst | 1 + doc/en/how-to/tmp_path.rst | 12 ++++++++++++ doc/en/reference/reference.rst | 4 ++++ src/_pytest/pathlib.py | 4 +++- src/_pytest/pytester.py | 8 ++++++-- src/_pytest/tmpdir.py | 16 ++++++++++------ testing/test_tmpdir.py | 16 ++++++++++++++++ 8 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 changelog/10738.feature.rst diff --git a/AUTHORS b/AUTHORS index 0395feceb60..e4d9e413a28 100644 --- a/AUTHORS +++ b/AUTHORS @@ -172,6 +172,7 @@ Javier Romero Jeff Rackauckas Jeff Widman Jenni Rinker +Jim Brannlund John Eddie Ayson John Litborn John Towler diff --git a/changelog/10738.feature.rst b/changelog/10738.feature.rst new file mode 100644 index 00000000000..7c7b668dc31 --- /dev/null +++ b/changelog/10738.feature.rst @@ -0,0 +1 @@ +Added ``PYTEST_TMPDIR_FILE_MASK`` environment variable which controls the file permissions of any files or directories created by the ``tmp_path`` fixtures. diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index 792933dd87e..a58c1eb97af 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -155,4 +155,16 @@ When distributing tests on the local machine using ``pytest-xdist``, care is tak automatically configure a basetemp directory for the sub processes such that all temporary data lands below a single per-test run basetemp directory. + +.. _`file permissions`: + +File permissions +---------------- + +Any file or directory created by the above fixtures are by default created with private permissions (file mask 700). + +You can override the file mask by setting the :envvar:`PYTEST_TMPDIR_FILE_MODE` environment variable as an octal string, the default being `0o700`. + +This is for example useful in cases where created files or directories have to be shared by docker containers etc. + .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index c882157eeb7..adced601cca 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1090,6 +1090,10 @@ Sets a `pygment style `_ to use for the code Sets the :envvar:`PYTEST_THEME` to be either *dark* or *light*. +.. envar:: PYTEST_TMPDIR_FILE_MODE + +Sets the file mode of any temporary files or directories. Defaults to `0o700`. See :fixture:`tmp_path` :fixture:`tmp_path_factory`. + .. envvar:: PY_COLORS When set to ``1``, pytest will use color in terminal output. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 9f9463d8862..065ddebc49c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -51,6 +51,8 @@ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself ) +TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) + def _ignore_error(exception): return ( @@ -206,7 +208,7 @@ def _force_symlink( pass -def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: +def make_numbered_dir(root: Path, prefix: str, mode: int = TMPDIR_FILE_MODE) -> Path: """Create a directory with an increased number as suffix for the given prefix.""" for i in range(10): # try up to 10 times to create the folder diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a9299944dec..a9f15d4eb64 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -82,6 +82,8 @@ "/var/lib/sss/mc/passwd" ] +TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) + def pytest_addoption(parser: Parser) -> None: parser.addoption( @@ -1502,7 +1504,9 @@ def runpytest_subprocess( The result. """ __tracebackhide__ = True - p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) + p = make_numbered_dir( + root=self.path, prefix="runpytest-", mode=TMPDIR_FILE_MODE + ) args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: @@ -1521,7 +1525,7 @@ def spawn_pytest( The pexpect child is returned. """ basetemp = self.path / "temp-pexpect" - basetemp.mkdir(mode=0o700) + basetemp.mkdir(mode=TMPDIR_FILE_MODE) invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index ec44623dc24..0a50200ee04 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -41,6 +41,8 @@ tmppath_result_key = StashKey[Dict[str, bool]]() +TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) + @final @dataclasses.dataclass @@ -136,9 +138,11 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: basename = self._ensure_relative_to_basetemp(basename) if not numbered: p = self.getbasetemp().joinpath(basename) - p.mkdir(mode=0o700) + p.mkdir(mode=TMPDIR_FILE_MODE) else: - p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700) + p = make_numbered_dir( + root=self.getbasetemp(), prefix=basename, mode=TMPDIR_FILE_MODE + ) self._trace("mktemp", p) return p @@ -155,7 +159,7 @@ def getbasetemp(self) -> Path: basetemp = self._given_basetemp if basetemp.exists(): rm_rf(basetemp) - basetemp.mkdir(mode=0o700) + basetemp.mkdir(mode=TMPDIR_FILE_MODE) basetemp = basetemp.resolve() else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") @@ -165,11 +169,11 @@ def getbasetemp(self) -> Path: # make_numbered_dir() call rootdir = temproot.joinpath(f"pytest-of-{user}") try: - rootdir.mkdir(mode=0o700, exist_ok=True) + rootdir.mkdir(mode=TMPDIR_FILE_MODE, exist_ok=True) except OSError: # getuser() likely returned illegal characters for the platform, use unknown back off mechanism rootdir = temproot.joinpath("pytest-of-unknown") - rootdir.mkdir(mode=0o700, exist_ok=True) + rootdir.mkdir(mode=TMPDIR_FILE_MODE, exist_ok=True) # Because we use exist_ok=True with a predictable name, make sure # we are the owners, to prevent any funny business (on unix, where # temproot is usually shared). @@ -197,7 +201,7 @@ def getbasetemp(self) -> Path: root=rootdir, keep=keep, lock_timeout=LOCK_TIMEOUT, - mode=0o700, + mode=TMPDIR_FILE_MODE, ) assert basetemp is not None, basetemp self._basetemp = basetemp diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index fcb0775dd5f..8b34985ec2d 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -623,3 +623,19 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions( # After - fixed. assert (basetemp.parent.stat().st_mode & 0o077) == 0 + + +def test_tmp_path_factory_user_specified_permissions( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + """Verify that pytest creates directories under /tmp with user specified permissions.""" + # Use the test's tmp_path as the system temproot (/tmp). + monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path)) + monkeypatch.setenv("PYTEST_TMPDIR_FILE_MODE", "0o777") + tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) + basetemp = tmp_factory.getbasetemp() + + # User specified permissions. + assert (basetemp.stat().st_mode & 0o000) == 0 + # Parent too (pytest-of-foo). + assert (basetemp.parent.stat().st_mode & 0o000) == 0 From c788acebc28b602f52162f11e910ff3da61571f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Fri, 3 Mar 2023 16:43:46 +0100 Subject: [PATCH 2/4] missed one --- changelog/10738.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/10738.feature.rst b/changelog/10738.feature.rst index 7c7b668dc31..27bca301f75 100644 --- a/changelog/10738.feature.rst +++ b/changelog/10738.feature.rst @@ -1 +1 @@ -Added ``PYTEST_TMPDIR_FILE_MASK`` environment variable which controls the file permissions of any files or directories created by the ``tmp_path`` fixtures. +Added ``PYTEST_TMPDIR_FILE_MODE`` environment variable which controls the file permissions of any files or directories created by the ``tmp_path`` fixtures. From 72379263233d25c099acba47254982301a220478 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Mar 2023 12:56:48 -0300 Subject: [PATCH 3/4] Update doc/en/reference/reference.rst --- doc/en/reference/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index adced601cca..41ff4acbe54 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1090,7 +1090,7 @@ Sets a `pygment style `_ to use for the code Sets the :envvar:`PYTEST_THEME` to be either *dark* or *light*. -.. envar:: PYTEST_TMPDIR_FILE_MODE +.. envvar:: PYTEST_TMPDIR_FILE_MODE Sets the file mode of any temporary files or directories. Defaults to `0o700`. See :fixture:`tmp_path` :fixture:`tmp_path_factory`. From c83aafe4d3ebbc5a82c4ae9a061f5a44e7251208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Fri, 3 Mar 2023 20:57:06 +0100 Subject: [PATCH 4/4] changes based on PR comments --- doc/en/how-to/tmp_path.rst | 2 +- doc/en/reference/reference.rst | 2 +- src/_pytest/pathlib.py | 9 ++++++--- src/_pytest/pytester.py | 7 +++---- src/_pytest/tmpdir.py | 15 +++++++-------- testing/test_tmpdir.py | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index a58c1eb97af..0a5f9ac9ffd 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -163,7 +163,7 @@ File permissions Any file or directory created by the above fixtures are by default created with private permissions (file mask 700). -You can override the file mask by setting the :envvar:`PYTEST_TMPDIR_FILE_MODE` environment variable as an octal string, the default being `0o700`. +You can override the file mask by setting the :envvar:`PYTEST_TMPDIR_FILE_MODE` environment variable as an octal string, the default being `700`. This is for example useful in cases where created files or directories have to be shared by docker containers etc. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 41ff4acbe54..0c6e517473b 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1092,7 +1092,7 @@ Sets the :envvar:`PYTEST_THEME` to be either *dark* or *light*. .. envvar:: PYTEST_TMPDIR_FILE_MODE -Sets the file mode of any temporary files or directories. Defaults to `0o700`. See :fixture:`tmp_path` :fixture:`tmp_path_factory`. +Sets the file mode of any temporary files or directories. Defaults to `700`. See :fixture:`tmp_path` :fixture:`tmp_path_factory`. .. envvar:: PY_COLORS diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 065ddebc49c..bc9a4d1c33c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -51,8 +51,6 @@ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself ) -TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) - def _ignore_error(exception): return ( @@ -208,8 +206,9 @@ def _force_symlink( pass -def make_numbered_dir(root: Path, prefix: str, mode: int = TMPDIR_FILE_MODE) -> Path: +def make_numbered_dir(root: Path, prefix: str, mode: Union[int, None] = None) -> Path: """Create a directory with an increased number as suffix for the given prefix.""" + mode = mode or tmpdir_file_mode() for i in range(10): # try up to 10 times to create the folder max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) @@ -748,3 +747,7 @@ def copytree(source: Path, target: Path) -> None: shutil.copyfile(x, newx) elif x.is_dir(): newx.mkdir(exist_ok=True) + + +def tmpdir_file_mode() -> int: + return int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "700"), 8) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a9f15d4eb64..b6960251db7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -62,6 +62,7 @@ from _pytest.pathlib import bestrelpath from _pytest.pathlib import copytree from _pytest.pathlib import make_numbered_dir +from _pytest.pathlib import tmpdir_file_mode from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory @@ -82,8 +83,6 @@ "/var/lib/sss/mc/passwd" ] -TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) - def pytest_addoption(parser: Parser) -> None: parser.addoption( @@ -1505,7 +1504,7 @@ def runpytest_subprocess( """ __tracebackhide__ = True p = make_numbered_dir( - root=self.path, prefix="runpytest-", mode=TMPDIR_FILE_MODE + root=self.path, prefix="runpytest-", mode=tmpdir_file_mode() ) args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] @@ -1525,7 +1524,7 @@ def spawn_pytest( The pexpect child is returned. """ basetemp = self.path / "temp-pexpect" - basetemp.mkdir(mode=TMPDIR_FILE_MODE) + basetemp.mkdir(mode=tmpdir_file_mode()) invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 0a50200ee04..959549ddf8e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -30,6 +30,7 @@ from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf from .pathlib import cleanup_dead_symlink +from .pathlib import tmpdir_file_mode from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode @@ -41,8 +42,6 @@ tmppath_result_key = StashKey[Dict[str, bool]]() -TMPDIR_FILE_MODE = int(os.getenv("PYTEST_TMPDIR_FILE_MODE", "0o700"), 8) - @final @dataclasses.dataclass @@ -138,10 +137,10 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: basename = self._ensure_relative_to_basetemp(basename) if not numbered: p = self.getbasetemp().joinpath(basename) - p.mkdir(mode=TMPDIR_FILE_MODE) + p.mkdir(mode=tmpdir_file_mode()) else: p = make_numbered_dir( - root=self.getbasetemp(), prefix=basename, mode=TMPDIR_FILE_MODE + root=self.getbasetemp(), prefix=basename, mode=tmpdir_file_mode() ) self._trace("mktemp", p) return p @@ -159,7 +158,7 @@ def getbasetemp(self) -> Path: basetemp = self._given_basetemp if basetemp.exists(): rm_rf(basetemp) - basetemp.mkdir(mode=TMPDIR_FILE_MODE) + basetemp.mkdir(mode=tmpdir_file_mode()) basetemp = basetemp.resolve() else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") @@ -169,11 +168,11 @@ def getbasetemp(self) -> Path: # make_numbered_dir() call rootdir = temproot.joinpath(f"pytest-of-{user}") try: - rootdir.mkdir(mode=TMPDIR_FILE_MODE, exist_ok=True) + rootdir.mkdir(mode=tmpdir_file_mode(), exist_ok=True) except OSError: # getuser() likely returned illegal characters for the platform, use unknown back off mechanism rootdir = temproot.joinpath("pytest-of-unknown") - rootdir.mkdir(mode=TMPDIR_FILE_MODE, exist_ok=True) + rootdir.mkdir(mode=tmpdir_file_mode(), exist_ok=True) # Because we use exist_ok=True with a predictable name, make sure # we are the owners, to prevent any funny business (on unix, where # temproot is usually shared). @@ -201,7 +200,7 @@ def getbasetemp(self) -> Path: root=rootdir, keep=keep, lock_timeout=LOCK_TIMEOUT, - mode=TMPDIR_FILE_MODE, + mode=tmpdir_file_mode(), ) assert basetemp is not None, basetemp self._basetemp = basetemp diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 8b34985ec2d..bcede3613de 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -631,7 +631,7 @@ def test_tmp_path_factory_user_specified_permissions( """Verify that pytest creates directories under /tmp with user specified permissions.""" # Use the test's tmp_path as the system temproot (/tmp). monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path)) - monkeypatch.setenv("PYTEST_TMPDIR_FILE_MODE", "0o777") + monkeypatch.setenv("PYTEST_TMPDIR_FILE_MODE", "777") tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) basetemp = tmp_factory.getbasetemp()