As others have pointed out, you need to await the decorated function before restoring the working directory, as invoking an async function doesn't execute it.
As others have also pointed out, doing this correctly is much harder than it seems because a coroutine can suspend to the event loop while running and, while it is suspended, a different coroutine can change the directory using the same decorator. With a simple-minded implementation of the decorator, resuming the original coroutine would break it because the working directory would no longer be the one it expects. Ideally you'd avoid this issue by structuring your code so it doesn't rely on the current working directory. But technically, it is possible to implement a correct directory-preserving decorator, it just takes additional effort. While I don't recommend that you do this in production, if you're curious how to do it, read on.
This answer shows how to apply a context manager every time a coroutine is resumed. The idea is to create a coroutine wrapper, an awaitable whose __await__
invokes the __await__
of the original coroutine. Normally this would use yield from
, but our wrapper doesn't do so, and instead emulates it with a hand-written loop that uses send()
to resume the inner awaitable's iterator. This provides control over every suspension and resumption of the inner awaitable, which is utilized to apply the context manager on every resumption. Note that this requires a reusable context manager, one that can be entered more than once.
To implement the decorator we will need a reusable directory-preserving context manager that not only restores the previous working directory in __exit__
, but also re-applies it in the next __enter__
. The former restores the old working directory whenever the coroutine is suspended (or when it returns), and the latter restores the new working directory whenever the coroutine is resumed. The decorator will just pass this context manager to the coroutine wrapper:
# copy CoroWrapper from https://stackoverflow.com/a/56079900/1600898
# context manager preserving the current directory
# can be re-entered multiple times
class PreserveDir:
def __init__(self):
self.inner_dir = None
def __enter__(self):
self.outer_dir = os.getcwd()
if self.inner_dir is not None:
os.chdir(self.inner_dir)
def __exit__(self, *exc_info):
self.inner_dir = os.getcwd()
os.chdir(self.outer_dir)
def preserve_dir(fn):
async def wrapped(*args, **kwds):
return await CoroWrapper(fn(*args, **kwds), PreserveDir())
return wrapped
This setup passes not only your original test, but a more involved test that spawns multiple concurrent coroutines that use the same decorator to different directories. For example:
@preserve_dir
async def ordinary1():
os.chdir("/tmp")
print('ordinary1', os.getcwd())
await asyncio.sleep(1)
print('ordinary1', os.getcwd())
@preserve_dir
async def ordinary2():
os.chdir("/")
print('ordinary2', os.getcwd())
await asyncio.sleep(0.5)
print('ordinary2', os.getcwd())
await asyncio.sleep(0.5)
print('ordinary2', os.getcwd())
async def main():
await asyncio.gather(ordinary1(), ordinary2())
print(os.getcwd())
asyncio.run(main())
print(os.getcwd())
output:
/home/user4815162342
ordinary1 /tmp
ordinary2 /
ordinary2 /
ordinary1 /tmp
ordinary2 /
/home/user4815162342
A caveat with this approach is that the directory preservation is tied to the current task. So if you delegate execution to a sub-coroutine, it will observe the modified directory if it's just awaited, but not if it's awaited using await asyncio.gather(coro1(), coro2())
.