Skip to content

Commit 81192ca

Browse files
committed
pytester: use monkeypatch.chdir() for dir changing
The current method as the following problem, described by Sadra Barikbin: The tests that request both `pytester` and `monkeypatch` and use `monkeypatch.chdir` without context, relying on `monkeypatch`'s teardown to restore cwd. This doesn't work because the following sequence of actions take place: - `monkeypatch` is set up. - `pytester` is set up. It saves the original cwd and changes it to a new one dedicated to the test function. - Test function calls `monkeypatch.chdir()` without context. `monkeypatch` saves cwd, which is not the original one, before changing it. - `pytester` is torn down. It restores the cwd to the original one. - `monkeypatch` is torn down. It restores cwd to what it has saved. The solution here is to have pytester use `monkeypatch.chdir()` itself, then everything is handled correctly.
1 parent 4ae102c commit 81192ca

File tree

7 files changed

+47
-59
lines changed

7 files changed

+47
-59
lines changed

changelog/11315.trivial.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory.
2+
If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored.
3+
Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead.

src/_pytest/pytester.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -625,14 +625,6 @@ def assert_outcomes(
625625
)
626626

627627

628-
class CwdSnapshot:
629-
def __init__(self) -> None:
630-
self.__saved = os.getcwd()
631-
632-
def restore(self) -> None:
633-
os.chdir(self.__saved)
634-
635-
636628
class SysModulesSnapshot:
637629
def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None:
638630
self.__preserve = preserve
@@ -696,15 +688,14 @@ def __init__(
696688
#: be added to the list. The type of items to add to the list depends on
697689
#: the method using them so refer to them for details.
698690
self.plugins: List[Union[str, _PluggyPlugin]] = []
699-
self._cwd_snapshot = CwdSnapshot()
700691
self._sys_path_snapshot = SysPathsSnapshot()
701692
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
702-
self.chdir()
703693
self._request.addfinalizer(self._finalize)
704694
self._method = self._request.config.getoption("--runpytest")
705695
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
706696

707697
self._monkeypatch = mp = monkeypatch
698+
self.chdir()
708699
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
709700
# Ensure no unexpected caching via tox.
710701
mp.delenv("TOX_ENV_DIR", raising=False)
@@ -735,7 +726,6 @@ def _finalize(self) -> None:
735726
"""
736727
self._sys_modules_snapshot.restore()
737728
self._sys_path_snapshot.restore()
738-
self._cwd_snapshot.restore()
739729

740730
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
741731
# Some zope modules used by twisted-related tests keep internal state
@@ -760,7 +750,7 @@ def chdir(self) -> None:
760750
761751
This is done automatically upon instantiation.
762752
"""
763-
os.chdir(self.path)
753+
self._monkeypatch.chdir(self.path)
764754

765755
def _makefile(
766756
self,

testing/_py/test_local.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,14 +1080,14 @@ def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir):
10801080
name = "pointsback123"
10811081
ModuleType = type(os)
10821082
p = tmpdir.ensure(name + ".py")
1083-
for ending in (".pyc", "$py.class", ".pyo"):
1084-
mod = ModuleType(name)
1085-
pseudopath = tmpdir.ensure(name + ending)
1086-
mod.__file__ = str(pseudopath)
1087-
monkeypatch.setitem(sys.modules, name, mod)
1088-
newmod = p.pyimport()
1089-
assert mod == newmod
1090-
monkeypatch.undo()
1083+
with monkeypatch.context() as mp:
1084+
for ending in (".pyc", "$py.class", ".pyo"):
1085+
mod = ModuleType(name)
1086+
pseudopath = tmpdir.ensure(name + ending)
1087+
mod.__file__ = str(pseudopath)
1088+
mp.setitem(sys.modules, name, mod)
1089+
newmod = p.pyimport()
1090+
assert mod == newmod
10911091
mod = ModuleType(name)
10921092
pseudopath = tmpdir.ensure(name + "123.py")
10931093
mod.__file__ = str(pseudopath)

testing/code/test_excinfo.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,11 @@ def entry():
854854
reprtb = p.repr_traceback(excinfo)
855855
assert len(reprtb.reprentries) == 3
856856

857-
def test_traceback_short_no_source(self, importasmod, monkeypatch) -> None:
857+
def test_traceback_short_no_source(
858+
self,
859+
importasmod,
860+
monkeypatch: pytest.MonkeyPatch,
861+
) -> None:
858862
mod = importasmod(
859863
"""
860864
def func1():
@@ -866,14 +870,14 @@ def entry():
866870
excinfo = pytest.raises(ValueError, mod.entry)
867871
from _pytest._code.code import Code
868872

869-
monkeypatch.setattr(Code, "path", "bogus")
870-
p = FormattedExcinfo(style="short")
871-
reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
872-
lines = reprtb.lines
873-
last_p = FormattedExcinfo(style="short")
874-
last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
875-
last_lines = last_reprtb.lines
876-
monkeypatch.undo()
873+
with monkeypatch.context() as mp:
874+
mp.setattr(Code, "path", "bogus")
875+
p = FormattedExcinfo(style="short")
876+
reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
877+
lines = reprtb.lines
878+
last_p = FormattedExcinfo(style="short")
879+
last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
880+
last_lines = last_reprtb.lines
877881
assert lines[0] == " func1()"
878882

879883
assert last_lines[0] == ' raise ValueError("hello")'

testing/test_assertrewrite.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,11 @@ def test_foo():
895895
)
896896

897897
@pytest.mark.skipif('"__pypy__" in sys.modules')
898-
def test_pyc_vs_pyo(self, pytester: Pytester, monkeypatch) -> None:
898+
def test_pyc_vs_pyo(
899+
self,
900+
pytester: Pytester,
901+
monkeypatch: pytest.MonkeyPatch,
902+
) -> None:
899903
pytester.makepyfile(
900904
"""
901905
import pytest
@@ -905,13 +909,13 @@ def test_optimized():
905909
)
906910
p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-")
907911
tmp = "--basetemp=%s" % p
908-
monkeypatch.setenv("PYTHONOPTIMIZE", "2")
909-
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
910-
monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
911-
assert pytester.runpytest_subprocess(tmp).ret == 0
912-
tagged = "test_pyc_vs_pyo." + PYTEST_TAG
913-
assert tagged + ".pyo" in os.listdir("__pycache__")
914-
monkeypatch.undo()
912+
with monkeypatch.context() as mp:
913+
mp.setenv("PYTHONOPTIMIZE", "2")
914+
mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
915+
mp.delenv("PYTHONPYCACHEPREFIX", raising=False)
916+
assert pytester.runpytest_subprocess(tmp).ret == 0
917+
tagged = "test_pyc_vs_pyo." + PYTEST_TAG
918+
assert tagged + ".pyo" in os.listdir("__pycache__")
915919
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
916920
monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
917921
assert pytester.runpytest_subprocess(tmp).ret == 1

testing/test_pathlib.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,15 @@ def test_check_filepath_consistency(
236236
name = "pointsback123"
237237
p = tmp_path.joinpath(name + ".py")
238238
p.touch()
239-
for ending in (".pyc", ".pyo"):
240-
mod = ModuleType(name)
241-
pseudopath = tmp_path.joinpath(name + ending)
242-
pseudopath.touch()
243-
mod.__file__ = str(pseudopath)
244-
monkeypatch.setitem(sys.modules, name, mod)
245-
newmod = import_path(p, root=tmp_path)
246-
assert mod == newmod
247-
monkeypatch.undo()
239+
with monkeypatch.context() as mp:
240+
for ending in (".pyc", ".pyo"):
241+
mod = ModuleType(name)
242+
pseudopath = tmp_path.joinpath(name + ending)
243+
pseudopath.touch()
244+
mod.__file__ = str(pseudopath)
245+
mp.setitem(sys.modules, name, mod)
246+
newmod = import_path(p, root=tmp_path)
247+
assert mod == newmod
248248
mod = ModuleType(name)
249249
pseudopath = tmp_path.joinpath(name + "123.py")
250250
pseudopath.touch()

testing/test_pytester.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import subprocess
33
import sys
44
import time
5-
from pathlib import Path
65
from types import ModuleType
76
from typing import List
87

@@ -11,7 +10,6 @@
1110
from _pytest.config import ExitCode
1211
from _pytest.config import PytestPluginManager
1312
from _pytest.monkeypatch import MonkeyPatch
14-
from _pytest.pytester import CwdSnapshot
1513
from _pytest.pytester import HookRecorder
1614
from _pytest.pytester import LineMatcher
1715
from _pytest.pytester import Pytester
@@ -301,17 +299,6 @@ def test_assert_outcomes_after_pytest_error(pytester: Pytester) -> None:
301299
result.assert_outcomes(passed=0)
302300

303301

304-
def test_cwd_snapshot(pytester: Pytester) -> None:
305-
foo = pytester.mkdir("foo")
306-
bar = pytester.mkdir("bar")
307-
os.chdir(foo)
308-
snapshot = CwdSnapshot()
309-
os.chdir(bar)
310-
assert Path().absolute() == bar
311-
snapshot.restore()
312-
assert Path().absolute() == foo
313-
314-
315302
class TestSysModulesSnapshot:
316303
key = "my-test-module"
317304

0 commit comments

Comments
 (0)