Skip to content

Commit 3592980

Browse files
FFY00ambv
andauthored
bpo-25625: add contextlib.chdir (GH-28271)
Added non parallel-safe :func:`~contextlib.chdir` context manager to change the current working directory and then restore it on exit. Simple wrapper around :func:`~os.chdir`. Signed-off-by: Filipe Laíns <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent ad6d162 commit 3592980

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
lines changed

Doc/library/contextlib.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,23 @@ Functions and classes provided:
353353
.. versionadded:: 3.5
354354

355355

356+
.. function:: chdir(path)
357+
358+
Non parallel-safe context manager to change the current working directory.
359+
As this changes a global state, the working directory, it is not suitable
360+
for use in most threaded or aync contexts. It is also not suitable for most
361+
non-linear code execution, like generators, where the program execution is
362+
temporarily relinquished -- unless explicitely desired, you should not yield
363+
when this context manager is active.
364+
365+
This is a simple wrapper around :func:`~os.chdir`, it changes the current
366+
working directory upon entering and restores the old one on exit.
367+
368+
This context manager is :ref:`reentrant <reentrant-cms>`.
369+
370+
.. versionadded:: 3.11
371+
372+
356373
.. class:: ContextDecorator()
357374

358375
A base class that enables a context manager to also be used as a decorator.
@@ -900,8 +917,8 @@ but may also be used *inside* a :keyword:`!with` statement that is already
900917
using the same context manager.
901918

902919
:class:`threading.RLock` is an example of a reentrant context manager, as are
903-
:func:`suppress` and :func:`redirect_stdout`. Here's a very simple example of
904-
reentrant use::
920+
:func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very
921+
simple example of reentrant use::
905922

906923
>>> from contextlib import redirect_stdout
907924
>>> from io import StringIO

Lib/contextlib.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Utilities for with-statement contexts. See PEP 343."""
22
import abc
3+
import os
34
import sys
45
import _collections_abc
56
from collections import deque
@@ -9,7 +10,8 @@
910
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
1011
"AbstractContextManager", "AbstractAsyncContextManager",
1112
"AsyncExitStack", "ContextDecorator", "ExitStack",
12-
"redirect_stdout", "redirect_stderr", "suppress", "aclosing"]
13+
"redirect_stdout", "redirect_stderr", "suppress", "aclosing",
14+
"chdir"]
1315

1416

1517
class AbstractContextManager(abc.ABC):
@@ -762,3 +764,18 @@ async def __aenter__(self):
762764

763765
async def __aexit__(self, *excinfo):
764766
pass
767+
768+
769+
class chdir(AbstractContextManager):
770+
"""Non thread-safe context manager to change the current working directory."""
771+
772+
def __init__(self, path):
773+
self.path = path
774+
self._old_cwd = []
775+
776+
def __enter__(self):
777+
self._old_cwd.append(os.getcwd())
778+
os.chdir(self.path)
779+
780+
def __exit__(self, *excinfo):
781+
os.chdir(self._old_cwd.pop())

Lib/test/test_contextlib.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Unit tests for contextlib.py, and other context managers."""
22

33
import io
4+
import os
45
import sys
56
import tempfile
67
import threading
@@ -1114,5 +1115,47 @@ def test_cm_is_reentrant(self):
11141115
1/0
11151116
self.assertTrue(outer_continued)
11161117

1118+
1119+
class TestChdir(unittest.TestCase):
1120+
def test_simple(self):
1121+
old_cwd = os.getcwd()
1122+
target = os.path.join(os.path.dirname(__file__), 'data')
1123+
self.assertNotEqual(old_cwd, target)
1124+
1125+
with chdir(target):
1126+
self.assertEqual(os.getcwd(), target)
1127+
self.assertEqual(os.getcwd(), old_cwd)
1128+
1129+
def test_reentrant(self):
1130+
old_cwd = os.getcwd()
1131+
target1 = os.path.join(os.path.dirname(__file__), 'data')
1132+
target2 = os.path.join(os.path.dirname(__file__), 'ziptestdata')
1133+
self.assertNotIn(old_cwd, (target1, target2))
1134+
chdir1, chdir2 = chdir(target1), chdir(target2)
1135+
1136+
with chdir1:
1137+
self.assertEqual(os.getcwd(), target1)
1138+
with chdir2:
1139+
self.assertEqual(os.getcwd(), target2)
1140+
with chdir1:
1141+
self.assertEqual(os.getcwd(), target1)
1142+
self.assertEqual(os.getcwd(), target2)
1143+
self.assertEqual(os.getcwd(), target1)
1144+
self.assertEqual(os.getcwd(), old_cwd)
1145+
1146+
def test_exception(self):
1147+
old_cwd = os.getcwd()
1148+
target = os.path.join(os.path.dirname(__file__), 'data')
1149+
self.assertNotEqual(old_cwd, target)
1150+
1151+
try:
1152+
with chdir(target):
1153+
self.assertEqual(os.getcwd(), target)
1154+
raise RuntimeError("boom")
1155+
except RuntimeError as re:
1156+
self.assertEqual(str(re), "boom")
1157+
self.assertEqual(os.getcwd(), old_cwd)
1158+
1159+
11171160
if __name__ == "__main__":
11181161
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added non parallel-safe :func:`~contextlib.chdir` context manager to change
2+
the current working directory and then restore it on exit. Simple wrapper
3+
around :func:`~os.chdir`.

0 commit comments

Comments
 (0)