Skip to content

Commit f06124d

Browse files
committed
Ignore PermissionError when checking cwd during import
On macOS `getcwd(3)` can return EACCES if a path component isn't readable, resulting in PermissionError. `PathFinder.find_spec()` now catches these and ignores them - the same treatment as a missing/deleted cwd. Introduces 2 new `test.support.os_helper` context managers - `save_mode(path, ...)` - restores the mode of a path on exit - `save_cwd(...)` - restores the current directory on exit Unlike `change_cwd(path)` (which always performs a `chdir(path)`), `save_cwd()` allows the code inside the `with` block to control this. The new context manager just restores the original working directory afterward. This is allows finer control of exception handling and robust environment restoration across platforms in `FinderTests.test_permission_error_cwd()`.
1 parent 7595e67 commit f06124d

File tree

5 files changed

+83
-6
lines changed

5 files changed

+83
-6
lines changed

Doc/reference/import.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -871,10 +871,10 @@ module.
871871

872872
The current working directory -- denoted by an empty string -- is handled
873873
slightly differently from other entries on :data:`sys.path`. First, if the
874-
current working directory is found to not exist, no value is stored in
875-
:data:`sys.path_importer_cache`. Second, the value for the current working
876-
directory is looked up fresh for each module lookup. Third, the path used for
877-
:data:`sys.path_importer_cache` and returned by
874+
current working directory cannot be determined or is found to not exist, no
875+
value is stored in :data:`sys.path_importer_cache`. Second, the value for the
876+
current working directory is looked up fresh for each module lookup. Third,
877+
the path used for :data:`sys.path_importer_cache` and returned by
878878
:meth:`importlib.machinery.PathFinder.find_spec` will be the actual current
879879
working directory and not the empty string.
880880

Lib/importlib/_bootstrap_external.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ def _path_importer_cache(cls, path):
15061506
if path == '':
15071507
try:
15081508
path = _os.getcwd()
1509-
except FileNotFoundError:
1509+
except (FileNotFoundError, PermissionError):
15101510
# Don't cache the failure as the cwd can easily change to
15111511
# a valid directory later on.
15121512
return None

Lib/test/support/os_helper.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,33 @@ def skip_unless_working_chmod(test):
294294
return test if ok else unittest.skip(msg)(test)
295295

296296

297+
@contextlib.contextmanager
298+
def save_mode(path, *, quiet=False):
299+
"""Context manager that restores the mode (permissions) of path on exit.
300+
301+
Arguments:
302+
303+
path: Path of the file to restore mode of.
304+
305+
quiet: if False (the default), the context manager raises an exception
306+
on error. Otherwise, it issues only a warning and keeps the current
307+
working directory the same.
308+
309+
"""
310+
saved_mode = os.stat(path)
311+
try:
312+
yield
313+
finally:
314+
try:
315+
os.chmod(path, saved_mode.st_mode)
316+
except OSError as exc:
317+
if not quiet:
318+
raise
319+
warnings.warn(f'tests may fail, unable to restore the mode of '
320+
f'{path!r} to {saved_mode.st_mode}: {exc}',
321+
RuntimeWarning, stacklevel=3)
322+
323+
297324
# Check whether the current effective user has the capability to override
298325
# DAC (discretionary access control). Typically user root is able to
299326
# bypass file read, write, and execute permission checks. The capability
@@ -508,9 +535,36 @@ def temp_dir(path=None, quiet=False):
508535
rmtree(path)
509536

510537

538+
@contextlib.contextmanager
539+
def save_cwd(*, quiet=False):
540+
"""Return a context manager that restores the current working directory on
541+
exit.
542+
543+
Arguments:
544+
545+
quiet: if False (the default), the context manager raises an exception
546+
on error. Otherwise, it issues only a warning and keeps the current
547+
working directory the same.
548+
549+
"""
550+
saved_dir = os.getcwd()
551+
try:
552+
yield
553+
finally:
554+
try:
555+
os.chdir(saved_dir)
556+
except OSError as exc:
557+
if not quiet:
558+
raise
559+
warnings.warn(f'tests may fail, unable to restore the current '
560+
f'working directory to {saved_dir!r}: {exc}',
561+
RuntimeWarning, stacklevel=3)
562+
563+
511564
@contextlib.contextmanager
512565
def change_cwd(path, quiet=False):
513-
"""Return a context manager that changes the current working directory.
566+
"""Return a context manager that changes the current working directory on
567+
entry, and restores it on exit.
514568
515569
Arguments:
516570

Lib/test/test_importlib/import_/test_path.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from test.support import os_helper
12
from test.test_importlib import util
23

34
importlib = util.import_importlib('importlib')
@@ -153,6 +154,25 @@ def test_deleted_cwd(self):
153154
# Do not want FileNotFoundError raised.
154155
self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
155156

157+
@os_helper.skip_unless_working_chmod
158+
def test_permission_error_cwd(self):
159+
# gh-115911
160+
with (
161+
os_helper.temp_dir() as new_dir,
162+
os_helper.save_mode(new_dir),
163+
os_helper.save_cwd(),
164+
util.import_state(path=['']),
165+
):
166+
os.chdir(new_dir)
167+
try:
168+
os.chmod(new_dir, 0o000)
169+
except OSError:
170+
self.skipTest("platform does not allow "
171+
"changing mode of the cwd")
172+
173+
# Do not want PermissionError raised.
174+
self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
175+
156176
def test_invalidate_caches_finders(self):
157177
# Finders with an invalidate_caches() method have it called.
158178
class FakeFinder:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
If the current working directory cannot be determined due to permissions,
2+
then import will no longer raise :exc:`PermissionError`. Patch by Alex
3+
Willmer.

0 commit comments

Comments
 (0)