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..27bca301f75 --- /dev/null +++ b/changelog/10738.feature.rst @@ -0,0 +1 @@ +Added ``PYTEST_TMPDIR_FILE_MODE`` 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..0a5f9ac9ffd 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 `700`. + +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..0c6e517473b 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*. +.. envvar:: PYTEST_TMPDIR_FILE_MODE + +Sets the file mode of any temporary files or directories. Defaults to `700`. 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..bc9a4d1c33c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -206,8 +206,9 @@ 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: 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) @@ -746,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 a9299944dec..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 @@ -1502,7 +1503,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 +1524,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 5f347665f9a..52255d0a034 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -29,7 +29,9 @@ from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf from .pathlib import cleanup_dead_symlink -from _pytest.compat import final, get_user_id +from .pathlib import tmpdir_file_mode +from _pytest.compat import final +from _pytest.compat import get_user_id from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -135,9 +137,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 @@ -154,7 +158,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") @@ -164,11 +168,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). @@ -193,7 +197,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..bcede3613de 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", "777") + 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