Mailing List Archive

bpo-25625: add contextlib.chdir (GH-28271)
https://github.com/python/cpython/commit/3592980f9122ab0d9ed93711347742d110b749c2
commit: 3592980f9122ab0d9ed93711347742d110b749c2
branch: main
author: Filipe Laíns <lains@riseup.net>
committer: ambv <lukasz@langa.pl>
date: 2021-10-20T00:19:27+02:00
summary:

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>

files:
A Misc/NEWS.d/next/Library/2021-09-10-12-53-28.bpo-25625.SzcBCw.rst
M Doc/library/contextlib.rst
M Lib/contextlib.py
M Lib/test/test_contextlib.py

diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst
index bc38a63a52d97..ae0ee7232a10c 100644
--- a/Doc/library/contextlib.rst
+++ b/Doc/library/contextlib.rst
@@ -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
diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index d90ca5d8ef988..ee72258505714 100644
--- a/Lib/contextlib.py
+++ b/Lib/contextlib.py
@@ -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 @@
__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 @@ async def __aenter__(self):

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())
diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py
index 7982d9d835a2b..bc8e4e4e2918f 100644
--- a/Lib/test/test_contextlib.py
+++ b/Lib/test/test_contextlib.py
@@ -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 @@ def test_cm_is_reentrant(self):
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()
diff --git a/Misc/NEWS.d/next/Library/2021-09-10-12-53-28.bpo-25625.SzcBCw.rst b/Misc/NEWS.d/next/Library/2021-09-10-12-53-28.bpo-25625.SzcBCw.rst
new file mode 100644
index 0000000000000..c001683b657f5
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-09-10-12-53-28.bpo-25625.SzcBCw.rst
@@ -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`.

_______________________________________________
Python-checkins mailing list
Python-checkins@python.org
https://mail.python.org/mailman/listinfo/python-checkins