Welcome, guest | Sign In | My Account | Store | Cart

A context manager that handle a temporary change of the working directory

Python, 75 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/python
# -*- encoding: utf-8 -*-
from __future__ import with_statement
import os
import os.path

class ChangeDirectory(object):
    """
    ChangeDirectory is a context manager that allowing 
    you to temporary change the working directory.

    >>> import tempfile
    >>> td = os.path.realpath(tempfile.mkdtemp())
    >>> currentdirectory = os.getcwd()
    >>> with ChangeDirectory(td) as cd:
    ...     assert cd.current == td
    ...     assert os.getcwd() == td
    ...     assert cd.previous == currentdirectory
    ...     assert os.path.normpath(os.path.join(cd.current, cd.relative)) == cd.previous
    ...
    >>> assert os.getcwd() == currentdirectory
    >>> with ChangeDirectory(td) as cd:
    ...     os.mkdir('foo')
    ...     with ChangeDirectory('foo') as cd2:
    ...         assert cd2.previous == cd.current
    ...         assert cd2.relative == '..'
    ...         assert os.getcwd() == os.path.join(td, 'foo')
    ...     assert os.getcwd() == td
    ...     assert cd.current == td
    ...     os.rmdir('foo')
    ...
    >>> os.rmdir(td)
    >>> with ChangeDirectory('.') as cd:
    ...     assert cd.current == currentdirectory
    ...     assert cd.current == cd.previous
    ...     assert cd.relative == '.'
    """

    def __init__(self, directory):
        self._dir = directory
        self._cwd = os.getcwd()
        self._pwd = self._cwd

    @property
    def current(self):
        return self._cwd
    
    @property
    def previous(self):
        return self._pwd
    
    @property
    def relative(self):
        c = self._cwd.split(os.path.sep)
        p = self._pwd.split(os.path.sep)
        l = min(len(c), len(p))
        i = 0
        while i < l and c[i] == p[i]:
            i += 1
        return os.path.normpath(os.path.join(*(['.'] + (['..'] * (len(c) - i)) + p[i:])))
    
    def __enter__(self):
        self._pwd = self._cwd
        os.chdir(self._dir)
        self._cwd = os.getcwd()
        return self

    def __exit__(self, *args):
        os.chdir(self._pwd)
        self._cwd = self._pwd


if __name__ == '__main__':
    import doctest
    doctest.testmod()

5 comments

David Lambert 15 years, 2 months ago  # | flag

might be easier to use contextlib.contexmanager decorator, if that exists in your python version.

Carlos Valiente 12 years, 9 months ago  # | flag

Nice! I'm using something a bit simpler, since all the information from the properties of ChangeDirectory is available outside the context mnager:

import contextlib
import os


@contextlib.contextmanager
def working_directory(path):
    """A context manager which changes the working directory to the given
    path, and then changes it back to its previous value on exit.

    """
    prev_cwd = os.getcwd()
    os.chdir(path)
    yield
    os.chdir(prev_cwd)
Greg Warner 11 years, 9 months ago  # | flag

I added the try/finally lines so that if an exception occurs, then the directory is still switched back.

import contextlib
import os


@contextlib.contextmanager
def working_directory(path):
    """A context manager which changes the working directory to the given
    path, and then changes it back to its previous value on exit.

    """
    prev_cwd = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(prev_cwd)
Alison Smith 7 years, 4 months ago  # | flag

@Greg: Adding the try/finally has no effect; even without them, the directory is switched back if an exception occurs. That is the whole point of a context manager!

The code by Carlos is the correct solution.

Alison Smith 7 years, 4 months ago  # | flag

@Greg: Woah, sorry, you were right. It turns out you do need the try/finally block.

I thought there was no way that yield could possibly throw an exception because the code called as a result of a yield happens outside the generator's call stack, but it turns out that it is possible by calling the .throw() method of a generator. contextlib does this for contextmanager-decorated generators, so an exception in a with block propogates to an exception in the decorated generator.