备份并恢复目录的Python上下文管理器导致终端状态异常的问题咨询
备份并恢复目录的Python上下文管理器导致终端状态异常的问题咨询
我写了一个上下文管理器,用于临时备份目录并在退出时恢复它,实现代码如下:
import shutil import tempfile from pathlib import Path from contextlib import contextmanager @contextmanager def on_exit(directory: Path): backup = Path(tempfile.mkdtemp(prefix="backup_")) / directory.name shutil.copytree(directory, backup, symlinks=True) try: yield finally: shutil.rmtree(directory) shutil.copytree(backup, directory, symlinks=True) shutil.rmtree(backup)
下面是使用这个上下文管理器的测试用例:
from pathlib import Path from src.utils import rollback def get_repo_root() -> Path: current_dir = Path(__file__).resolve() while current_dir != current_dir.parent: if (current_dir / ".git").exists(): return current_dir current_dir = current_dir.parent raise RuntimeError("No repository root found.") def test_rollback_on_exit(): with rollback.on_exit(get_repo_root()): ...
directory是我的项目根目录,我从这个目录运行pytest。- 测试能通过,但执行后我的终端会进入不一致的状态。比如运行测试后,我遇到了这样的行为:
(dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> echo $PWD /home/user/projects/dummy (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> echo $VIRTUAL_ENV /home/user/.cache/pypoetry/virtualenvs/dummy-83ZLFGNe-py3.12 (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> ls -la | wc -l 17 (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> pytest tests/test_rollback.py ============================================= test session starts ============================================== platform linux -- Python 3.12.3, pytest-7.2.1, pluggy-1.5.0 rootdir: /home/user/projects/dummy plugins: cov-6.0.0, asyncio-0.21.1, typeguard-4.4.1, hypothesis-6.119.3 asyncio: mode=Mode.STRICT collected 1 item tests/test_rollback.py . [100%] =============================================== warnings summary =============================================== tests/test_rollback.py: 20 warnings /home/user/.cache/pypoetry/virtualenvs/dummy-83ZLFGNe-py3.12/lib/python3.12/site-packages/typer/core.py:300: DeprecationWarning: 'autocompletion' is renamed to 'shell_complete'. The old name is deprecated and will be removed in Click 8.1. See the docs about 'Parameter' for information about new behavior. _typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion) -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ======================================== 1 passed, 20 warnings in 0.74s ======================================== (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> echo $PWD /home/user/projects/dummy (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> echo $VIRTUAL_ENV /home/user/.cache/pypoetry/virtualenvs/dummy-83ZLFGNe-py3.12 (dummy-py3.12) user@laptop ~/p/dummy> ls -la | wc -l 1 (dummy-py3.12) user@laptop ~/p/dummy> cd . (dummy-py3.12) user@laptop ~/p/dummy (feat/rollback)> ls -la | wc -l 17
目录看起来是空的,直到我运行cd .来“刷新”它才恢复正常。
额外观察
- 这个问题不只是pytest会遇到,比如测试后运行
source ~/.config/fish/config.fish也会产生类似错误(getcwd: cannot access parent directories)。 $PWD和$VIRTUAL_ENV在测试前后都没有变化,说明虚拟环境本身不是问题所在。
调试尝试
我尝试修改上下文管理器,在删除/恢复原目录前切换工作目录:
@contextmanager def change_dir(target_path): original_path = Path.cwd() try: os.chdir(target_path) yield finally: os.chdir(original_path) @contextmanager def on_exit(directory: Path): backup = Path(tempfile.mkdtemp(prefix="backup_")) / directory.name shutil.copytree(directory, backup, symlinks=True) try: yield finally: with change_dir("/"): shutil.rmtree(directory) shutil.copytree(backup, directory, symlinks=True) shutil.rmtree(backup)
虽然测试还是能通过,但执行后终端状态仍然不一致,我还是必须手动执行cd .来解决问题。
问题
- 为什么会发生这种情况?这和shell、pytest还是虚拟环境有关?
- 我该如何修改我的上下文管理器或测试设置,才能确保测试执行后终端状态完全恢复正常?
更新
- 在删除和替换仓库前切换目录确实有效,如果不这么做,测试后的状态无法正确重置,其他测试会受影响并失败。
- 我尝试通过bash脚本运行,没用。
- 我尝试在子shell中运行,没用。
- 我尝试使用pytest finalizers,也没用。
备注:内容来源于stack exchange,提问作者Michiel Karrenbelt
相关产品推荐
相关产品推荐

