# type: ignore from functools import wraps, partial import os import types import pathlib import trio from trio._util import async_wraps, Final # re-wrap return value from methods that return new instances of pathlib.Path def rewrap_path(value): if isinstance(value, pathlib.Path): value = Path(value) return value def _forward_factory(cls, attr_name, attr): @wraps(attr) def wrapper(self, *args, **kwargs): attr = getattr(self._wrapped, attr_name) value = attr(*args, **kwargs) return rewrap_path(value) return wrapper def _forward_magic(cls, attr): sentinel = object() @wraps(attr) def wrapper(self, other=sentinel): if other is sentinel: return attr(self._wrapped) if isinstance(other, cls): other = other._wrapped value = attr(self._wrapped, other) return rewrap_path(value) return wrapper def iter_wrapper_factory(cls, meth_name): @async_wraps(cls, cls._wraps, meth_name) async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) # Make sure that the full iteration is performed in the thread # by converting the generator produced by pathlib into a list items = await trio.to_thread.run_sync(lambda: list(func())) return (rewrap_path(item) for item in items) return wrapper def thread_wrapper_factory(cls, meth_name): @async_wraps(cls, cls._wraps, meth_name) async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) value = await trio.to_thread.run_sync(func) return rewrap_path(value) return wrapper def classmethod_wrapper_factory(cls, meth_name): @classmethod @async_wraps(cls, cls._wraps, meth_name) async def wrapper(cls, *args, **kwargs): meth = getattr(cls._wraps, meth_name) func = partial(meth, *args, **kwargs) value = await trio.to_thread.run_sync(func) return rewrap_path(value) return wrapper class AsyncAutoWrapperType(Final): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) cls._forward = [] type(cls).generate_forwards(cls, attrs) type(cls).generate_wraps(cls, attrs) type(cls).generate_magic(cls, attrs) type(cls).generate_iter(cls, attrs) def generate_forwards(cls, attrs): # forward functions of _forwards for attr_name, attr in cls._forwards.__dict__.items(): if attr_name.startswith("_") or attr_name in attrs: continue if isinstance(attr, property): cls._forward.append(attr_name) elif isinstance(attr, types.FunctionType): wrapper = _forward_factory(cls, attr_name, attr) setattr(cls, attr_name, wrapper) else: raise TypeError(attr_name, type(attr)) def generate_wraps(cls, attrs): # generate wrappers for functions of _wraps for attr_name, attr in cls._wraps.__dict__.items(): # .z. exclude cls._wrap_iter if attr_name.startswith("_") or attr_name in attrs: continue if isinstance(attr, classmethod): wrapper = classmethod_wrapper_factory(cls, attr_name) setattr(cls, attr_name, wrapper) elif isinstance(attr, types.FunctionType): wrapper = thread_wrapper_factory(cls, attr_name) setattr(cls, attr_name, wrapper) else: raise TypeError(attr_name, type(attr)) def generate_magic(cls, attrs): # generate wrappers for magic for attr_name in cls._forward_magic: attr = getattr(cls._forwards, attr_name) wrapper = _forward_magic(cls, attr) setattr(cls, attr_name, wrapper) def generate_iter(cls, attrs): # generate wrappers for methods that return iterators for attr_name, attr in cls._wraps.__dict__.items(): if attr_name in cls._wrap_iter: wrapper = iter_wrapper_factory(cls, attr_name) setattr(cls, attr_name, wrapper) class Path(metaclass=AsyncAutoWrapperType): """A :class:`pathlib.Path` wrapper that executes blocking methods in :meth:`trio.to_thread.run_sync`. """ _wraps = pathlib.Path _forwards = pathlib.PurePath _forward_magic = [ "__str__", "__bytes__", "__truediv__", "__rtruediv__", "__eq__", "__lt__", "__le__", "__gt__", "__ge__", "__hash__", ] _wrap_iter = ["glob", "rglob", "iterdir"] def __init__(self, *args): self._wrapped = pathlib.Path(*args) def __getattr__(self, name): if name in self._forward: value = getattr(self._wrapped, name) return rewrap_path(value) raise AttributeError(name) def __dir__(self): return super().__dir__() + self._forward def __repr__(self): return "trio.Path({})".format(repr(str(self))) def __fspath__(self): return os.fspath(self._wrapped) @wraps(pathlib.Path.open) async def open(self, *args, **kwargs): """Open the file pointed to by the path, like the :func:`trio.open_file` function does. """ func = partial(self._wrapped.open, *args, **kwargs) value = await trio.to_thread.run_sync(func) return trio.wrap_file(value) Path.iterdir.__doc__ = """ Like :meth:`pathlib.Path.iterdir`, but async. This is an async method that returns a synchronous iterator, so you use it like:: for subpath in await mypath.iterdir(): ... Note that it actually loads the whole directory list into memory immediately, during the initial call. (See `issue #501 `__ for discussion.) """ # The value of Path.absolute.__doc__ makes a reference to # :meth:~pathlib.Path.absolute, which does not exist. Removing this makes more # sense than inventing our own special docstring for this. del Path.absolute.__doc__ os.PathLike.register(Path)