Michiel Karrenbelt
Michiel Karrenbelt

Reputation: 143

Python context manager that backs up and restores `cwd` leaves terminal in erroneous state

I have a context manager that temporarily backs up a directory and restores it upon exit. Here's the implementation:

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)

Here's a test that uses the context manager:

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()):
        ...
(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

The directory appears empty until I run cd . to "refresh" it.

Additional Observations

Debugging attempt:

I tried modifying the context manager to change the working directory before removing/restoring the original directory:

@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)

While this also passes the test, the terminal state is still inconsistent after execution. I must manually refresh it (cd .) to resolve the issue just the same.

Questions:

Update:

Upvotes: 1

Views: 55

Answers (1)

Tim Roberts
Tim Roberts

Reputation: 54767

If you are removing and replacing the "current directory" (that is, "."), then this is expected. The shell knows your "current directory" by its inode number, not by its path. When you remove that directory, the inode points into empty space. Even though you create a new ".", that "." has a new inode number. The shell has cached the old one. The "cd ." fixes this.

Upvotes: 1

Related Questions