The current working directory -- denoted by an empty string -- is handled
slightly differently from other entries on :data:`sys.path`. First, if the
-current working directory is found to not exist, no value is stored in
-:data:`sys.path_importer_cache`. Second, the value for the current working
-directory is looked up fresh for each module lookup. Third, the path used for
-:data:`sys.path_importer_cache` and returned by
+current working directory cannot be determined or is found not to exist, no
+value is stored in :data:`sys.path_importer_cache`. Second, the value for the
+current working directory is looked up fresh for each module lookup. Third,
+the path used for :data:`sys.path_importer_cache` and returned by
:meth:`importlib.machinery.PathFinder.find_spec` will be the actual current
working directory and not the empty string.
if path == '':
try:
path = _os.getcwd()
- except FileNotFoundError:
+ except (FileNotFoundError, PermissionError):
# Don't cache the failure as the cwd can easily change to
# a valid directory later on.
return None
return test if ok else unittest.skip(msg)(test)
+@contextlib.contextmanager
+def save_mode(path, *, quiet=False):
+ """Context manager that restores the mode (permissions) of *path* on exit.
+
+ Arguments:
+
+ path: Path of the file to restore the mode of.
+
+ quiet: if False (the default), the context manager raises an exception
+ on error. Otherwise, it issues only a warning and keeps the current
+ working directory the same.
+
+ """
+ saved_mode = os.stat(path)
+ try:
+ yield
+ finally:
+ try:
+ os.chmod(path, saved_mode.st_mode)
+ except OSError as exc:
+ if not quiet:
+ raise
+ warnings.warn(f'tests may fail, unable to restore the mode of '
+ f'{path!r} to {saved_mode.st_mode}: {exc}',
+ RuntimeWarning, stacklevel=3)
+
+
# Check whether the current effective user has the capability to override
# DAC (discretionary access control). Typically user root is able to
# bypass file read, write, and execute permission checks. The capability
+from test.support import os_helper
from test.test_importlib import util
importlib = util.import_importlib('importlib')
# Do not want FileNotFoundError raised.
self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
+ @os_helper.skip_unless_working_chmod
+ def test_permission_error_cwd(self):
+ # gh-115911: Test that an unreadable CWD does not break imports, in
+ # particular during early stages of interpreter startup.
+ with (
+ os_helper.temp_dir() as new_dir,
+ os_helper.save_mode(new_dir),
+ os_helper.change_cwd(new_dir),
+ util.import_state(path=['']),
+ ):
+ # chmod() is done here (inside the 'with' block) because the order
+ # of teardown operations cannot be the reverse of setup order. See
+ # https://github.com/python/cpython/pull/116131#discussion_r1739649390
+ try:
+ os.chmod(new_dir, 0o000)
+ except OSError:
+ self.skipTest("platform does not allow "
+ "changing mode of the cwd")
+
+ # Do not want PermissionError raised.
+ self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
+
def test_invalidate_caches_finders(self):
# Finders with an invalidate_caches() method have it called.
class FakeFinder:
--- /dev/null
+If the current working directory cannot be determined due to permissions,
+then import will no longer raise :exc:`PermissionError`. Patch by Alex
+Willmer.