mirror of https://github.com/python/cpython
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 <lains@riseup.net> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
ad6d162e51
commit
3592980f91
|
@ -353,6 +353,23 @@ Functions and classes provided:
|
|||
.. versionadded:: 3.5
|
||||
|
||||
|
||||
.. function:: chdir(path)
|
||||
|
||||
Non parallel-safe context manager to change the current working directory.
|
||||
As this changes a global state, the working directory, it is not suitable
|
||||
for use in most threaded or aync contexts. It is also not suitable for most
|
||||
non-linear code execution, like generators, where the program execution is
|
||||
temporarily relinquished -- unless explicitely desired, you should not yield
|
||||
when this context manager is active.
|
||||
|
||||
This is a simple wrapper around :func:`~os.chdir`, it changes the current
|
||||
working directory upon entering and restores the old one on exit.
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 3.11
|
||||
|
||||
|
||||
.. class:: ContextDecorator()
|
||||
|
||||
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
|
|||
using the same context manager.
|
||||
|
||||
:class:`threading.RLock` is an example of a reentrant context manager, as are
|
||||
:func:`suppress` and :func:`redirect_stdout`. Here's a very simple example of
|
||||
reentrant use::
|
||||
:func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very
|
||||
simple example of reentrant use::
|
||||
|
||||
>>> from contextlib import redirect_stdout
|
||||
>>> from io import StringIO
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Utilities for with-statement contexts. See PEP 343."""
|
||||
import abc
|
||||
import os
|
||||
import sys
|
||||
import _collections_abc
|
||||
from collections import deque
|
||||
|
@ -9,7 +10,8 @@ from types import MethodType, GenericAlias
|
|||
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
|
||||
"AbstractContextManager", "AbstractAsyncContextManager",
|
||||
"AsyncExitStack", "ContextDecorator", "ExitStack",
|
||||
"redirect_stdout", "redirect_stderr", "suppress", "aclosing"]
|
||||
"redirect_stdout", "redirect_stderr", "suppress", "aclosing",
|
||||
"chdir"]
|
||||
|
||||
|
||||
class AbstractContextManager(abc.ABC):
|
||||
|
@ -762,3 +764,18 @@ class nullcontext(AbstractContextManager, AbstractAsyncContextManager):
|
|||
|
||||
async def __aexit__(self, *excinfo):
|
||||
pass
|
||||
|
||||
|
||||
class chdir(AbstractContextManager):
|
||||
"""Non thread-safe context manager to change the current working directory."""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self._old_cwd = []
|
||||
|
||||
def __enter__(self):
|
||||
self._old_cwd.append(os.getcwd())
|
||||
os.chdir(self.path)
|
||||
|
||||
def __exit__(self, *excinfo):
|
||||
os.chdir(self._old_cwd.pop())
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Unit tests for contextlib.py, and other context managers."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
@ -1114,5 +1115,47 @@ class TestSuppress(unittest.TestCase):
|
|||
1/0
|
||||
self.assertTrue(outer_continued)
|
||||
|
||||
|
||||
class TestChdir(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
old_cwd = os.getcwd()
|
||||
target = os.path.join(os.path.dirname(__file__), 'data')
|
||||
self.assertNotEqual(old_cwd, target)
|
||||
|
||||
with chdir(target):
|
||||
self.assertEqual(os.getcwd(), target)
|
||||
self.assertEqual(os.getcwd(), old_cwd)
|
||||
|
||||
def test_reentrant(self):
|
||||
old_cwd = os.getcwd()
|
||||
target1 = os.path.join(os.path.dirname(__file__), 'data')
|
||||
target2 = os.path.join(os.path.dirname(__file__), 'ziptestdata')
|
||||
self.assertNotIn(old_cwd, (target1, target2))
|
||||
chdir1, chdir2 = chdir(target1), chdir(target2)
|
||||
|
||||
with chdir1:
|
||||
self.assertEqual(os.getcwd(), target1)
|
||||
with chdir2:
|
||||
self.assertEqual(os.getcwd(), target2)
|
||||
with chdir1:
|
||||
self.assertEqual(os.getcwd(), target1)
|
||||
self.assertEqual(os.getcwd(), target2)
|
||||
self.assertEqual(os.getcwd(), target1)
|
||||
self.assertEqual(os.getcwd(), old_cwd)
|
||||
|
||||
def test_exception(self):
|
||||
old_cwd = os.getcwd()
|
||||
target = os.path.join(os.path.dirname(__file__), 'data')
|
||||
self.assertNotEqual(old_cwd, target)
|
||||
|
||||
try:
|
||||
with chdir(target):
|
||||
self.assertEqual(os.getcwd(), target)
|
||||
raise RuntimeError("boom")
|
||||
except RuntimeError as re:
|
||||
self.assertEqual(str(re), "boom")
|
||||
self.assertEqual(os.getcwd(), old_cwd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
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`.
|
Loading…
Reference in New Issue