Skip to content

mode 0o000 directory causes following test suites with -W error to fail #15169

Closed

Description

@sertonix

(Sorry for the wall of text, it is a very specific edge case)

When running a pytest test suite that creates a directory with mode 0o000 in it's tmp_path and pytest attempts to clean up that directory (seems to happen only after running pytest a few times) pytest shows a warning that it could not remove the tmp_path due to "Directory not empty". Test suites which consider warnings as errors (eg. using -W error) can fail due to this even if the tests are all correct.

Ideally rm_rf() in _pytest/pathlib.py would set the S_IRUSR|S_IWUSR|S_IXUSR bits of directories while recursing. I have not tried it yet but I suspect that the on_rm_rf_error handler runs to late to re-try recursing into directories.

Depending on a condition that I couldn't figure out yet the error is ether about scandir or Directory not empty (both error messages are below). In the latter case it seems like the directory with mode 0o000 is silently ignored causing the parent directory to not be removable.

A minimal shell script to reproduce the issue:

cd "$(mktemp -d)"
export TMPDIR="$PWD"

mkdir tests
cat << EOF > tests/test.py
def test_foo(tmp_path: Path) -> None:
	(tmp_path / "mode-0").mkdir(mode=0o000)
EOF

# passes
pytest tests/test.py
# passes, because pytest is not yet attempting to remove the mod 0o000 directory
pytest -W error tests/test.py
pytest -W error tests/test.py
# fails
pytest -W error tests/test.py

A testsuite which creates such a directory can be found here: https://github.com/tox-dev/python-discovery/blob/469741a689fee16fe41135335d5cc10a767dd16c/tests/test_discovery.py#L78

This happens on alpine linux edge with python 3.14.8 and py3-pytest 9.1.1 but I am relatively certain that this is not relevant to the issue.

tests/test.py .                                                                                                                                                       [100%]Traceback (most recent call last):
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 367, in cleanup_numbered_dir
    try_cleanup(path, consider_lock_dead_if_created_before)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ResourceWarning: unclosed scandir iterator <posix.ScandirIterator object at 0x7f323c078230>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/bin/pytest", line 8, in <module>
    sys.exit(_console_main())
             ~~~~~~~~~~~~~^^
  File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 253, in _console_main
    code = _main(prog=_get_prog_name(sys.argv))
  File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 235, in _main
    config._ensure_unconfigure()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 1212, in _ensure_unconfigure
    self.hook.pytest_unconfigure(config=self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 173, in pytest_unconfigure
    collect_unraisable(config)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 79, in collect_unraisable
    raise errors[0]
  File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 67, in collect_unraisable
    warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing scandir iterator <posix.ScandirIterator object at 0x7f323c078230>: None
Traceback (most recent call last):
  File "/usr/lib/python3.14/shutil.py", line 753, in _rmtree_safe_fd_step
    os.rmdir(name, dir_fd=dirfd)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 39] Directory not empty: '/run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 203, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/usr/lib/python3.14/site-packages/pytest/__main__.py", line 9, in <module>
    raise SystemExit(_console_main())
                     ~~~~~~~~~~~~~^^
  File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 253, in _console_main
    code = _main(prog=_get_prog_name(sys.argv))
  File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 229, in _main
    ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/usr/lib/python3.14/site-packages/_pytest/main.py", line 377, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "/usr/lib/python3.14/site-packages/_pytest/main.py", line 365, in wrap_session
    config.hook.pytest_sessionfinish(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        session=session, exitstatus=session.exitstatus
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/logging.py", line 888, in pytest_sessionfinish
    return (yield)
            ^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/terminal.py", line 961, in pytest_sessionfinish
    result = yield
             ^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/warnings.py", line 119, in pytest_sessionfinish
    return (yield)
            ^^^^^
  File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/usr/lib/python3.14/site-packages/_pytest/tmpdir.py", line 340, in pytest_sessionfinish
    tmp_path_factory._exit_stack.close()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.14/contextlib.py", line 627, in close
    self.__exit__(None, None, None)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/contextlib.py", line 619, in __exit__
    raise exc
  File "/usr/lib/python3.14/contextlib.py", line 604, in __exit__
    if cb(*exc_details):
       ~~^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/contextlib.py", line 482, in _exit_wrapper
    callback(*args, **kwds)
    ~~~~~~~~^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 369, in cleanup_numbered_dir
    try_cleanup(path, consider_lock_dead_if_created_before)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 338, in try_cleanup
    maybe_delete_a_numbered_dir(path)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 290, in maybe_delete_a_numbered_dir
    rm_rf(garbage)
    ~~~~~^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 167, in rm_rf
    shutil.rmtree(str(path), onexc=onerror)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/shutil.py", line 852, in rmtree
    _rmtree_impl(path, dir_fd, onexc)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/shutil.py", line 721, in _rmtree_safe_fd
    _rmtree_safe_fd_step(stack, onexc)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/shutil.py", line 802, in _rmtree_safe_fd_step
    onexc(func, path, err)
    ~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 95, in on_rm_rf_error
    warnings.warn(
    ~~~~~~~~~~~~~^
        PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
pytest.PytestWarning: (rm_rf) error removing /run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0
<class 'OSError'>: [Errno 39] Directory not empty: '/run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0'

Activity

  1. sertonix commented on Oct 13, 2026

    @sertonix
    Author

    Not sure how I missed this but it looks like #14331 already fixed the issue on the main branch.

    The scandir iterator fix from #15171 might still be relevant but I don't know how to determine that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions