From 9cbc366415e7030225405b662f1103d33216e5f5 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 02:04:20 -0500 Subject: [PATCH 01/57] trio.io: initial implementation --- trio/io/__init__.py | 1 + trio/io/io.py | 34 ++++++++++++++++++++ trio/io/types.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 trio/io/__init__.py create mode 100644 trio/io/io.py create mode 100644 trio/io/types.py diff --git a/trio/io/__init__.py b/trio/io/__init__.py new file mode 100644 index 0000000000..caad9572a8 --- /dev/null +++ b/trio/io/__init__.py @@ -0,0 +1 @@ +from .io import * diff --git a/trio/io/io.py b/trio/io/io.py new file mode 100644 index 0000000000..92b3645b3c --- /dev/null +++ b/trio/io/io.py @@ -0,0 +1,34 @@ +from functools import singledispatch +import io + +import trio +from trio.io import types + + +__all__ = ['open', 'wrap'] + + +async def open(file, mode='r', buffering=-1, encoding=None, errors=None, + newline=None, closefd=True, opener=None): + _file = await trio.run_in_worker_thread(io.open, file) + return wrap(_file) + + +@singledispatch +def wrap(file): + raise TypeError(file) + + +@wrap.register(io._io._TextIOBase) +def _(file): + return types.AsyncTextIOBase(file) + + +@wrap.register(io._io._BufferedIOBase) +def _(file): + return types.AsyncBufferedIOBase(file) + + +@wrap.register(io._io._RawIOBase) +def _(file): + return types.AsyncRawIOBase(file) diff --git a/trio/io/types.py b/trio/io/types.py new file mode 100644 index 0000000000..c0aaeb5aef --- /dev/null +++ b/trio/io/types.py @@ -0,0 +1,75 @@ +from functools import partial + +import trio + + +def _method_factory(cls, meth_name): + async def wrapper(self, *args, **kwargs): + meth = getattr(self._file, meth_name) + func = partial(meth, *args, **kwargs) + return await trio.run_in_worker_thread(func) + + wrapper.__name__ = meth_name + wrapper.__qualname__ = '.'.join((__name__, + cls.__name__, + meth_name)) + return wrapper + + +class AsyncIOType(type): + def __init__(cls, name, bases, attrs): + super().__init__(name, bases, attrs) + + for meth_name in cls._wrap: + wrapper = _method_factory(cls, meth_name) + setattr(cls, meth_name, wrapper) + + +class AsyncIOBase(metaclass=AsyncIOType): + _forward = ['readable', 'writable', 'seekable', 'isatty', + 'closed', 'fileno'] + + _wrap = ['close', 'flush', 'readline', 'readlines', 'tell', + 'writelines', 'seek', 'truncate'] + + def __init__(self, file): + self._file = file + + def __getattr__(self, name): + if name in self._forward: + return getattr(self._file, name) + raise AttributeError(name) + + def __dir__(self): + return super().__dir__() + list(self._forward) + + async def __aiter__(self): + return self + + async def __anext__(self): + line = await self.readline() + if line: + return line + else: + return StopAsyncIteration + + async def __aenter__(self): + return self + + async def __aexit__(self, typ, value, traceback): + return await self.close() + + +class AsyncRawIOBase(AsyncIOBase): + _wrap = ['read', 'readall', 'readinto', 'write'] + + +class AsyncBufferedIOBase(AsyncIOBase): + _wrap = ['readinto', 'detach', 'read', 'read1', 'write'] + + +class AsyncTextIOBase(AsyncIOBase): + _forward = AsyncIOBase._forward + \ + ['encoding', 'errors', 'newlines'] + + _wrap = ['detach', 'read', 'readline', 'write'] From b401562634b1e4cb373a83480231db048270f2eb Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 12:58:12 -0500 Subject: [PATCH 02/57] trio.io: wrap open in AsyncGeneratorContextManager This replaces the open implementation with an asynchronous generator, and wraps it in an asynchronous context manager. --- trio/io/io.py | 32 +++++++++++++++++++++++++++++--- trio/io/types.py | 6 ------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/trio/io/io.py b/trio/io/io.py index 92b3645b3c..ef867ec6e4 100644 --- a/trio/io/io.py +++ b/trio/io/io.py @@ -1,4 +1,4 @@ -from functools import singledispatch +from functools import singledispatch, wraps import io import trio @@ -8,10 +8,36 @@ __all__ = ['open', 'wrap'] +class AsyncGeneratorContextManager: + def __init__(self, gen): + self.gen = gen + + async def __aenter__(self): + return await self.gen.__anext__() + + async def __aexit__(self, typ, value, traceback): + try: + await self.gen.__anext__() + except StopAsyncIteration: + return + raise RuntimeError("generator didn't stop") + + +def async_generator_context(func): + @wraps(func) + def wrapper(*args, **kwargs): + return AsyncGeneratorContextManager(func(*args, **kwargs)) + return wrapper + + +@async_generator_context async def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): - _file = await trio.run_in_worker_thread(io.open, file) - return wrap(_file) + try: + file = wrap(await trio.run_in_worker_thread(io.open, file)) + yield file + finally: + await file.close() @singledispatch diff --git a/trio/io/types.py b/trio/io/types.py index c0aaeb5aef..26b779b038 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -53,12 +53,6 @@ async def __anext__(self): else: return StopAsyncIteration - async def __aenter__(self): - return self - - async def __aexit__(self, typ, value, traceback): - return await self.close() - class AsyncRawIOBase(AsyncIOBase): _wrap = ['read', 'readall', 'readinto', 'write'] From f20874fe085c2f5ecb98576392e295e2b79cd261 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 13:05:33 -0500 Subject: [PATCH 03/57] trio.io: wrap heap classes Despite not directly inheriting them, implementations are at least registered subclasses of the heap versions of *IOBase classes. --- trio/io/io.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trio/io/io.py b/trio/io/io.py index ef867ec6e4..e9be47718e 100644 --- a/trio/io/io.py +++ b/trio/io/io.py @@ -45,16 +45,16 @@ def wrap(file): raise TypeError(file) -@wrap.register(io._io._TextIOBase) +@wrap.register(io.TextIOBase) def _(file): return types.AsyncTextIOBase(file) -@wrap.register(io._io._BufferedIOBase) +@wrap.register(io.BufferedIOBase) def _(file): return types.AsyncBufferedIOBase(file) -@wrap.register(io._io._RawIOBase) +@wrap.register(io.RawIOBase) def _(file): return types.AsyncRawIOBase(file) From f7f7bcc8f81418dcb1e8f7a59ffd7e2fac3efe06 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 15:50:24 -0500 Subject: [PATCH 04/57] io: replace async_generator_context with closing The async generator context was broken, and fixing it seemed un-necessarily complicated. --- trio/io/io.py | 35 +++++++++++++++++------------------ trio/io/types.py | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/trio/io/io.py b/trio/io/io.py index e9be47718e..b8647dc2eb 100644 --- a/trio/io/io.py +++ b/trio/io/io.py @@ -1,4 +1,4 @@ -from functools import singledispatch, wraps +from functools import singledispatch, wraps, partial import io import trio @@ -8,36 +8,35 @@ __all__ = ['open', 'wrap'] -class AsyncGeneratorContextManager: - def __init__(self, gen): - self.gen = gen +class ClosingContextManager: + def __init__(self, coro): + self._coro = coro + self._wrapper = None async def __aenter__(self): - return await self.gen.__anext__() + self._wrapper = await self._coro + return self._wrapper async def __aexit__(self, typ, value, traceback): - try: - await self.gen.__anext__() - except StopAsyncIteration: - return - raise RuntimeError("generator didn't stop") + await self._wrapper.close() + def __await__(self): + return self._coro.__await__() -def async_generator_context(func): + +def closing(func): @wraps(func) def wrapper(*args, **kwargs): - return AsyncGeneratorContextManager(func(*args, **kwargs)) + return ClosingContextManager(func(*args, **kwargs)) return wrapper -@async_generator_context +@closing async def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): - try: - file = wrap(await trio.run_in_worker_thread(io.open, file)) - yield file - finally: - await file.close() + _file = wrap(await trio.run_in_worker_thread(io.open, file, mode, + buffering, encoding, errors, newline, closefd, opener)) + return _file @singledispatch diff --git a/trio/io/types.py b/trio/io/types.py index 26b779b038..a28f4d008a 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -51,7 +51,7 @@ async def __anext__(self): if line: return line else: - return StopAsyncIteration + raise StopAsyncIteration class AsyncRawIOBase(AsyncIOBase): From b0668dd7e05bd80f8dd47b2588d95392e68b6432 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 15:52:29 -0500 Subject: [PATCH 05/57] tests: add io tests --- trio/tests/test_io.py | 96 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 trio/tests/test_io.py diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py new file mode 100644 index 0000000000..570f918c5e --- /dev/null +++ b/trio/tests/test_io.py @@ -0,0 +1,96 @@ +import io as _io +import tempfile + +import pytest +from unittest import mock +from unittest.mock import patch, sentinel + +from trio import io +from trio.io import types + + +concrete_cls = [ + _io.StringIO, # io.TextIOBase + _io.BytesIO, # io.BufferedIOBase + _io.FileIO # io.RawIOBase +] + +wrapper_cls = [ + types.AsyncTextIOBase, + types.AsyncBufferedIOBase, + types.AsyncRawIOBase +] + + +@pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) +def test_wrap(cls, wrap_cls): + wrapped = io.wrap(cls.__new__(cls)) + + assert isinstance(wrapped, wrap_cls) + + +def test_wrap_invalid(): + with pytest.raises(TypeError): + io.wrap(str()) + + +@pytest.mark.parametrize("wrap_cls", wrapper_cls) +def test_types_forward(wrap_cls): + inst = wrap_cls(sentinel) + + for attr_name in wrap_cls._forward: + assert getattr(inst, attr_name) == getattr(sentinel, attr_name) + + +def test_types_forward_invalid(): + inst = types.AsyncIOBase(None) + + with pytest.raises(AttributeError): + inst.nonexistant_attr + + +def test_types_forward_in_dir(): + inst = types.AsyncIOBase(None) + + assert all(attr in dir(inst) for attr in inst._forward) + + +@pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) +async def test_types_wrap(cls, wrap_cls): + mock_cls = mock.Mock(spec_set=cls) + inst = wrap_cls(mock_cls) + + for meth_name in wrap_cls._wrap + types.AsyncIOBase._wrap: + meth = getattr(inst, meth_name) + mock_meth = getattr(mock_cls, meth_name) + + value = await meth(sentinel.argument, kw=sentinel.kw) + + mock_meth.assert_called_once_with(sentinel.argument, kw=sentinel.kw) + assert value == mock_meth() + mock_cls.reset_mock() + + +async def test_open_context_manager(tmpdir): + async with io.open(tmpdir.join('test'), 'w') as f: + assert isinstance(f, types.AsyncIOBase) + assert not f.closed + + assert f.closed + + +async def test_open_await(tmpdir): + f = await io.open(tmpdir.join('test'), 'w') + + assert isinstance(f, types.AsyncIOBase) + assert not f.closed + + +async def test_async_iter(): + string = 'test\nstring\nend' + + inst = io.wrap(_io.StringIO(string)) + + expected = iter(string.splitlines(True)) + async for actual in inst: + assert actual == next(expected) From ab54e727898717647dfef0838bdefb15c7f51fb1 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 16:09:42 -0500 Subject: [PATCH 06/57] test_io: fix python3.5 compatibility ancient versions of python don't have proper path support. --- trio/tests/test_io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index 570f918c5e..d22e7467b0 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -72,7 +72,8 @@ async def test_types_wrap(cls, wrap_cls): async def test_open_context_manager(tmpdir): - async with io.open(tmpdir.join('test'), 'w') as f: + path = tmpdir.join('test').__fspath__() + async with io.open(path, 'w') as f: assert isinstance(f, types.AsyncIOBase) assert not f.closed @@ -80,7 +81,8 @@ async def test_open_context_manager(tmpdir): async def test_open_await(tmpdir): - f = await io.open(tmpdir.join('test'), 'w') + path = tmpdir.join('test').__fspath__() + f = await io.open(path, 'w') assert isinstance(f, types.AsyncIOBase) assert not f.closed From 4d3614f34568b4181397f795a541c836b7724a88 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 16:13:30 -0500 Subject: [PATCH 07/57] io: fix warnings --- trio/io/types.py | 2 +- trio/tests/test_io.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/trio/io/types.py b/trio/io/types.py index a28f4d008a..d6aec3a978 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -43,7 +43,7 @@ def __getattr__(self, name): def __dir__(self): return super().__dir__() + list(self._forward) - async def __aiter__(self): + def __aiter__(self): return self async def __anext__(self): diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index d22e7467b0..5bf78087c3 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -87,6 +87,8 @@ async def test_open_await(tmpdir): assert isinstance(f, types.AsyncIOBase) assert not f.closed + f.close() + async def test_async_iter(): string = 'test\nstring\nend' From 6dc85bbe8107331e3658d21fc0cf10fdbafbe9d9 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 29 May 2017 18:04:13 -0500 Subject: [PATCH 08/57] trio.io: docs wip --- docs/source/glossary.rst | 19 +++++++++++++++++ docs/source/index.rst | 1 + docs/source/reference-io.rst | 24 +++++++++++++++++++--- trio/io/__init__.py | 1 + trio/io/io.py | 40 +++++++++++++++++++++++++++++++++++- trio/io/types.py | 3 +++ 6 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 docs/source/glossary.rst diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst new file mode 100644 index 0000000000..29d0b864e8 --- /dev/null +++ b/docs/source/glossary.rst @@ -0,0 +1,19 @@ +:orphan: + +.. _glossary: + +******** +Glossary +******** + +.. glossary:: + + asynchronous file object + This is an object with an API identical to a :term:`file object`, + with the exception that all nontrivial methods are wrapped in coroutine + functions. + + Like file objects, there are also three categories of asynchronous file + objects, corresponding to each file object type. Their interfaces are + defined in the :mod:`trio.io.types` module. The main way to create an + asynchronous file object is by using the :func:`trio.io.open` function. diff --git a/docs/source/index.rst b/docs/source/index.rst index 7bff4d2a15..3ea681fbdb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -83,3 +83,4 @@ Vital statistics: * :ref:`genindex` * :ref:`modindex` * :ref:`search` +* :ref:`glossary` diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index c7c3a4df75..9386d0a785 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -374,12 +374,30 @@ Socket objects * :meth:`~socket.socket.set_inheritable` * :meth:`~socket.socket.get_inheritable` +Asynchronous disk I/O +--------------------- -Async disk I/O --------------- +.. currentmodule:: trio.io +.. module:: trio.io -`Not implemented yet! `__ +The :mod:`trio.io` module provides wrappers around :class:`~io.IOBase` +subclasses. Methods that could block are executed in +:meth:`trio.run_in_worker_thread`. +.. autofunction:: open + +.. autofunction:: wrap + +Asynchronous file objects +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: trio.io + +.. autoclass:: AsyncRawIOBase + +.. autoclass:: AsyncBufferedIOBase + +.. autoclass:: AsyncTextIOBase Subprocesses ------------ diff --git a/trio/io/__init__.py b/trio/io/__init__.py index caad9572a8..9613a1746b 100644 --- a/trio/io/__init__.py +++ b/trio/io/__init__.py @@ -1 +1,2 @@ from .io import * +from .types import * diff --git a/trio/io/io.py b/trio/io/io.py index b8647dc2eb..5d18fd9ca3 100644 --- a/trio/io/io.py +++ b/trio/io/io.py @@ -1,4 +1,4 @@ -from functools import singledispatch, wraps, partial +from functools import singledispatch, wraps import io import trio @@ -34,6 +34,20 @@ def wrapper(*args, **kwargs): @closing async def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): + """Asynchronous version of :func:`~io.open`. + + Returns: + An :term:`asynchronous file object` wrapped in an :term:`asynchronous context manager`. + + Example:: + + async with trio.io.open(filename) as f: + async for line in f: + pass + + assert f.closed + + """ _file = wrap(await trio.run_in_worker_thread(io.open, file, mode, buffering, encoding, errors, newline, closefd, opener)) return _file @@ -41,6 +55,30 @@ async def open(file, mode='r', buffering=-1, encoding=None, errors=None, @singledispatch def wrap(file): + """This wraps any file-like object in an equivalent asynchronous file-like + object. + + Args: + file: a :term:`file object` + + Returns: + An :term:`asynchronous file object` + + Example:: + + f = StringIO('asdf') + async_f = wrap(f) + + assert await async_f.read() == 'asdf' + + It is also possible to extend :func:`wrap` to support new types:: + + @wrap.register(pyfakefs.fake_filesystem.FakeFileWrapper): + def _(file): + return trio.io.AsyncRawIOBase(file) + + """ + raise TypeError(file) diff --git a/trio/io/types.py b/trio/io/types.py index d6aec3a978..05fa0efd43 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -3,6 +3,9 @@ import trio +__all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase'] + + def _method_factory(cls, meth_name): async def wrapper(self, *args, **kwargs): meth = getattr(self._file, meth_name) From 7b704a221361e845b5679b5c58216b146a61ada1 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 14:00:41 -0500 Subject: [PATCH 09/57] io: re-add AsyncIOBase context manager This adds support for the pattern: f = await io.open(path) async with f: pass --- trio/io/types.py | 6 ++++++ trio/tests/test_io.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/trio/io/types.py b/trio/io/types.py index 05fa0efd43..d33395c79e 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -56,6 +56,12 @@ async def __anext__(self): else: raise StopAsyncIteration + async def __aenter__(self): + return self + + async def __aexit__(self, typ, value, traceback): + await self.close() + class AsyncRawIOBase(AsyncIOBase): _wrap = ['read', 'readall', 'readinto', 'write'] diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index 5bf78087c3..31278df6ff 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -87,7 +87,16 @@ async def test_open_await(tmpdir): assert isinstance(f, types.AsyncIOBase) assert not f.closed - f.close() + await f.close() + + +async def test_open_await_context_manager(tmpdir): + path = tmpdir.join('test').__fspath__() + f = await io.open(path, 'w') + async with f: + assert not f.closed + + assert f.closed async def test_async_iter(): From e5b69c8cb76e8060567435fe99dcf36bdc1eb3ba Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 14:05:35 -0500 Subject: [PATCH 10/57] io: add aiter compatibility wrapper This adds pre-3.5.2 compatibility. --- trio/io/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/trio/io/types.py b/trio/io/types.py index d33395c79e..66f282b2dd 100644 --- a/trio/io/types.py +++ b/trio/io/types.py @@ -1,6 +1,7 @@ from functools import partial import trio +from trio._util import aiter_compat __all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase'] @@ -46,6 +47,7 @@ def __getattr__(self, name): def __dir__(self): return super().__dir__() + list(self._forward) + @aiter_compat def __aiter__(self): return self From 77fde94640f7f483f022e3e9575b59fbebb0b894 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 14:22:21 -0500 Subject: [PATCH 11/57] io: rename public API This renames io.open to io.open_file and io.wrap to io.wrap_file. This also replaces singledispatch with if logic, and allows wrap_file to return instances of AsyncIOBase. --- docs/source/reference-io.rst | 4 +-- trio/io/io.py | 49 +++++++++++++----------------------- trio/tests/test_io.py | 18 +++++++------ 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 9386d0a785..13043eb47b 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -384,9 +384,9 @@ The :mod:`trio.io` module provides wrappers around :class:`~io.IOBase` subclasses. Methods that could block are executed in :meth:`trio.run_in_worker_thread`. -.. autofunction:: open +.. autofunction:: open_file -.. autofunction:: wrap +.. autofunction:: wrap_file Asynchronous file objects ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/trio/io/io.py b/trio/io/io.py index 5d18fd9ca3..145d7728ee 100644 --- a/trio/io/io.py +++ b/trio/io/io.py @@ -1,11 +1,11 @@ -from functools import singledispatch, wraps +from functools import wraps import io import trio from trio.io import types -__all__ = ['open', 'wrap'] +__all__ = ['open_file', 'wrap_file'] class ClosingContextManager: @@ -32,8 +32,8 @@ def wrapper(*args, **kwargs): @closing -async def open(file, mode='r', buffering=-1, encoding=None, errors=None, - newline=None, closefd=True, opener=None): +async def open_file(file, mode='r', buffering=-1, encoding=None, errors=None, + newline=None, closefd=True, opener=None): """Asynchronous version of :func:`~io.open`. Returns: @@ -41,20 +41,19 @@ async def open(file, mode='r', buffering=-1, encoding=None, errors=None, Example:: - async with trio.io.open(filename) as f: + async with trio.io.open_file(filename) as f: async for line in f: pass assert f.closed """ - _file = wrap(await trio.run_in_worker_thread(io.open, file, mode, - buffering, encoding, errors, newline, closefd, opener)) + _file = wrap_file(await trio.run_in_worker_thread(io.open, file, mode, + buffering, encoding, errors, newline, closefd, opener)) return _file -@singledispatch -def wrap(file): +def wrap_file(file): """This wraps any file-like object in an equivalent asynchronous file-like object. @@ -67,31 +66,19 @@ def wrap(file): Example:: f = StringIO('asdf') - async_f = wrap(f) + async_f = wrap_file(f) assert await async_f.read() == 'asdf' - It is also possible to extend :func:`wrap` to support new types:: - - @wrap.register(pyfakefs.fake_filesystem.FakeFileWrapper): - def _(file): - return trio.io.AsyncRawIOBase(file) - """ - raise TypeError(file) - - -@wrap.register(io.TextIOBase) -def _(file): - return types.AsyncTextIOBase(file) + if isinstance(file, io.TextIOBase): + return types.AsyncTextIOBase(file) + if isinstance(file, io.BufferedIOBase): + return types.AsyncBufferedIOBase(file) + if isinstance(file, io.RawIOBase): + return types.AsyncRawIOBase(file) + if isinstance(file, io.IOBase): + return types.AsyncIOBase(file) - -@wrap.register(io.BufferedIOBase) -def _(file): - return types.AsyncBufferedIOBase(file) - - -@wrap.register(io.RawIOBase) -def _(file): - return types.AsyncRawIOBase(file) + raise TypeError(file) diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index 31278df6ff..9361e6c96f 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -12,26 +12,28 @@ concrete_cls = [ _io.StringIO, # io.TextIOBase _io.BytesIO, # io.BufferedIOBase - _io.FileIO # io.RawIOBase + _io.FileIO, # io.RawIOBase + _io.IOBase # ] wrapper_cls = [ types.AsyncTextIOBase, types.AsyncBufferedIOBase, - types.AsyncRawIOBase + types.AsyncRawIOBase, + types.AsyncIOBase ] @pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) def test_wrap(cls, wrap_cls): - wrapped = io.wrap(cls.__new__(cls)) + wrapped = io.wrap_file(cls.__new__(cls)) assert isinstance(wrapped, wrap_cls) def test_wrap_invalid(): with pytest.raises(TypeError): - io.wrap(str()) + io.wrap_file(str()) @pytest.mark.parametrize("wrap_cls", wrapper_cls) @@ -73,7 +75,7 @@ async def test_types_wrap(cls, wrap_cls): async def test_open_context_manager(tmpdir): path = tmpdir.join('test').__fspath__() - async with io.open(path, 'w') as f: + async with io.open_file(path, 'w') as f: assert isinstance(f, types.AsyncIOBase) assert not f.closed @@ -82,7 +84,7 @@ async def test_open_context_manager(tmpdir): async def test_open_await(tmpdir): path = tmpdir.join('test').__fspath__() - f = await io.open(path, 'w') + f = await io.open_file(path, 'w') assert isinstance(f, types.AsyncIOBase) assert not f.closed @@ -92,7 +94,7 @@ async def test_open_await(tmpdir): async def test_open_await_context_manager(tmpdir): path = tmpdir.join('test').__fspath__() - f = await io.open(path, 'w') + f = await io.open_file(path, 'w') async with f: assert not f.closed @@ -102,7 +104,7 @@ async def test_open_await_context_manager(tmpdir): async def test_async_iter(): string = 'test\nstring\nend' - inst = io.wrap(_io.StringIO(string)) + inst = io.wrap_file(_io.StringIO(string)) expected = iter(string.splitlines(True)) async for actual in inst: From 482ca8fb8d202617fe6b40d2e793c6eed8db597d Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 15:58:27 -0500 Subject: [PATCH 12/57] trio: rename trio.io to trio._file_io This clarifies that the package is specifically implementing file io, and is a private package. As a result, the public APIs are now imported from the top-level trio package. --- docs/source/glossary.rst | 4 +-- docs/source/reference-io.rst | 14 +++++----- trio/__init__.py | 3 +++ trio/_file_io/__init__.py | 7 +++++ trio/{io/io.py => _file_io/_file_io.py} | 10 +++---- trio/{io/types.py => _file_io/_types.py} | 3 ++- trio/io/__init__.py | 2 -- trio/tests/test_io.py | 33 ++++++++++++------------ 8 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 trio/_file_io/__init__.py rename trio/{io/io.py => _file_io/_file_io.py} (89%) rename trio/{io/types.py => _file_io/_types.py} (98%) delete mode 100644 trio/io/__init__.py diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 29d0b864e8..bc0de295a9 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -15,5 +15,5 @@ Glossary Like file objects, there are also three categories of asynchronous file objects, corresponding to each file object type. Their interfaces are - defined in the :mod:`trio.io.types` module. The main way to create an - asynchronous file object is by using the :func:`trio.io.open` function. + defined in :ref:`asynchronous-file-objects`. The main way to create an + asynchronous file object is by using the :func:`trio.open_file` function. diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 13043eb47b..102268b058 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -377,21 +377,23 @@ Socket objects Asynchronous disk I/O --------------------- -.. currentmodule:: trio.io -.. module:: trio.io +.. currentmodule:: trio -The :mod:`trio.io` module provides wrappers around :class:`~io.IOBase` -subclasses. Methods that could block are executed in -:meth:`trio.run_in_worker_thread`. +trio provides wrappers around :class:`~io.IOBase` subclasses. Methods that could +block are executed in :meth:`trio.run_in_worker_thread`. .. autofunction:: open_file .. autofunction:: wrap_file +.. _asynchronous-file-objects: + Asynchronous file objects ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. currentmodule:: trio.io +.. currentmodule:: trio + +.. autoclass:: AsyncIOBase .. autoclass:: AsyncRawIOBase diff --git a/trio/__init__.py b/trio/__init__.py index 040b2fdf10..c464ddeb8b 100644 --- a/trio/__init__.py +++ b/trio/__init__.py @@ -50,6 +50,9 @@ from ._network import * __all__ += _network.__all__ +from ._file_io import * +__all__ += _file_io.__all__ + # Imported by default from . import socket from . import abc diff --git a/trio/_file_io/__init__.py b/trio/_file_io/__init__.py new file mode 100644 index 0000000000..7c5f457566 --- /dev/null +++ b/trio/_file_io/__init__.py @@ -0,0 +1,7 @@ +__all__ = [] + +from ._file_io import * +__all__ += _file_io.__all__ + +from ._types import * +__all__ += _types.__all__ diff --git a/trio/io/io.py b/trio/_file_io/_file_io.py similarity index 89% rename from trio/io/io.py rename to trio/_file_io/_file_io.py index 145d7728ee..8ef7d2ee35 100644 --- a/trio/io/io.py +++ b/trio/_file_io/_file_io.py @@ -2,7 +2,7 @@ import io import trio -from trio.io import types +from trio._file_io import _types __all__ = ['open_file', 'wrap_file'] @@ -73,12 +73,12 @@ def wrap_file(file): """ if isinstance(file, io.TextIOBase): - return types.AsyncTextIOBase(file) + return _types.AsyncTextIOBase(file) if isinstance(file, io.BufferedIOBase): - return types.AsyncBufferedIOBase(file) + return _types.AsyncBufferedIOBase(file) if isinstance(file, io.RawIOBase): - return types.AsyncRawIOBase(file) + return _types.AsyncRawIOBase(file) if isinstance(file, io.IOBase): - return types.AsyncIOBase(file) + return _types.AsyncIOBase(file) raise TypeError(file) diff --git a/trio/io/types.py b/trio/_file_io/_types.py similarity index 98% rename from trio/io/types.py rename to trio/_file_io/_types.py index 66f282b2dd..37a767d2e6 100644 --- a/trio/io/types.py +++ b/trio/_file_io/_types.py @@ -4,7 +4,8 @@ from trio._util import aiter_compat -__all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase'] +__all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase', + 'AsyncIOBase'] def _method_factory(cls, meth_name): diff --git a/trio/io/__init__.py b/trio/io/__init__.py deleted file mode 100644 index 9613a1746b..0000000000 --- a/trio/io/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .io import * -from .types import * diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index 9361e6c96f..7ae9861351 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -5,8 +5,7 @@ from unittest import mock from unittest.mock import patch, sentinel -from trio import io -from trio.io import types +import trio concrete_cls = [ @@ -17,23 +16,23 @@ ] wrapper_cls = [ - types.AsyncTextIOBase, - types.AsyncBufferedIOBase, - types.AsyncRawIOBase, - types.AsyncIOBase + trio.AsyncTextIOBase, + trio.AsyncBufferedIOBase, + trio.AsyncRawIOBase, + trio.AsyncIOBase ] @pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) def test_wrap(cls, wrap_cls): - wrapped = io.wrap_file(cls.__new__(cls)) + wrapped = trio.wrap_file(cls.__new__(cls)) assert isinstance(wrapped, wrap_cls) def test_wrap_invalid(): with pytest.raises(TypeError): - io.wrap_file(str()) + trio.wrap_file(str()) @pytest.mark.parametrize("wrap_cls", wrapper_cls) @@ -45,14 +44,14 @@ def test_types_forward(wrap_cls): def test_types_forward_invalid(): - inst = types.AsyncIOBase(None) + inst = trio.AsyncIOBase(None) with pytest.raises(AttributeError): inst.nonexistant_attr def test_types_forward_in_dir(): - inst = types.AsyncIOBase(None) + inst = trio.AsyncIOBase(None) assert all(attr in dir(inst) for attr in inst._forward) @@ -62,7 +61,7 @@ async def test_types_wrap(cls, wrap_cls): mock_cls = mock.Mock(spec_set=cls) inst = wrap_cls(mock_cls) - for meth_name in wrap_cls._wrap + types.AsyncIOBase._wrap: + for meth_name in wrap_cls._wrap + trio.AsyncIOBase._wrap: meth = getattr(inst, meth_name) mock_meth = getattr(mock_cls, meth_name) @@ -75,8 +74,8 @@ async def test_types_wrap(cls, wrap_cls): async def test_open_context_manager(tmpdir): path = tmpdir.join('test').__fspath__() - async with io.open_file(path, 'w') as f: - assert isinstance(f, types.AsyncIOBase) + async with trio.open_file(path, 'w') as f: + assert isinstance(f, trio.AsyncIOBase) assert not f.closed assert f.closed @@ -84,9 +83,9 @@ async def test_open_context_manager(tmpdir): async def test_open_await(tmpdir): path = tmpdir.join('test').__fspath__() - f = await io.open_file(path, 'w') + f = await trio.open_file(path, 'w') - assert isinstance(f, types.AsyncIOBase) + assert isinstance(f, trio.AsyncIOBase) assert not f.closed await f.close() @@ -94,7 +93,7 @@ async def test_open_await(tmpdir): async def test_open_await_context_manager(tmpdir): path = tmpdir.join('test').__fspath__() - f = await io.open_file(path, 'w') + f = await trio.open_file(path, 'w') async with f: assert not f.closed @@ -104,7 +103,7 @@ async def test_open_await_context_manager(tmpdir): async def test_async_iter(): string = 'test\nstring\nend' - inst = io.wrap_file(_io.StringIO(string)) + inst = trio.wrap_file(_io.StringIO(string)) expected = iter(string.splitlines(True)) async for actual in inst: From 1dad9eea8cf1430d61c74e462cbc14ca66a95f6b Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 16:26:40 -0500 Subject: [PATCH 13/57] _file_io: gaurantee the underlying file is closed during cancellation --- trio/_file_io/_types.py | 10 +++++++++- trio/tests/test_io.py | 25 +++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/trio/_file_io/_types.py b/trio/_file_io/_types.py index 37a767d2e6..3e78f17c03 100644 --- a/trio/_file_io/_types.py +++ b/trio/_file_io/_types.py @@ -1,6 +1,7 @@ from functools import partial import trio +from trio import _core from trio._util import aiter_compat @@ -34,7 +35,7 @@ class AsyncIOBase(metaclass=AsyncIOType): _forward = ['readable', 'writable', 'seekable', 'isatty', 'closed', 'fileno'] - _wrap = ['close', 'flush', 'readline', 'readlines', 'tell', + _wrap = ['flush', 'readline', 'readlines', 'tell', 'writelines', 'seek', 'truncate'] def __init__(self, file): @@ -65,6 +66,13 @@ async def __aenter__(self): async def __aexit__(self, typ, value, traceback): await self.close() + async def close(self): + # ensure the underling file is closed during cancellation + with _core.open_cancel_scope(shield=True): + await trio.run_in_worker_thread(self._file.close) + + await _core.yield_if_cancelled() + class AsyncRawIOBase(AsyncIOBase): _wrap = ['read', 'readall', 'readinto', 'write'] diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index 7ae9861351..f44ee6117e 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -6,6 +6,7 @@ from unittest.mock import patch, sentinel import trio +from trio import _core concrete_cls = [ @@ -23,6 +24,11 @@ ] +@pytest.fixture +def path(tmpdir): + return tmpdir.join('test').__fspath__() + + @pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) def test_wrap(cls, wrap_cls): wrapped = trio.wrap_file(cls.__new__(cls)) @@ -72,8 +78,7 @@ async def test_types_wrap(cls, wrap_cls): mock_cls.reset_mock() -async def test_open_context_manager(tmpdir): - path = tmpdir.join('test').__fspath__() +async def test_open_context_manager(path): async with trio.open_file(path, 'w') as f: assert isinstance(f, trio.AsyncIOBase) assert not f.closed @@ -81,8 +86,7 @@ async def test_open_context_manager(tmpdir): assert f.closed -async def test_open_await(tmpdir): - path = tmpdir.join('test').__fspath__() +async def test_open_await(path): f = await trio.open_file(path, 'w') assert isinstance(f, trio.AsyncIOBase) @@ -91,8 +95,7 @@ async def test_open_await(tmpdir): await f.close() -async def test_open_await_context_manager(tmpdir): - path = tmpdir.join('test').__fspath__() +async def test_open_await_context_manager(path): f = await trio.open_file(path, 'w') async with f: assert not f.closed @@ -108,3 +111,13 @@ async def test_async_iter(): expected = iter(string.splitlines(True)) async for actual in inst: assert actual == next(expected) + + +async def test_close_cancelled(path): + with _core.open_cancel_scope() as cscope: + async with trio.open_file(path, 'w') as f: + cscope.cancel() + with pytest.raises(_core.Cancelled): + await f.write('a') + + assert f.closed From bc5c08548a17653393b09582a654fca6786fb09f Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 20:55:08 -0500 Subject: [PATCH 14/57] _file_io: move _method_factory to _helpers module This makes more sense if it is shared between more modules in _file_io. _method_factory is also renamed to thread_wrapper_factory, which makes it more clear what it does. --- trio/_file_io/_helpers.py | 26 ++++++++++++++++++++++++++ trio/_file_io/_types.py | 26 +++++++------------------- 2 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 trio/_file_io/_helpers.py diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py new file mode 100644 index 0000000000..df6e4c3ab1 --- /dev/null +++ b/trio/_file_io/_helpers.py @@ -0,0 +1,26 @@ +import trio + +from functools import wraps, partial + + +def copy_metadata(func): + @wraps(func) + def wrapper(cls, attr_name): + wrapped = func(cls, attr_name) + + wrapped.__name__ = attr_name + wrapped.__qualname__ = '.'.join((__name__, + cls.__name__, + attr_name)) + return wrapped + return wrapper + + +@copy_metadata +def thread_wrapper_factory(cls, meth_name): + async def wrapper(self, *args, **kwargs): + meth = getattr(self._wrapped, meth_name) + func = partial(meth, *args, **kwargs) + return await trio.run_in_worker_thread(func) + + return wrapper diff --git a/trio/_file_io/_types.py b/trio/_file_io/_types.py index 3e78f17c03..f16de74518 100644 --- a/trio/_file_io/_types.py +++ b/trio/_file_io/_types.py @@ -3,35 +3,23 @@ import trio from trio import _core from trio._util import aiter_compat +from trio._file_io._helpers import thread_wrapper_factory __all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase', 'AsyncIOBase'] -def _method_factory(cls, meth_name): - async def wrapper(self, *args, **kwargs): - meth = getattr(self._file, meth_name) - func = partial(meth, *args, **kwargs) - return await trio.run_in_worker_thread(func) - - wrapper.__name__ = meth_name - wrapper.__qualname__ = '.'.join((__name__, - cls.__name__, - meth_name)) - return wrapper - - -class AsyncIOType(type): +class AsyncWrapperType(type): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) for meth_name in cls._wrap: - wrapper = _method_factory(cls, meth_name) + wrapper = thread_wrapper_factory(cls, meth_name) setattr(cls, meth_name, wrapper) -class AsyncIOBase(metaclass=AsyncIOType): +class AsyncIOBase(metaclass=AsyncWrapperType): _forward = ['readable', 'writable', 'seekable', 'isatty', 'closed', 'fileno'] @@ -39,11 +27,11 @@ class AsyncIOBase(metaclass=AsyncIOType): 'writelines', 'seek', 'truncate'] def __init__(self, file): - self._file = file + self._wrapped = file def __getattr__(self, name): if name in self._forward: - return getattr(self._file, name) + return getattr(self._wrapped, name) raise AttributeError(name) def __dir__(self): @@ -69,7 +57,7 @@ async def __aexit__(self, typ, value, traceback): async def close(self): # ensure the underling file is closed during cancellation with _core.open_cancel_scope(shield=True): - await trio.run_in_worker_thread(self._file.close) + await trio.run_in_worker_thread(self._wrapped.close) await _core.yield_if_cancelled() From d828f39e46cdc991be3275956c8d37326aad694b Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 23:05:47 -0500 Subject: [PATCH 15/57] trio: initial path implementation --- trio/_file_io/__init__.py | 3 ++ trio/_file_io/_helpers.py | 8 +++ trio/_file_io/_path.py | 110 ++++++++++++++++++++++++++++++++++++++ trio/tests/test_path.py | 22 ++++++++ 4 files changed, 143 insertions(+) create mode 100644 trio/_file_io/_path.py create mode 100644 trio/tests/test_path.py diff --git a/trio/_file_io/__init__.py b/trio/_file_io/__init__.py index 7c5f457566..66f9e2865f 100644 --- a/trio/_file_io/__init__.py +++ b/trio/_file_io/__init__.py @@ -5,3 +5,6 @@ from ._types import * __all__ += _types.__all__ + +from ._path import * +__all__ += _path.__all__ diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index df6e4c3ab1..a92add7c9e 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -24,3 +24,11 @@ async def wrapper(self, *args, **kwargs): return await trio.run_in_worker_thread(func) return wrapper + + +def getattr_factory(cls, forward): + def __getattr__(self, name): + if name in forward: + return getattr(self._wrapped, name) + raise AttributeError(name) + return __getattr__ diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py new file mode 100644 index 0000000000..be4c7b8b34 --- /dev/null +++ b/trio/_file_io/_path.py @@ -0,0 +1,110 @@ +from functools import wraps, update_wrapper +import os +import types +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath, PosixPath, WindowsPath + +from trio._file_io._helpers import thread_wrapper_factory, getattr_factory, copy_metadata + + +__all__ = ['AsyncPath', 'AsyncPosixPath', 'AsyncWindowsPath'] + + +def _forward_factory(cls, attr_name, attr): + @wraps(attr) + def wrapper(self, *args, **kwargs): + attr = getattr(self._wrapped, attr_name) + value = attr(*args, **kwargs) + if isinstance(value, cls._forwards): + # re-wrap methods that return new paths + value = cls._from_path(value) + return value + + return wrapper + + +def _wrapper_factory(cls, attr_name): + wrapped = thread_wrapper_factory(cls, attr_name) + + @wraps(wrapped) + async def wrapper(self, *args, **kwargs): + value = await wrapped(self, *args, **kwargs) + if isinstance(value, cls._wraps): + value = cls._from_path(value) + return value + + return wrapper + + +class AsyncAutoWrapperType(type): + def __init__(cls, name, bases, attrs): + super().__init__(name, bases, attrs) + # only initialize the superclass + if bases: + return + + forward = [] + # 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): + 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)) + + setattr(cls, '__getattr__', getattr_factory(cls, forward)) + + # generate wrappers for functions of _wraps + for attr_name, attr in cls._wraps.__dict__.items(): + if attr_name.startswith('_') or attr_name in attrs: + continue + + if isinstance(attr, classmethod): + setattr(cls, attr_name, attr) + elif isinstance(attr, types.FunctionType): + wrapper = _wrapper_factory(cls, attr_name) + setattr(cls, attr_name, wrapper) + else: + raise TypeError(attr_name, type(attr)) + + +class AsyncPath(metaclass=AsyncAutoWrapperType): + _wraps = Path + _forwards = PurePath + + def __new__(cls, *args, **kwargs): + path = Path(*args, **kwargs) + + self = cls._from_path(path) + return self + + @classmethod + def _from_path(cls, path): + if isinstance(path, PosixPath): + cls = AsyncPosixPath + elif isinstance(path, WindowsPath): + cls = AsyncWindowsPath + else: + raise TypeError(type(path)) + + self = object.__new__(cls) + self._wrapped = path + return self + + def __fspath__(self): + return self._wrapped.__fspath__() + + +os.PathLike.register(AsyncPath) + + +class AsyncPosixPath(AsyncPath): + __slots__ = () + + +class AsyncWindowsPath(AsyncPath): + __slots__ = () diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py new file mode 100644 index 0000000000..3d1a314465 --- /dev/null +++ b/trio/tests/test_path.py @@ -0,0 +1,22 @@ +import pathlib + +import pytest + +import trio + + +@pytest.fixture +def path(): + return trio.AsyncPath() + + +async def test_windows_owner_group_raises(path): + path._wrapped.__class__ = pathlib.WindowsPath + + assert isinstance(path._wrapped, pathlib.WindowsPath) + + with pytest.raises(NotImplementedError): + await path.owner() + + with pytest.raises(NotImplementedError): + await path.group() From df0b6cb7a9d5ddf056225429803e1e3ef66a6ff6 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 4 Jun 2017 23:20:18 -0500 Subject: [PATCH 16/57] _path: fix python3.5 compatibility python3.5 doesn't have os.PathLike --- trio/_file_io/_path.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index be4c7b8b34..0829462cc0 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -99,7 +99,9 @@ def __fspath__(self): return self._wrapped.__fspath__() -os.PathLike.register(AsyncPath) +# python3.5 compat +if hasattr(os, 'PathLike'): + os.PathLike.register(AsyncPath) class AsyncPosixPath(AsyncPath): From 39b4d0ca8167eb29af20bc3d0e744732d24463dd Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 5 Jun 2017 00:24:31 -0500 Subject: [PATCH 17/57] _path: remove WindowsPath and PosixPath wrappers These are not necessary. --- trio/_file_io/_path.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 0829462cc0..1c9c59bdbc 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -1,12 +1,12 @@ from functools import wraps, update_wrapper import os import types -from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath, PosixPath, WindowsPath +from pathlib import Path, PurePath from trio._file_io._helpers import thread_wrapper_factory, getattr_factory, copy_metadata -__all__ = ['AsyncPath', 'AsyncPosixPath', 'AsyncWindowsPath'] +__all__ = ['AsyncPath'] def _forward_factory(cls, attr_name, attr): @@ -38,9 +38,6 @@ async def wrapper(self, *args, **kwargs): class AsyncAutoWrapperType(type): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) - # only initialize the superclass - if bases: - return forward = [] # forward functions of _forwards @@ -84,13 +81,6 @@ def __new__(cls, *args, **kwargs): @classmethod def _from_path(cls, path): - if isinstance(path, PosixPath): - cls = AsyncPosixPath - elif isinstance(path, WindowsPath): - cls = AsyncWindowsPath - else: - raise TypeError(type(path)) - self = object.__new__(cls) self._wrapped = path return self @@ -102,11 +92,3 @@ def __fspath__(self): # python3.5 compat if hasattr(os, 'PathLike'): os.PathLike.register(AsyncPath) - - -class AsyncPosixPath(AsyncPath): - __slots__ = () - - -class AsyncWindowsPath(AsyncPath): - __slots__ = () From efb739c329760a93c5f60bad624a16ccf2e78b16 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 5 Jun 2017 01:37:50 -0500 Subject: [PATCH 18/57] _file_io: promote wrapper_factory cls re-wrapping to _helpers Move the cls re-wrapping logic from _path to _helpers, so it can be consumed by _file_io. This allows methods that return new instances of the wrapped object/type, like BufferedIOBase.detach, to be re-wrapped. Also added the corresponding tests for BufferedIOBase. --- trio/_file_io/_helpers.py | 5 ++++- trio/_file_io/_path.py | 23 +++++------------------ trio/_file_io/_types.py | 8 +++++++- trio/tests/test_io.py | 12 ++++++++++++ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index a92add7c9e..e097ad5061 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -21,7 +21,10 @@ def thread_wrapper_factory(cls, meth_name): async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) - return await trio.run_in_worker_thread(func) + value = await trio.run_in_worker_thread(func) + if isinstance(value, cls._wraps): + value = cls._from_wrapped(value) + return value return wrapper diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 1c9c59bdbc..fa26ce6d12 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -16,20 +16,7 @@ def wrapper(self, *args, **kwargs): value = attr(*args, **kwargs) if isinstance(value, cls._forwards): # re-wrap methods that return new paths - value = cls._from_path(value) - return value - - return wrapper - - -def _wrapper_factory(cls, attr_name): - wrapped = thread_wrapper_factory(cls, attr_name) - - @wraps(wrapped) - async def wrapper(self, *args, **kwargs): - value = await wrapped(self, *args, **kwargs) - if isinstance(value, cls._wraps): - value = cls._from_path(value) + value = cls._from_wrapped(value) return value return wrapper @@ -63,7 +50,7 @@ def __init__(cls, name, bases, attrs): if isinstance(attr, classmethod): setattr(cls, attr_name, attr) elif isinstance(attr, types.FunctionType): - wrapper = _wrapper_factory(cls, attr_name) + wrapper = thread_wrapper_factory(cls, attr_name) setattr(cls, attr_name, wrapper) else: raise TypeError(attr_name, type(attr)) @@ -76,13 +63,13 @@ class AsyncPath(metaclass=AsyncAutoWrapperType): def __new__(cls, *args, **kwargs): path = Path(*args, **kwargs) - self = cls._from_path(path) + self = cls._from_wrapped(path) return self @classmethod - def _from_path(cls, path): + def _from_wrapped(cls, wrapped): self = object.__new__(cls) - self._wrapped = path + self._wrapped = wrapped return self def __fspath__(self): diff --git a/trio/_file_io/_types.py b/trio/_file_io/_types.py index f16de74518..cd81911dfc 100644 --- a/trio/_file_io/_types.py +++ b/trio/_file_io/_types.py @@ -1,4 +1,4 @@ -from functools import partial +import io import trio from trio import _core @@ -26,9 +26,15 @@ class AsyncIOBase(metaclass=AsyncWrapperType): _wrap = ['flush', 'readline', 'readlines', 'tell', 'writelines', 'seek', 'truncate'] + _wraps = io.IOBase + def __init__(self, file): self._wrapped = file + @classmethod + def _from_wrapped(cls, wrapped): + return cls(wrapped) + def __getattr__(self, name): if name in self._forward: return getattr(self._wrapped, name) diff --git a/trio/tests/test_io.py b/trio/tests/test_io.py index f44ee6117e..5e9f77b7b4 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_io.py @@ -121,3 +121,15 @@ async def test_close_cancelled(path): await f.write('a') assert f.closed + + +async def test_detach_rewraps_asynciobase(): + raw = _io.BytesIO() + buffered = _io.BufferedReader(raw) + + inst = trio.AsyncBufferedIOBase(buffered) + + detached = await inst.detach() + + assert isinstance(detached, trio.AsyncIOBase) + assert detached._wrapped == raw From 86543fb0e9c3ee470a82b571ae582211e2ca28c0 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 00:06:49 -0500 Subject: [PATCH 19/57] _path: special-case open method AsyncPath.open should look identical to open_file, so similarly wrap both the return value with wrap_file, and wrap the method in ClosingContextManager. --- trio/_file_io/_path.py | 10 +++++++++- trio/tests/test_path.py | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index fa26ce6d12..036a4d753a 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -1,8 +1,10 @@ -from functools import wraps, update_wrapper +from functools import wraps, partial import os import types from pathlib import Path, PurePath +import trio +from trio._file_io._file_io import closing from trio._file_io._helpers import thread_wrapper_factory, getattr_factory, copy_metadata @@ -75,6 +77,12 @@ def _from_wrapped(cls, wrapped): def __fspath__(self): return self._wrapped.__fspath__() + @closing + async def open(self, *args, **kwargs): + func = partial(self._wrapped.open, *args, **kwargs) + value = await trio.run_in_worker_thread(func) + return trio.wrap_file(value) + # python3.5 compat if hasattr(os, 'PathLike'): diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 3d1a314465..7fb9bef568 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -6,8 +6,9 @@ @pytest.fixture -def path(): - return trio.AsyncPath() +def path(tmpdir): + p = tmpdir.join('test').__fspath__() + return trio.AsyncPath(p) async def test_windows_owner_group_raises(path): @@ -20,3 +21,17 @@ async def test_windows_owner_group_raises(path): with pytest.raises(NotImplementedError): await path.group() + + +async def test_open_is_async_context_manager(path): + async with path.open('w') as f: + assert isinstance(f, trio.AsyncIO) + + assert f.closed + + +async def test_open_is_awaitable_context_manager(path): + async with await path.open('w') as f: + assert isinstance(f, trio.AsyncIO) + + assert f.closed From 95a1966b27e26d6ec880f2129a91d3c22c64e716 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 00:19:36 -0500 Subject: [PATCH 20/57] _path: implement __dir__ The getattr_factory thing was cute, but it prevented __dir__ from being implementable. This removes getattr_factory, and moves the list of forwarded properties to a class-accessible _forward attribute. --- trio/_file_io/_helpers.py | 8 -------- trio/_file_io/_path.py | 16 +++++++++++----- trio/tests/test_path.py | 7 +++++++ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index e097ad5061..9fbc4b673d 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -27,11 +27,3 @@ async def wrapper(self, *args, **kwargs): return value return wrapper - - -def getattr_factory(cls, forward): - def __getattr__(self, name): - if name in forward: - return getattr(self._wrapped, name) - raise AttributeError(name) - return __getattr__ diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 036a4d753a..569d7291ef 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -5,7 +5,7 @@ import trio from trio._file_io._file_io import closing -from trio._file_io._helpers import thread_wrapper_factory, getattr_factory, copy_metadata +from trio._file_io._helpers import thread_wrapper_factory __all__ = ['AsyncPath'] @@ -28,22 +28,20 @@ class AsyncAutoWrapperType(type): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) - forward = [] + cls._forward = [] # 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): - forward.append(attr_name) + 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)) - setattr(cls, '__getattr__', getattr_factory(cls, forward)) - # generate wrappers for functions of _wraps for attr_name, attr in cls._wraps.__dict__.items(): if attr_name.startswith('_') or attr_name in attrs: @@ -68,6 +66,14 @@ def __new__(cls, *args, **kwargs): self = cls._from_wrapped(path) return self + def __getattr__(self, name): + if name in self._forward: + return getattr(self._wrapped, name) + raise AttributeError(name) + + def __dir__(self): + return super().__dir__() + self._forward + @classmethod def _from_wrapped(cls, wrapped): self = object.__new__(cls) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 7fb9bef568..f3b420bef8 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -35,3 +35,10 @@ async def test_open_is_awaitable_context_manager(path): assert isinstance(f, trio.AsyncIO) assert f.closed + + +async def test_forwarded_properties(path): + # use `name` as a representative of forwarded properties + + assert 'name' in dir(path) + assert path.name == 'test' From 7f88ca03982a61a7c99359e4cc82483c4d47ce74 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 12:48:08 -0500 Subject: [PATCH 21/57] _file_io: despecialize AsyncIO; remove wrapper type This moves wrapper generation from class creation to getattr. --- trio/_file_io/__init__.py | 3 - trio/_file_io/_file_io.py | 102 +++++++++++++++------ trio/_file_io/_helpers.py | 23 +++++ trio/_file_io/_types.py | 83 ----------------- trio/tests/{test_io.py => test_file_io.py} | 73 ++++++++------- 5 files changed, 133 insertions(+), 151 deletions(-) delete mode 100644 trio/_file_io/_types.py rename trio/tests/{test_io.py => test_file_io.py} (60%) diff --git a/trio/_file_io/__init__.py b/trio/_file_io/__init__.py index 66f9e2865f..1241242eae 100644 --- a/trio/_file_io/__init__.py +++ b/trio/_file_io/__init__.py @@ -3,8 +3,5 @@ from ._file_io import * __all__ += _file_io.__all__ -from ._types import * -__all__ += _types.__all__ - from ._path import * __all__ += _path.__all__ diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index 8ef7d2ee35..6a3370dea1 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -1,34 +1,86 @@ -from functools import wraps +from functools import partial import io import trio -from trio._file_io import _types - - -__all__ = ['open_file', 'wrap_file'] - - -class ClosingContextManager: - def __init__(self, coro): - self._coro = coro - self._wrapper = None +from trio import _core +from trio._util import aiter_compat +from trio._file_io._helpers import closing + + +__all__ = ['open_file', 'wrap_file', 'AsyncIO'] + +_FILE_SYNC_ATTRS = [ + 'closed', + 'encoding', 'errors', 'fileno', 'isatty', 'newlines', + 'readable', 'seekable', 'writable', +] + +_FILE_ASYNC_METHODS = [ + 'detach', 'flush', + 'read', 'read1', 'readall', 'readinto', 'readline', 'readlines', + 'seek', 'tell', 'truncate', + 'write', 'writelines', +] + + +class AsyncIO: + def __init__(self, file): + self._wrapped = file + + self._available_sync_attrs = [a for a in _FILE_SYNC_ATTRS if hasattr(self._wrapped, a)] + self._available_async_methods = [a for a in _FILE_ASYNC_METHODS if hasattr(self._wrapped, a)] + + @property + def wrapped(self): + return self._wrapped + + def __getattr__(self, name): + if name in self._available_sync_attrs: + return getattr(self._wrapped, name) + if name in self._available_async_methods: + meth = getattr(self._wrapped, name) + async def async_wrapper(*args, **kwargs): + return await trio.run_in_worker_thread(partial(meth, *args, **kwargs)) + async_wrapper.__name__ = name + async_wrapper.__qualname__ = self.__class__.__qualname__ + "." + name + # cache the generated method + setattr(self, name, async_wrapper) + return async_wrapper + + raise AttributeError(name) + + def __dir__(self): + return set(super().__dir__() + + self._available_sync_attrs + + self._available_async_methods) + + @aiter_compat + def __aiter__(self): + return self + + async def __anext__(self): + line = await self.readline() + if line: + return line + else: + raise StopAsyncIteration async def __aenter__(self): - self._wrapper = await self._coro - return self._wrapper + return self async def __aexit__(self, typ, value, traceback): - await self._wrapper.close() + await self.close() - def __await__(self): - return self._coro.__await__() + async def detach(self): + raw = await trio.run_in_worker_thread(self._wrapped.detach) + return wrap_file(raw) + async def close(self): + # ensure the underling file is closed during cancellation + with _core.open_cancel_scope(shield=True): + await trio.run_in_worker_thread(self._wrapped.close) -def closing(func): - @wraps(func) - def wrapper(*args, **kwargs): - return ClosingContextManager(func(*args, **kwargs)) - return wrapper + await _core.yield_if_cancelled() @closing @@ -72,13 +124,7 @@ def wrap_file(file): """ - if isinstance(file, io.TextIOBase): - return _types.AsyncTextIOBase(file) - if isinstance(file, io.BufferedIOBase): - return _types.AsyncBufferedIOBase(file) - if isinstance(file, io.RawIOBase): - return _types.AsyncRawIOBase(file) if isinstance(file, io.IOBase): - return _types.AsyncIOBase(file) + return AsyncIO(file) raise TypeError(file) diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index 9fbc4b673d..69f1fc5612 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -27,3 +27,26 @@ async def wrapper(self, *args, **kwargs): return value return wrapper + + +class ClosingContextManager: + def __init__(self, coro): + self._coro = coro + self._wrapper = None + + async def __aenter__(self): + self._wrapper = await self._coro + return self._wrapper + + async def __aexit__(self, typ, value, traceback): + await self._wrapper.close() + + def __await__(self): + return self._coro.__await__() + + +def closing(func): + @wraps(func) + def wrapper(*args, **kwargs): + return ClosingContextManager(func(*args, **kwargs)) + return wrapper diff --git a/trio/_file_io/_types.py b/trio/_file_io/_types.py deleted file mode 100644 index cd81911dfc..0000000000 --- a/trio/_file_io/_types.py +++ /dev/null @@ -1,83 +0,0 @@ -import io - -import trio -from trio import _core -from trio._util import aiter_compat -from trio._file_io._helpers import thread_wrapper_factory - - -__all__ = ['AsyncRawIOBase', 'AsyncBufferedIOBase', 'AsyncTextIOBase', - 'AsyncIOBase'] - - -class AsyncWrapperType(type): - def __init__(cls, name, bases, attrs): - super().__init__(name, bases, attrs) - - for meth_name in cls._wrap: - wrapper = thread_wrapper_factory(cls, meth_name) - setattr(cls, meth_name, wrapper) - - -class AsyncIOBase(metaclass=AsyncWrapperType): - _forward = ['readable', 'writable', 'seekable', 'isatty', - 'closed', 'fileno'] - - _wrap = ['flush', 'readline', 'readlines', 'tell', - 'writelines', 'seek', 'truncate'] - - _wraps = io.IOBase - - def __init__(self, file): - self._wrapped = file - - @classmethod - def _from_wrapped(cls, wrapped): - return cls(wrapped) - - def __getattr__(self, name): - if name in self._forward: - return getattr(self._wrapped, name) - raise AttributeError(name) - - def __dir__(self): - return super().__dir__() + list(self._forward) - - @aiter_compat - def __aiter__(self): - return self - - async def __anext__(self): - line = await self.readline() - if line: - return line - else: - raise StopAsyncIteration - - async def __aenter__(self): - return self - - async def __aexit__(self, typ, value, traceback): - await self.close() - - async def close(self): - # ensure the underling file is closed during cancellation - with _core.open_cancel_scope(shield=True): - await trio.run_in_worker_thread(self._wrapped.close) - - await _core.yield_if_cancelled() - - -class AsyncRawIOBase(AsyncIOBase): - _wrap = ['read', 'readall', 'readinto', 'write'] - - -class AsyncBufferedIOBase(AsyncIOBase): - _wrap = ['readinto', 'detach', 'read', 'read1', 'write'] - - -class AsyncTextIOBase(AsyncIOBase): - _forward = AsyncIOBase._forward + \ - ['encoding', 'errors', 'newlines'] - - _wrap = ['detach', 'read', 'readline', 'write'] diff --git a/trio/tests/test_io.py b/trio/tests/test_file_io.py similarity index 60% rename from trio/tests/test_io.py rename to trio/tests/test_file_io.py index 5e9f77b7b4..8ca447505a 100644 --- a/trio/tests/test_io.py +++ b/trio/tests/test_file_io.py @@ -1,4 +1,4 @@ -import io as _io +import io import tempfile import pytest @@ -10,17 +10,10 @@ concrete_cls = [ - _io.StringIO, # io.TextIOBase - _io.BytesIO, # io.BufferedIOBase - _io.FileIO, # io.RawIOBase - _io.IOBase # -] - -wrapper_cls = [ - trio.AsyncTextIOBase, - trio.AsyncBufferedIOBase, - trio.AsyncRawIOBase, - trio.AsyncIOBase + io.StringIO, + io.BytesIO, + io.FileIO, + io.IOBase ] @@ -29,45 +22,37 @@ def path(tmpdir): return tmpdir.join('test').__fspath__() -@pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) -def test_wrap(cls, wrap_cls): - wrapped = trio.wrap_file(cls.__new__(cls)) - - assert isinstance(wrapped, wrap_cls) - - def test_wrap_invalid(): with pytest.raises(TypeError): trio.wrap_file(str()) -@pytest.mark.parametrize("wrap_cls", wrapper_cls) -def test_types_forward(wrap_cls): - inst = wrap_cls(sentinel) +def test_types_forward(): + inst = trio.AsyncIO(sentinel) - for attr_name in wrap_cls._forward: + for attr_name in inst._available_sync_attrs: assert getattr(inst, attr_name) == getattr(sentinel, attr_name) def test_types_forward_invalid(): - inst = trio.AsyncIOBase(None) + inst = trio.AsyncIO(None) with pytest.raises(AttributeError): inst.nonexistant_attr def test_types_forward_in_dir(): - inst = trio.AsyncIOBase(None) + inst = trio.AsyncIO(io.StringIO()) - assert all(attr in dir(inst) for attr in inst._forward) + assert all(attr in dir(inst) for attr in inst._available_sync_attrs) -@pytest.mark.parametrize("cls,wrap_cls", zip(concrete_cls, wrapper_cls)) -async def test_types_wrap(cls, wrap_cls): +@pytest.mark.parametrize("cls", zip(concrete_cls)) +async def test_types_wrap(cls): mock_cls = mock.Mock(spec_set=cls) - inst = wrap_cls(mock_cls) + inst = trio.AsyncIO(mock_cls) - for meth_name in wrap_cls._wrap + trio.AsyncIOBase._wrap: + for meth_name in inst._available_async_methods: meth = getattr(inst, meth_name) mock_meth = getattr(mock_cls, meth_name) @@ -80,7 +65,7 @@ async def test_types_wrap(cls, wrap_cls): async def test_open_context_manager(path): async with trio.open_file(path, 'w') as f: - assert isinstance(f, trio.AsyncIOBase) + assert isinstance(f, trio.AsyncIO) assert not f.closed assert f.closed @@ -89,7 +74,7 @@ async def test_open_context_manager(path): async def test_open_await(path): f = await trio.open_file(path, 'w') - assert isinstance(f, trio.AsyncIOBase) + assert isinstance(f, trio.AsyncIO) assert not f.closed await f.close() @@ -106,7 +91,7 @@ async def test_open_await_context_manager(path): async def test_async_iter(): string = 'test\nstring\nend' - inst = trio.wrap_file(_io.StringIO(string)) + inst = trio.wrap_file(io.StringIO(string)) expected = iter(string.splitlines(True)) async for actual in inst: @@ -124,12 +109,26 @@ async def test_close_cancelled(path): async def test_detach_rewraps_asynciobase(): - raw = _io.BytesIO() - buffered = _io.BufferedReader(raw) + raw = io.BytesIO() + buffered = io.BufferedReader(raw) - inst = trio.AsyncBufferedIOBase(buffered) + inst = trio.AsyncIO(buffered) detached = await inst.detach() - assert isinstance(detached, trio.AsyncIOBase) + assert isinstance(detached, trio.AsyncIO) assert detached._wrapped == raw + + +async def test_async_method_generated_once(): + inst = trio.wrap_file(io.StringIO()) + + for meth_name in inst._available_async_methods: + assert getattr(inst, meth_name) == getattr(inst, meth_name) + + +async def test_wrapped_property(): + wrapped = io.StringIO() + inst = trio.wrap_file(wrapped) + + assert inst.wrapped == wrapped From 13d0735ab6478ff61db192bd95d2565139309301 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 13:24:13 -0500 Subject: [PATCH 22/57] _file_io/_helpers: consolidate __name__ magic in async_wraps This also adds signature tests for AsyncIO and AsyncPath. --- trio/_file_io/_file_io.py | 16 +++++++++------- trio/_file_io/_helpers.py | 31 +++++++------------------------ trio/_file_io/_path.py | 15 ++++++++++++++- trio/tests/test_file_io.py | 8 ++++++++ trio/tests/test_path.py | 7 +++++++ 5 files changed, 45 insertions(+), 32 deletions(-) diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index 6a3370dea1..1e16f6edfd 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -4,7 +4,7 @@ import trio from trio import _core from trio._util import aiter_compat -from trio._file_io._helpers import closing +from trio._file_io._helpers import closing, async_wraps __all__ = ['open_file', 'wrap_file', 'AsyncIO'] @@ -39,13 +39,15 @@ def __getattr__(self, name): return getattr(self._wrapped, name) if name in self._available_async_methods: meth = getattr(self._wrapped, name) - async def async_wrapper(*args, **kwargs): - return await trio.run_in_worker_thread(partial(meth, *args, **kwargs)) - async_wrapper.__name__ = name - async_wrapper.__qualname__ = self.__class__.__qualname__ + "." + name + + @async_wraps(self.__class__, name) + async def wrapper(*args, **kwargs): + func = partial(meth, *args, **kwargs) + return await trio.run_in_worker_thread(func) + # cache the generated method - setattr(self, name, async_wrapper) - return async_wrapper + setattr(self, name, wrapper) + return wrapper raise AttributeError(name) diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index 69f1fc5612..ee68d70458 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -3,30 +3,13 @@ from functools import wraps, partial -def copy_metadata(func): - @wraps(func) - def wrapper(cls, attr_name): - wrapped = func(cls, attr_name) - - wrapped.__name__ = attr_name - wrapped.__qualname__ = '.'.join((__name__, - cls.__name__, - attr_name)) - return wrapped - return wrapper - - -@copy_metadata -def thread_wrapper_factory(cls, meth_name): - async def wrapper(self, *args, **kwargs): - meth = getattr(self._wrapped, meth_name) - func = partial(meth, *args, **kwargs) - value = await trio.run_in_worker_thread(func) - if isinstance(value, cls._wraps): - value = cls._from_wrapped(value) - return value - - return wrapper +def async_wraps(cls, attr_name): + def decorator(func): + func.__name__ = attr_name + func.__qualname__ = '.'.join((cls.__qualname__, + attr_name)) + return func + return decorator class ClosingContextManager: diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 569d7291ef..5a7a40d7b2 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -4,8 +4,8 @@ from pathlib import Path, PurePath import trio +from trio._file_io._helpers import async_wraps from trio._file_io._file_io import closing -from trio._file_io._helpers import thread_wrapper_factory __all__ = ['AsyncPath'] @@ -24,6 +24,19 @@ def wrapper(self, *args, **kwargs): return wrapper +def thread_wrapper_factory(cls, meth_name): + @async_wraps(cls, meth_name) + async def wrapper(self, *args, **kwargs): + meth = getattr(self._wrapped, meth_name) + func = partial(meth, *args, **kwargs) + value = await trio.run_in_worker_thread(func) + if isinstance(value, cls._wraps): + value = cls._from_wrapped(value) + return value + + return wrapper + + class AsyncAutoWrapperType(type): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 8ca447505a..230adbf802 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -132,3 +132,11 @@ async def test_wrapped_property(): inst = trio.wrap_file(wrapped) assert inst.wrapped == wrapped + + +async def test_async_method_signature(): + inst = trio.wrap_file(io.StringIO()) + + # use read as a representative of all async methods + assert inst.read.__name__ == 'read' + assert inst.read.__qualname__ == 'AsyncIO.read' diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index f3b420bef8..b5c4b33289 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -42,3 +42,10 @@ async def test_forwarded_properties(path): assert 'name' in dir(path) assert path.name == 'test' + + +async def test_async_method_signature(path): + # use `resolve` as a representative of wrapped methods + + assert path.resolve.__name__ == 'resolve' + assert path.resolve.__qualname__ == 'AsyncPath.resolve' From 84c0238ebc006f04124ed7f5b103a013c04a5271 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 13:36:12 -0500 Subject: [PATCH 23/57] async_wraps: add __doc__ --- trio/_file_io/_file_io.py | 2 +- trio/_file_io/_helpers.py | 9 ++++++++- trio/_file_io/_path.py | 2 +- trio/tests/test_file_io.py | 2 ++ trio/tests/test_path.py | 2 ++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index 1e16f6edfd..b496fa007a 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -40,7 +40,7 @@ def __getattr__(self, name): if name in self._available_async_methods: meth = getattr(self._wrapped, name) - @async_wraps(self.__class__, name) + @async_wraps(self.__class__, self._wrapped.__class__, name) async def wrapper(*args, **kwargs): func = partial(meth, *args, **kwargs) return await trio.run_in_worker_thread(func) diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index ee68d70458..766ec189d8 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -3,11 +3,18 @@ from functools import wraps, partial -def async_wraps(cls, attr_name): +def async_wraps(cls, wrapped_cls, attr_name): def decorator(func): func.__name__ = attr_name func.__qualname__ = '.'.join((cls.__qualname__, attr_name)) + + func.__doc__ = """Like :meth:`~{}.{}.{}`, but async. + + """.format(wrapped_cls.__module__, + wrapped_cls.__qualname__, + attr_name) + return func return decorator diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 5a7a40d7b2..ad48c43031 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -25,7 +25,7 @@ def wrapper(self, *args, **kwargs): def thread_wrapper_factory(cls, meth_name): - @async_wraps(cls, meth_name) + @async_wraps(cls, Path, meth_name) async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 230adbf802..682b4771ca 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -140,3 +140,5 @@ async def test_async_method_signature(): # use read as a representative of all async methods assert inst.read.__name__ == 'read' assert inst.read.__qualname__ == 'AsyncIO.read' + + assert 'io.StringIO.read' in inst.read.__doc__ diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index b5c4b33289..fc0e9ed503 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -49,3 +49,5 @@ async def test_async_method_signature(path): assert path.resolve.__name__ == 'resolve' assert path.resolve.__qualname__ == 'AsyncPath.resolve' + + assert 'pathlib.Path.resolve' in path.resolve.__doc__ From 2a26335925f711359ec94d41f83e34bf3993bc70 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 14:18:52 -0500 Subject: [PATCH 24/57] test_file_io: clean up tests This removes type-specialized tests entirely, and moves some setup code to fixtures. --- trio/tests/test_file_io.py | 120 ++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 682b4771ca..1736f0d8bb 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -7,14 +7,7 @@ import trio from trio import _core - - -concrete_cls = [ - io.StringIO, - io.BytesIO, - io.FileIO, - io.IOBase -] +from trio._file_io._file_io import _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS @pytest.fixture @@ -22,45 +15,74 @@ def path(tmpdir): return tmpdir.join('test').__fspath__() +@pytest.fixture +def wrapped(): + return mock.Mock(spec_set=io.StringIO) + + +@pytest.fixture +def async_file(wrapped): + return trio.wrap_file(wrapped) + + def test_wrap_invalid(): with pytest.raises(TypeError): trio.wrap_file(str()) -def test_types_forward(): - inst = trio.AsyncIO(sentinel) +def test_wrapped_property(async_file, wrapped): + assert async_file.wrapped == wrapped - for attr_name in inst._available_sync_attrs: - assert getattr(inst, attr_name) == getattr(sentinel, attr_name) +def test_sync_attrs_forwarded(async_file, wrapped): + for attr_name in async_file._available_sync_attrs: + assert getattr(async_file, attr_name) == getattr(wrapped, attr_name) -def test_types_forward_invalid(): - inst = trio.AsyncIO(None) + +def test_sync_attrs_invalid_not_forwarded(async_file): + async_file._wrapped = sentinel + + assert hasattr(async_file.wrapped, 'invalid_attr') with pytest.raises(AttributeError): - inst.nonexistant_attr + getattr(async_file, 'invalid_attr') -def test_types_forward_in_dir(): +def test_sync_attrs_in_dir(): inst = trio.AsyncIO(io.StringIO()) assert all(attr in dir(inst) for attr in inst._available_sync_attrs) -@pytest.mark.parametrize("cls", zip(concrete_cls)) -async def test_types_wrap(cls): - mock_cls = mock.Mock(spec_set=cls) - inst = trio.AsyncIO(mock_cls) +def test_async_methods_generated_once(async_file): + for meth_name in async_file._available_async_methods: + assert getattr(async_file, meth_name) == getattr(async_file, meth_name) + + +def test_async_methods_signature(async_file): + # use read as a representative of all async methods + assert async_file.read.__name__ == 'read' + assert async_file.read.__qualname__ == 'AsyncIO.read' + + assert 'io.StringIO.read' in async_file.read.__doc__ + + +async def test_async_methods_wrap(async_file, wrapped): + skip = ['detach'] - for meth_name in inst._available_async_methods: - meth = getattr(inst, meth_name) - mock_meth = getattr(mock_cls, meth_name) + for meth_name in async_file._available_async_methods: + if meth_name in skip: + continue - value = await meth(sentinel.argument, kw=sentinel.kw) + meth = getattr(async_file, meth_name) + wrapped_meth = getattr(wrapped, meth_name) - mock_meth.assert_called_once_with(sentinel.argument, kw=sentinel.kw) - assert value == mock_meth() - mock_cls.reset_mock() + value = await meth(sentinel.argument, keyword=sentinel.keyword) + + wrapped_meth.assert_called_once_with(sentinel.argument, keyword=sentinel.keyword) + assert value == wrapped_meth() + + wrapped.reset_mock() async def test_open_context_manager(path): @@ -88,15 +110,17 @@ async def test_open_await_context_manager(path): assert f.closed -async def test_async_iter(): - string = 'test\nstring\nend' +async def test_async_iter(async_file): + async_file._wrapped = io.StringIO('test\nfoo\nbar') + expected = iter(list(async_file.wrapped)) + async_file.wrapped.seek(0) - inst = trio.wrap_file(io.StringIO(string)) - - expected = iter(string.splitlines(True)) - async for actual in inst: + async for actual in async_file: assert actual == next(expected) + with pytest.raises(StopIteration): + next(expected) + async def test_close_cancelled(path): with _core.open_cancel_scope() as cscope: @@ -112,33 +136,9 @@ async def test_detach_rewraps_asynciobase(): raw = io.BytesIO() buffered = io.BufferedReader(raw) - inst = trio.AsyncIO(buffered) + async_file = trio.wrap_file(buffered) - detached = await inst.detach() + detached = await async_file.detach() assert isinstance(detached, trio.AsyncIO) - assert detached._wrapped == raw - - -async def test_async_method_generated_once(): - inst = trio.wrap_file(io.StringIO()) - - for meth_name in inst._available_async_methods: - assert getattr(inst, meth_name) == getattr(inst, meth_name) - - -async def test_wrapped_property(): - wrapped = io.StringIO() - inst = trio.wrap_file(wrapped) - - assert inst.wrapped == wrapped - - -async def test_async_method_signature(): - inst = trio.wrap_file(io.StringIO()) - - # use read as a representative of all async methods - assert inst.read.__name__ == 'read' - assert inst.read.__qualname__ == 'AsyncIO.read' - - assert 'io.StringIO.read' in inst.read.__doc__ + assert detached.wrapped == raw From 066d1f267a63b8262669183346a8c4b567f74511 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 15:40:54 -0500 Subject: [PATCH 25/57] _file_io: remove closing We decided that it's better to only support one context manager convention, and that the non-magical one is better. --- trio/_file_io/_file_io.py | 3 +-- trio/_file_io/_helpers.py | 23 ----------------------- trio/_file_io/_path.py | 2 -- trio/tests/test_file_io.py | 19 +++++-------------- trio/tests/test_path.py | 7 ------- 5 files changed, 6 insertions(+), 48 deletions(-) diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index b496fa007a..d50435a881 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -4,7 +4,7 @@ import trio from trio import _core from trio._util import aiter_compat -from trio._file_io._helpers import closing, async_wraps +from trio._file_io._helpers import async_wraps __all__ = ['open_file', 'wrap_file', 'AsyncIO'] @@ -85,7 +85,6 @@ async def close(self): await _core.yield_if_cancelled() -@closing async def open_file(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): """Asynchronous version of :func:`~io.open`. diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py index 766ec189d8..30ee6a911b 100644 --- a/trio/_file_io/_helpers.py +++ b/trio/_file_io/_helpers.py @@ -17,26 +17,3 @@ def decorator(func): return func return decorator - - -class ClosingContextManager: - def __init__(self, coro): - self._coro = coro - self._wrapper = None - - async def __aenter__(self): - self._wrapper = await self._coro - return self._wrapper - - async def __aexit__(self, typ, value, traceback): - await self._wrapper.close() - - def __await__(self): - return self._coro.__await__() - - -def closing(func): - @wraps(func) - def wrapper(*args, **kwargs): - return ClosingContextManager(func(*args, **kwargs)) - return wrapper diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index ad48c43031..0f39b4ddbd 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -5,7 +5,6 @@ import trio from trio._file_io._helpers import async_wraps -from trio._file_io._file_io import closing __all__ = ['AsyncPath'] @@ -96,7 +95,6 @@ def _from_wrapped(cls, wrapped): def __fspath__(self): return self._wrapped.__fspath__() - @closing async def open(self, *args, **kwargs): func = partial(self._wrapped.open, *args, **kwargs) value = await trio.run_in_worker_thread(func) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 1736f0d8bb..6061e511e1 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -85,26 +85,17 @@ async def test_async_methods_wrap(async_file, wrapped): wrapped.reset_mock() -async def test_open_context_manager(path): - async with trio.open_file(path, 'w') as f: - assert isinstance(f, trio.AsyncIO) - assert not f.closed - - assert f.closed - - -async def test_open_await(path): +async def test_open(path): f = await trio.open_file(path, 'w') assert isinstance(f, trio.AsyncIO) - assert not f.closed await f.close() -async def test_open_await_context_manager(path): - f = await trio.open_file(path, 'w') - async with f: +async def test_open_context_manager(path): + async with await trio.open_file(path, 'w') as f: + assert isinstance(f, trio.AsyncIO) assert not f.closed assert f.closed @@ -124,7 +115,7 @@ async def test_async_iter(async_file): async def test_close_cancelled(path): with _core.open_cancel_scope() as cscope: - async with trio.open_file(path, 'w') as f: + async with await trio.open_file(path, 'w') as f: cscope.cancel() with pytest.raises(_core.Cancelled): await f.write('a') diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index fc0e9ed503..85084b63f7 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -24,13 +24,6 @@ async def test_windows_owner_group_raises(path): async def test_open_is_async_context_manager(path): - async with path.open('w') as f: - assert isinstance(f, trio.AsyncIO) - - assert f.closed - - -async def test_open_is_awaitable_context_manager(path): async with await path.open('w') as f: assert isinstance(f, trio.AsyncIO) From d58585647392f40c55eb87d0f64b9155b0a42943 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 16:24:55 -0500 Subject: [PATCH 26/57] _file_io: compute available attributes in __dir__ __init__ is probably more performance sensitive than __dir__, and __dir__ is the only thing that actually needs to know the complete list of available attributes. --- trio/_file_io/_file_io.py | 15 +++++++-------- trio/tests/test_file_io.py | 25 +++++++++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index d50435a881..cfa7def0aa 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -27,17 +27,14 @@ class AsyncIO: def __init__(self, file): self._wrapped = file - self._available_sync_attrs = [a for a in _FILE_SYNC_ATTRS if hasattr(self._wrapped, a)] - self._available_async_methods = [a for a in _FILE_ASYNC_METHODS if hasattr(self._wrapped, a)] - @property def wrapped(self): return self._wrapped def __getattr__(self, name): - if name in self._available_sync_attrs: + if name in _FILE_SYNC_ATTRS: return getattr(self._wrapped, name) - if name in self._available_async_methods: + if name in _FILE_ASYNC_METHODS: meth = getattr(self._wrapped, name) @async_wraps(self.__class__, self._wrapped.__class__, name) @@ -52,9 +49,11 @@ async def wrapper(*args, **kwargs): raise AttributeError(name) def __dir__(self): - return set(super().__dir__() + - self._available_sync_attrs + - self._available_async_methods) + attrs = set(super().__dir__()) + attrs.update(a for a in _FILE_SYNC_ATTRS if hasattr(self.wrapped, a)) + attrs.update(a for a in _FILE_ASYNC_METHODS if hasattr(self.wrapped, a)) + return attrs + @aiter_compat def __aiter__(self): diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 6061e511e1..2124ca48c0 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -35,7 +35,7 @@ def test_wrapped_property(async_file, wrapped): def test_sync_attrs_forwarded(async_file, wrapped): - for attr_name in async_file._available_sync_attrs: + for attr_name in _FILE_SYNC_ATTRS: assert getattr(async_file, attr_name) == getattr(wrapped, attr_name) @@ -51,11 +51,14 @@ def test_sync_attrs_invalid_not_forwarded(async_file): def test_sync_attrs_in_dir(): inst = trio.AsyncIO(io.StringIO()) - assert all(attr in dir(inst) for attr in inst._available_sync_attrs) + assert all(attr in dir(inst) for attr in _FILE_SYNC_ATTRS) def test_async_methods_generated_once(async_file): - for meth_name in async_file._available_async_methods: + for meth_name in _FILE_ASYNC_METHODS: + if meth_name not in dir(async_file): + continue + assert getattr(async_file, meth_name) == getattr(async_file, meth_name) @@ -70,7 +73,9 @@ def test_async_methods_signature(async_file): async def test_async_methods_wrap(async_file, wrapped): skip = ['detach'] - for meth_name in async_file._available_async_methods: + for meth_name in _FILE_ASYNC_METHODS: + if meth_name not in dir(async_file): + continue if meth_name in skip: continue @@ -85,6 +90,18 @@ async def test_async_methods_wrap(async_file, wrapped): wrapped.reset_mock() +async def test_async_methods_match_wrapper(async_file, wrapped): + for meth_name in _FILE_ASYNC_METHODS: + if meth_name in dir(async_file): + continue + + with pytest.raises(AttributeError): + getattr(async_file, meth_name) + + with pytest.raises(AttributeError): + getattr(wrapped, meth_name) + + async def test_open(path): f = await trio.open_file(path, 'w') From f91c7170679124f9c60d5c4beca76a3df2d8b7e0 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 17:00:24 -0500 Subject: [PATCH 27/57] _file_io: add methods and attributes not defined in *IOBase This add and categorizes all of the extra methods and attributes from non-*IOBase file object classes defined in io. Some tests that were either unclear or not robust enough as a result of this change were modified. --- trio/_file_io/_file_io.py | 7 +++++- trio/tests/test_file_io.py | 51 +++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index cfa7def0aa..fe862d6b9b 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -13,13 +13,18 @@ 'closed', 'encoding', 'errors', 'fileno', 'isatty', 'newlines', 'readable', 'seekable', 'writable', + # not defined in *IOBase: + 'buffer', 'raw', 'line_buffering', 'closefd', 'name', 'mode', + 'getvalue', 'getbuffer', ] _FILE_ASYNC_METHODS = [ - 'detach', 'flush', + 'flush', 'read', 'read1', 'readall', 'readinto', 'readline', 'readlines', 'seek', 'tell', 'truncate', 'write', 'writelines', + # not defined in *IOBase: + 'readinto1', 'peek', ] diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 2124ca48c0..297c5553ad 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -34,24 +34,42 @@ def test_wrapped_property(async_file, wrapped): assert async_file.wrapped == wrapped -def test_sync_attrs_forwarded(async_file, wrapped): - for attr_name in _FILE_SYNC_ATTRS: - assert getattr(async_file, attr_name) == getattr(wrapped, attr_name) +def test_dir_matches_wrapped(async_file, wrapped): + attrs = _FILE_SYNC_ATTRS + _FILE_ASYNC_METHODS + # all supported attrs in wrapped should be available in async_file + assert all(attr in dir(async_file) for attr in attrs if attr in dir(wrapped)) + # all supported attrs not in wrapped should not be available in async_file + assert not any(attr in dir(async_file) for attr in attrs if attr not in dir(wrapped)) -def test_sync_attrs_invalid_not_forwarded(async_file): + +def test_unsupported_not_forwarded(async_file): async_file._wrapped = sentinel - assert hasattr(async_file.wrapped, 'invalid_attr') + assert hasattr(async_file.wrapped, 'unsupported_attr') with pytest.raises(AttributeError): - getattr(async_file, 'invalid_attr') + getattr(async_file, 'unsupported_attr') + + +def test_sync_attrs_forwarded(async_file, wrapped): + for attr_name in _FILE_SYNC_ATTRS: + if attr_name not in dir(async_file): + continue + + assert getattr(async_file, attr_name) == getattr(wrapped, attr_name) -def test_sync_attrs_in_dir(): - inst = trio.AsyncIO(io.StringIO()) +def test_sync_attrs_match_wrapper(async_file, wrapped): + for attr_name in _FILE_SYNC_ATTRS: + if attr_name in dir(async_file): + continue + + with pytest.raises(AttributeError): + getattr(async_file, attr_name) - assert all(attr in dir(inst) for attr in _FILE_SYNC_ATTRS) + with pytest.raises(AttributeError): + getattr(wrapped, attr_name) def test_async_methods_generated_once(async_file): @@ -71,13 +89,9 @@ def test_async_methods_signature(async_file): async def test_async_methods_wrap(async_file, wrapped): - skip = ['detach'] - for meth_name in _FILE_ASYNC_METHODS: if meth_name not in dir(async_file): continue - if meth_name in skip: - continue meth = getattr(async_file, meth_name) wrapped_meth = getattr(wrapped, meth_name) @@ -132,10 +146,13 @@ async def test_async_iter(async_file): async def test_close_cancelled(path): with _core.open_cancel_scope() as cscope: - async with await trio.open_file(path, 'w') as f: - cscope.cancel() - with pytest.raises(_core.Cancelled): - await f.write('a') + f = await trio.open_file(path, 'w') + cscope.cancel() + + with pytest.raises(_core.Cancelled): + await f.write('a') + + await f.close() assert f.closed From 45d8ad80630cf0a6c4eb21e0b5e477974d91e98d Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 6 Jun 2017 18:59:20 -0500 Subject: [PATCH 28/57] docs: improve file_io documentation This also adds or updates docstrings within _file_io. --- docs/source/glossary.rst | 12 +++++------ docs/source/reference-io.rst | 12 ++--------- trio/_file_io/_file_io.py | 41 +++++++++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index bc0de295a9..3b9c0ee0d7 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -9,11 +9,9 @@ Glossary .. glossary:: asynchronous file object - This is an object with an API identical to a :term:`file object`, - with the exception that all nontrivial methods are wrapped in coroutine - functions. + This is an object with an API identical to a :term:`file object`, with + the exception that all nontrivial methods are coroutine functions. - Like file objects, there are also three categories of asynchronous file - objects, corresponding to each file object type. Their interfaces are - defined in :ref:`asynchronous-file-objects`. The main way to create an - asynchronous file object is by using the :func:`trio.open_file` function. + A non-normative interface is defined by :class:`trio.AsyncIO`. The main + way to create an asynchronous file object is by using the + :func:`trio.open_file` function. diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 102268b058..080e2a1aa2 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -379,9 +379,6 @@ Asynchronous disk I/O .. currentmodule:: trio -trio provides wrappers around :class:`~io.IOBase` subclasses. Methods that could -block are executed in :meth:`trio.run_in_worker_thread`. - .. autofunction:: open_file .. autofunction:: wrap_file @@ -393,13 +390,8 @@ Asynchronous file objects .. currentmodule:: trio -.. autoclass:: AsyncIOBase - -.. autoclass:: AsyncRawIOBase - -.. autoclass:: AsyncBufferedIOBase - -.. autoclass:: AsyncTextIOBase +.. autoclass:: AsyncIO + :members: Subprocesses ------------ diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index fe862d6b9b..07b0319746 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -29,11 +29,23 @@ class AsyncIO: + """:class:`trio.AsyncIO` is a generic :class:`~io.IOBase` wrapper that + implements the :term:`asynchronous file object` interface. Wrapped methods that + could block are executed in :meth:`trio.run_in_worker_thread`. + + All properties and methods defined in in :mod:`~io` are exposed by this + wrapper, if they exist in the wrapped file object. + """ + def __init__(self, file): self._wrapped = file @property def wrapped(self): + """object: A reference to the wrapped file object + + """ + return self._wrapped def __getattr__(self, name): @@ -78,10 +90,24 @@ async def __aexit__(self, typ, value, traceback): await self.close() async def detach(self): + """Like :meth:`~io.BufferedIOBase.detach`, but async. + + This also re-wraps the result in a new :term:`asynchronous file object` + wrapper. + + """ + raw = await trio.run_in_worker_thread(self._wrapped.detach) return wrap_file(raw) async def close(self): + """Like :meth:`~io.IOBase.close`, but async. + + This is also shielded from cancellation; if a cancellation scope is + cancelled, the wrapped file object will still be safely closed. + + """ + # ensure the underling file is closed during cancellation with _core.open_cancel_scope(shield=True): await trio.run_in_worker_thread(self._wrapped.close) @@ -94,11 +120,11 @@ async def open_file(file, mode='r', buffering=-1, encoding=None, errors=None, """Asynchronous version of :func:`~io.open`. Returns: - An :term:`asynchronous file object` wrapped in an :term:`asynchronous context manager`. + An :term:`asynchronous file object` Example:: - async with trio.io.open_file(filename) as f: + async with await trio.open_file(filename) as f: async for line in f: pass @@ -111,21 +137,20 @@ async def open_file(file, mode='r', buffering=-1, encoding=None, errors=None, def wrap_file(file): - """This wraps any file-like object in an equivalent asynchronous file-like - object. + """This wraps any file object in a wrapper that provides an asynchronous file + object interface. Args: file: a :term:`file object` Returns: - An :term:`asynchronous file object` + AsyncIO: a file object wrapper Example:: - f = StringIO('asdf') - async_f = wrap_file(f) + async_file = trio.wrap_file(StringIO('asdf')) - assert await async_f.read() == 'asdf' + assert await async_file.read() == 'asdf' """ From f217d736b8a426b22e295a61eb31c6b8e5e0f2c0 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 02:24:19 -0500 Subject: [PATCH 29/57] trio: rename AsyncIO to AsyncIOWrapper This frees us up to create other AsyncIO implementations, and clarifies that this one is the thread-wrapper implementation. --- docs/source/glossary.rst | 4 ++-- docs/source/reference-io.rst | 2 +- trio/_file_io/_file_io.py | 10 +++++----- trio/tests/test_file_io.py | 8 ++++---- trio/tests/test_path.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 3b9c0ee0d7..7786951386 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -12,6 +12,6 @@ Glossary This is an object with an API identical to a :term:`file object`, with the exception that all nontrivial methods are coroutine functions. - A non-normative interface is defined by :class:`trio.AsyncIO`. The main - way to create an asynchronous file object is by using the + A non-normative interface is defined by :class:`trio.AsyncIOWrapper`. The + main way to create an asynchronous file object is by using the :func:`trio.open_file` function. diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 080e2a1aa2..3c46612d8c 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -390,7 +390,7 @@ Asynchronous file objects .. currentmodule:: trio -.. autoclass:: AsyncIO +.. autoclass:: AsyncIOWrapper :members: Subprocesses diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index 07b0319746..974489bdf8 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -7,7 +7,7 @@ from trio._file_io._helpers import async_wraps -__all__ = ['open_file', 'wrap_file', 'AsyncIO'] +__all__ = ['open_file', 'wrap_file', 'AsyncIOWrapper'] _FILE_SYNC_ATTRS = [ 'closed', @@ -28,8 +28,8 @@ ] -class AsyncIO: - """:class:`trio.AsyncIO` is a generic :class:`~io.IOBase` wrapper that +class AsyncIOWrapper: + """:class:`trio.AsyncIOWrapper` is a generic :class:`~io.IOBase` wrapper that implements the :term:`asynchronous file object` interface. Wrapped methods that could block are executed in :meth:`trio.run_in_worker_thread`. @@ -144,7 +144,7 @@ def wrap_file(file): file: a :term:`file object` Returns: - AsyncIO: a file object wrapper + AsyncIOWrapper: a file object wrapper Example:: @@ -155,6 +155,6 @@ def wrap_file(file): """ if isinstance(file, io.IOBase): - return AsyncIO(file) + return AsyncIOWrapper(file) raise TypeError(file) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 297c5553ad..79946eb75a 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -83,7 +83,7 @@ def test_async_methods_generated_once(async_file): def test_async_methods_signature(async_file): # use read as a representative of all async methods assert async_file.read.__name__ == 'read' - assert async_file.read.__qualname__ == 'AsyncIO.read' + assert async_file.read.__qualname__ == 'AsyncIOWrapper.read' assert 'io.StringIO.read' in async_file.read.__doc__ @@ -119,14 +119,14 @@ async def test_async_methods_match_wrapper(async_file, wrapped): async def test_open(path): f = await trio.open_file(path, 'w') - assert isinstance(f, trio.AsyncIO) + assert isinstance(f, trio.AsyncIOWrapper) await f.close() async def test_open_context_manager(path): async with await trio.open_file(path, 'w') as f: - assert isinstance(f, trio.AsyncIO) + assert isinstance(f, trio.AsyncIOWrapper) assert not f.closed assert f.closed @@ -165,5 +165,5 @@ async def test_detach_rewraps_asynciobase(): detached = await async_file.detach() - assert isinstance(detached, trio.AsyncIO) + assert isinstance(detached, trio.AsyncIOWrapper) assert detached.wrapped == raw diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 85084b63f7..987ad69b58 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -25,7 +25,7 @@ async def test_windows_owner_group_raises(path): async def test_open_is_async_context_manager(path): async with await path.open('w') as f: - assert isinstance(f, trio.AsyncIO) + assert isinstance(f, trio.AsyncIOWrapper) assert f.closed From c7203b616c6edd966396119a36cf6199e455d0d3 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 03:11:29 -0500 Subject: [PATCH 30/57] path: add more tests --- trio/_file_io/_path.py | 5 ++++- trio/tests/test_path.py | 42 +++++++++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 0f39b4ddbd..6556555684 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -86,6 +86,9 @@ def __getattr__(self, name): def __dir__(self): return super().__dir__() + self._forward + def __repr__(self): + return 'AsyncPath({})'.format(self.__fspath__()) + @classmethod def _from_wrapped(cls, wrapped): self = object.__new__(cls) @@ -102,5 +105,5 @@ async def open(self, *args, **kwargs): # python3.5 compat -if hasattr(os, 'PathLike'): +if hasattr(os, 'PathLike'): # pragma: no cover os.PathLike.register(AsyncPath) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 987ad69b58..b9467042ce 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -11,16 +11,10 @@ def path(tmpdir): return trio.AsyncPath(p) -async def test_windows_owner_group_raises(path): - path._wrapped.__class__ = pathlib.WindowsPath - - assert isinstance(path._wrapped, pathlib.WindowsPath) - - with pytest.raises(NotImplementedError): - await path.owner() - - with pytest.raises(NotImplementedError): - await path.group() +def method_pair(path, method_name): + path = pathlib.Path(path) + async_path = trio.AsyncPath(path) + return getattr(path, method_name), getattr(async_path, method_name) async def test_open_is_async_context_manager(path): @@ -44,3 +38,31 @@ async def test_async_method_signature(path): assert path.resolve.__qualname__ == 'AsyncPath.resolve' assert 'pathlib.Path.resolve' in path.resolve.__doc__ + + +@pytest.mark.parametrize('method_name', ['is_dir', 'is_file']) +async def test_compare_async_stat_methods(method_name): + + method, async_method = method_pair('.', method_name) + + result = method() + async_result = await async_method() + + assert result == async_result + + +async def test_invalid_name_not_wrapped(path): + with pytest.raises(AttributeError): + getattr(path, 'invalid_fake_attr') + + +@pytest.mark.parametrize('method_name', ['absolute', 'resolve']) +async def test_forward_functions_rewrap(method_name): + + method, async_method = method_pair('.', method_name) + + result = method() + async_result = await async_method() + + assert isinstance(async_result, trio.AsyncPath) + assert result.__fspath__() == async_result.__fspath__() From 266bedb211ddd3a44f6ebfb12701216a48d6a7ad Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 03:38:07 -0500 Subject: [PATCH 31/57] path: add proper support for path magic --- trio/_file_io/_path.py | 14 +++++++++++--- trio/tests/test_path.py | 9 +++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 6556555684..2d6632b49d 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -41,6 +41,7 @@ def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) cls._forward = [] + # forward functions of _forwards for attr_name, attr in cls._forwards.__dict__.items(): if attr_name.startswith('_') or attr_name in attrs: @@ -67,10 +68,20 @@ def __init__(cls, name, bases, attrs): else: raise TypeError(attr_name, type(attr)) + # generate wrappers for magic + for attr_name in cls._forward_magic: + attr = getattr(cls._forwards, attr_name) + wrapper = _forward_factory(cls, attr_name, attr) + setattr(cls, attr_name, wrapper) + class AsyncPath(metaclass=AsyncAutoWrapperType): _wraps = Path _forwards = PurePath + _forward_magic = [ + '__str__', '__fspath__', '__bytes__', + '__eq__', '__lt__', '__le__', '__gt__', '__ge__' + ] def __new__(cls, *args, **kwargs): path = Path(*args, **kwargs) @@ -95,9 +106,6 @@ def _from_wrapped(cls, wrapped): self._wrapped = wrapped return self - def __fspath__(self): - return self._wrapped.__fspath__() - async def open(self, *args, **kwargs): func = partial(self._wrapped.open, *args, **kwargs) value = await trio.run_in_worker_thread(func) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index b9467042ce..81f05ca262 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -7,7 +7,7 @@ @pytest.fixture def path(tmpdir): - p = tmpdir.join('test').__fspath__() + p = str(tmpdir.join('test')) return trio.AsyncPath(p) @@ -24,6 +24,11 @@ async def test_open_is_async_context_manager(path): assert f.closed +async def test_forward_magic(path): + assert path.__fspath__() == path._wrapped.__fspath__() + assert str(path) == str(path._wrapped) + + async def test_forwarded_properties(path): # use `name` as a representative of forwarded properties @@ -65,4 +70,4 @@ async def test_forward_functions_rewrap(method_name): async_result = await async_method() assert isinstance(async_result, trio.AsyncPath) - assert result.__fspath__() == async_result.__fspath__() + assert str(result) == str(async_result) From 3126442a97d0fa177206b862938784f35a94685f Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 03:53:11 -0500 Subject: [PATCH 32/57] path: add tests for repr and forward rewrap --- trio/tests/test_path.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 81f05ca262..ee07c1f4a1 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -62,7 +62,7 @@ async def test_invalid_name_not_wrapped(path): @pytest.mark.parametrize('method_name', ['absolute', 'resolve']) -async def test_forward_functions_rewrap(method_name): +async def test_async_methods_rewrap(method_name): method, async_method = method_pair('.', method_name) @@ -71,3 +71,19 @@ async def test_forward_functions_rewrap(method_name): assert isinstance(async_result, trio.AsyncPath) assert str(result) == str(async_result) + + +async def test_forward_methods_rewrap(path, tmpdir): + with_name = path.with_name('foo') + with_suffix = path.with_suffix('.py') + + assert isinstance(with_name, trio.AsyncPath) + assert with_name == tmpdir.join('foo') + assert isinstance(with_suffix, trio.AsyncPath) + assert with_suffix == tmpdir.join('test.py') + + +async def test_repr(): + path = trio.AsyncPath('.') + + assert repr(path) == 'AsyncPath(.)' From d0d9b057226112bfef092e8125fd9eb9b37259df Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 04:10:55 -0500 Subject: [PATCH 33/57] path: add type tests --- trio/_file_io/_path.py | 6 ++++++ trio/tests/test_path.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index 2d6632b49d..f0d103db3d 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -41,7 +41,11 @@ 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) + 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: @@ -55,6 +59,7 @@ def __init__(cls, name, bases, attrs): 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(): if attr_name.startswith('_') or attr_name in attrs: @@ -68,6 +73,7 @@ def __init__(cls, name, bases, attrs): 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) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index ee07c1f4a1..34f99ae9c2 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -3,6 +3,7 @@ import pytest import trio +from trio._file_io._path import AsyncAutoWrapperType as Type @pytest.fixture @@ -87,3 +88,35 @@ async def test_repr(): path = trio.AsyncPath('.') assert repr(path) == 'AsyncPath(.)' + + +class MockWrapped: + unsupported = 'unsupported' + _private = 'private' + + +class MockWrapper: + _forwards = MockWrapped + _wraps = MockWrapped + + +async def test_type_forwards_unsupported(): + with pytest.raises(TypeError): + Type.generate_forwards(MockWrapper, {}) + + +async def test_type_wraps_unsupported(): + with pytest.raises(TypeError): + Type.generate_wraps(MockWrapper, {}) + + +async def test_type_forwards_private(): + Type.generate_forwards(MockWrapper, {'unsupported': None}) + + assert not hasattr(MockWrapper, '_private') + + +async def test_type_wraps_private(): + Type.generate_wraps(MockWrapper, {'unsupported': None}) + + assert not hasattr(MockWrapper, '_private') From 9cdf33ee75cf26cee517bc4bb61d084893710d10 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 04:22:27 -0500 Subject: [PATCH 34/57] path: fix python 3.5 compatibility --- trio/_file_io/_path.py | 10 ++++++++-- trio/tests/test_path.py | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index f0d103db3d..ca6d945f0f 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -85,7 +85,7 @@ class AsyncPath(metaclass=AsyncAutoWrapperType): _wraps = Path _forwards = PurePath _forward_magic = [ - '__str__', '__fspath__', '__bytes__', + '__str__', '__bytes__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__' ] @@ -104,7 +104,13 @@ def __dir__(self): return super().__dir__() + self._forward def __repr__(self): - return 'AsyncPath({})'.format(self.__fspath__()) + return 'AsyncPath({})'.format(str(self)) + + def __fspath__(self): + try: + return self._wrapped.__fspath__() + except AttributeError: + return str(self) @classmethod def _from_wrapped(cls, wrapped): diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 34f99ae9c2..fdfd828308 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -26,7 +26,6 @@ async def test_open_is_async_context_manager(path): async def test_forward_magic(path): - assert path.__fspath__() == path._wrapped.__fspath__() assert str(path) == str(path._wrapped) From da7b8ec0cac8546d7f9b7f96c9a91e2c8d45a27b Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 10 Jun 2017 04:37:03 -0500 Subject: [PATCH 35/57] path: add documentation --- docs/source/reference-io.rst | 8 ++++++-- trio/_file_io/_path.py | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 3c46612d8c..38ebfa896d 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -388,11 +388,15 @@ Asynchronous disk I/O Asynchronous file objects ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. currentmodule:: trio - .. autoclass:: AsyncIOWrapper :members: +Asynchronous path objects +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: AsyncPath + :members: + Subprocesses ------------ diff --git a/trio/_file_io/_path.py b/trio/_file_io/_path.py index ca6d945f0f..9858f2d6cc 100644 --- a/trio/_file_io/_path.py +++ b/trio/_file_io/_path.py @@ -82,6 +82,11 @@ def generate_magic(cls, attrs): class AsyncPath(metaclass=AsyncAutoWrapperType): + """:class:`trio.AsyncPath` is a :class:`~pathlib.Path` wrapper executes concrete + non-computational Path methods in :meth:`trio.run_in_worker_thread`. + + """ + _wraps = Path _forwards = PurePath _forward_magic = [ @@ -124,6 +129,10 @@ async def open(self, *args, **kwargs): return trio.wrap_file(value) +# not documented upstream +delattr(AsyncPath.absolute, '__doc__') + + # python3.5 compat if hasattr(os, 'PathLike'): # pragma: no cover os.PathLike.register(AsyncPath) From 179393a66add4b6678edecb0cc4c468ec0bf79ab Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:21:07 -0500 Subject: [PATCH 36/57] file_io: make AsyncIOWrapper private --- docs/source/glossary.rst | 5 ++--- docs/source/reference-io.rst | 8 -------- trio/_file_io/_file_io.py | 11 ++++++----- trio/tests/test_file_io.py | 8 ++++---- trio/tests/test_path.py | 3 ++- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 7786951386..20ceb1510a 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -10,8 +10,7 @@ Glossary asynchronous file object This is an object with an API identical to a :term:`file object`, with - the exception that all nontrivial methods are coroutine functions. + the exception that all non-computational methods are async functions. - A non-normative interface is defined by :class:`trio.AsyncIOWrapper`. The - main way to create an asynchronous file object is by using the + The main way to create an asynchronous file object is by using the :func:`trio.open_file` function. diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 38ebfa896d..efe636809e 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -383,14 +383,6 @@ Asynchronous disk I/O .. autofunction:: wrap_file -.. _asynchronous-file-objects: - -Asynchronous file objects -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: AsyncIOWrapper - :members: - Asynchronous path objects ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/trio/_file_io/_file_io.py b/trio/_file_io/_file_io.py index 974489bdf8..217d2e6693 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io/_file_io.py @@ -7,7 +7,7 @@ from trio._file_io._helpers import async_wraps -__all__ = ['open_file', 'wrap_file', 'AsyncIOWrapper'] +__all__ = ['open_file', 'wrap_file'] _FILE_SYNC_ATTRS = [ 'closed', @@ -29,12 +29,13 @@ class AsyncIOWrapper: - """:class:`trio.AsyncIOWrapper` is a generic :class:`~io.IOBase` wrapper that - implements the :term:`asynchronous file object` interface. Wrapped methods that - could block are executed in :meth:`trio.run_in_worker_thread`. + """A generic :class:`~io.IOBase` wrapper that implements the :term:`asynchronous + file object` interface. Wrapped methods that could block are executed in + :meth:`trio.run_in_worker_thread`. All properties and methods defined in in :mod:`~io` are exposed by this wrapper, if they exist in the wrapped file object. + """ def __init__(self, file): @@ -144,7 +145,7 @@ def wrap_file(file): file: a :term:`file object` Returns: - AsyncIOWrapper: a file object wrapper + An :term:`asynchronous file object` that wraps `file` Example:: diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 79946eb75a..d38cdd2489 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -7,7 +7,7 @@ import trio from trio import _core -from trio._file_io._file_io import _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS +from trio._file_io._file_io import AsyncIOWrapper, _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS @pytest.fixture @@ -119,14 +119,14 @@ async def test_async_methods_match_wrapper(async_file, wrapped): async def test_open(path): f = await trio.open_file(path, 'w') - assert isinstance(f, trio.AsyncIOWrapper) + assert isinstance(f, AsyncIOWrapper) await f.close() async def test_open_context_manager(path): async with await trio.open_file(path, 'w') as f: - assert isinstance(f, trio.AsyncIOWrapper) + assert isinstance(f, AsyncIOWrapper) assert not f.closed assert f.closed @@ -165,5 +165,5 @@ async def test_detach_rewraps_asynciobase(): detached = await async_file.detach() - assert isinstance(detached, trio.AsyncIOWrapper) + assert isinstance(detached, AsyncIOWrapper) assert detached.wrapped == raw diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index fdfd828308..4dbe77feef 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -4,6 +4,7 @@ import trio from trio._file_io._path import AsyncAutoWrapperType as Type +from trio._file_io._file_io import AsyncIOWrapper @pytest.fixture @@ -20,7 +21,7 @@ def method_pair(path, method_name): async def test_open_is_async_context_manager(path): async with await path.open('w') as f: - assert isinstance(f, trio.AsyncIOWrapper) + assert isinstance(f, AsyncIOWrapper) assert f.closed From ed6bd54b333152a5d0f6644e604189bb70604301 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:28:04 -0500 Subject: [PATCH 37/57] file_io: remove package This collapses the _file_io and _path modules into the trio package, and moves _helpers to _util --- trio/__init__.py | 3 +++ trio/{_file_io => }/_file_io.py | 3 +-- trio/_file_io/__init__.py | 7 ------- trio/_file_io/_helpers.py | 19 ------------------- trio/{_file_io => }/_path.py | 2 +- trio/_util.py | 19 +++++++++++++++++++ trio/tests/test_file_io.py | 2 +- trio/tests/test_path.py | 4 ++-- 8 files changed, 27 insertions(+), 32 deletions(-) rename trio/{_file_io => }/_file_io.py (98%) delete mode 100644 trio/_file_io/__init__.py delete mode 100644 trio/_file_io/_helpers.py rename trio/{_file_io => }/_path.py (98%) diff --git a/trio/__init__.py b/trio/__init__.py index c464ddeb8b..be94d9a873 100644 --- a/trio/__init__.py +++ b/trio/__init__.py @@ -53,6 +53,9 @@ from ._file_io import * __all__ += _file_io.__all__ +from ._path import * +__all__ += _path.__all__ + # Imported by default from . import socket from . import abc diff --git a/trio/_file_io/_file_io.py b/trio/_file_io.py similarity index 98% rename from trio/_file_io/_file_io.py rename to trio/_file_io.py index 217d2e6693..478e933d39 100644 --- a/trio/_file_io/_file_io.py +++ b/trio/_file_io.py @@ -3,8 +3,7 @@ import trio from trio import _core -from trio._util import aiter_compat -from trio._file_io._helpers import async_wraps +from trio._util import aiter_compat, async_wraps __all__ = ['open_file', 'wrap_file'] diff --git a/trio/_file_io/__init__.py b/trio/_file_io/__init__.py deleted file mode 100644 index 1241242eae..0000000000 --- a/trio/_file_io/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -__all__ = [] - -from ._file_io import * -__all__ += _file_io.__all__ - -from ._path import * -__all__ += _path.__all__ diff --git a/trio/_file_io/_helpers.py b/trio/_file_io/_helpers.py deleted file mode 100644 index 30ee6a911b..0000000000 --- a/trio/_file_io/_helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -import trio - -from functools import wraps, partial - - -def async_wraps(cls, wrapped_cls, attr_name): - def decorator(func): - func.__name__ = attr_name - func.__qualname__ = '.'.join((cls.__qualname__, - attr_name)) - - func.__doc__ = """Like :meth:`~{}.{}.{}`, but async. - - """.format(wrapped_cls.__module__, - wrapped_cls.__qualname__, - attr_name) - - return func - return decorator diff --git a/trio/_file_io/_path.py b/trio/_path.py similarity index 98% rename from trio/_file_io/_path.py rename to trio/_path.py index 9858f2d6cc..5be295b004 100644 --- a/trio/_file_io/_path.py +++ b/trio/_path.py @@ -4,7 +4,7 @@ from pathlib import Path, PurePath import trio -from trio._file_io._helpers import async_wraps +from trio._util import async_wraps __all__ = ['AsyncPath'] diff --git a/trio/_util.py b/trio/_util.py index 0986e02440..7b3692b697 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -188,3 +188,22 @@ async def __aenter__(self): async def __aexit__(self, *args): return self.sync.__exit__() + + +def async_wraps(cls, wrapped_cls, attr_name): + """Similar to wraps, but for async wrappers of non-async functions. + + """ + def decorator(func): + func.__name__ = attr_name + func.__qualname__ = '.'.join((cls.__qualname__, + attr_name)) + + func.__doc__ = """Like :meth:`~{}.{}.{}`, but async. + + """.format(wrapped_cls.__module__, + wrapped_cls.__qualname__, + attr_name) + + return func + return decorator diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index d38cdd2489..eec7b06e3a 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -7,7 +7,7 @@ import trio from trio import _core -from trio._file_io._file_io import AsyncIOWrapper, _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS +from trio._file_io import AsyncIOWrapper, _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS @pytest.fixture diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 4dbe77feef..d83fc27801 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -3,8 +3,8 @@ import pytest import trio -from trio._file_io._path import AsyncAutoWrapperType as Type -from trio._file_io._file_io import AsyncIOWrapper +from trio._path import AsyncAutoWrapperType as Type +from trio._file_io import AsyncIOWrapper @pytest.fixture From 67e8f9c9a001a4e391d7e4036df487509f5c424d Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:36:57 -0500 Subject: [PATCH 38/57] test_file_io: use wrap_file instead of private _wrapped --- trio/tests/test_file_io.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index eec7b06e3a..70d9ce826b 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -43,8 +43,12 @@ def test_dir_matches_wrapped(async_file, wrapped): assert not any(attr in dir(async_file) for attr in attrs if attr not in dir(wrapped)) -def test_unsupported_not_forwarded(async_file): - async_file._wrapped = sentinel +def test_unsupported_not_forwarded(): + class FakeFile(io.IOBase): + def unsupported_attr(self): + pass + + async_file = trio.wrap_file(FakeFile()) assert hasattr(async_file.wrapped, 'unsupported_attr') @@ -132,8 +136,8 @@ async def test_open_context_manager(path): assert f.closed -async def test_async_iter(async_file): - async_file._wrapped = io.StringIO('test\nfoo\nbar') +async def test_async_iter(): + async_file = trio.wrap_file(io.StringIO('test\nfoo\nbar')) expected = iter(list(async_file.wrapped)) async_file.wrapped.seek(0) From 39c33f06b6deb5d2a64704456d31aa4d698e603e Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:43:43 -0500 Subject: [PATCH 39/57] path: rename AsyncPath to Path --- docs/source/reference-io.rst | 2 +- trio/_path.py | 24 ++++++++++++------------ trio/tests/test_path.py | 16 ++++++++-------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index efe636809e..66fc348847 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -386,7 +386,7 @@ Asynchronous disk I/O Asynchronous path objects ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: AsyncPath +.. autoclass:: Path :members: Subprocesses diff --git a/trio/_path.py b/trio/_path.py index 5be295b004..6193a00ffe 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -1,13 +1,13 @@ from functools import wraps, partial import os import types -from pathlib import Path, PurePath +import pathlib import trio from trio._util import async_wraps -__all__ = ['AsyncPath'] +__all__ = ['Path'] def _forward_factory(cls, attr_name, attr): @@ -24,7 +24,7 @@ def wrapper(self, *args, **kwargs): def thread_wrapper_factory(cls, meth_name): - @async_wraps(cls, Path, meth_name) + @async_wraps(cls, pathlib.Path, meth_name) async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) @@ -81,21 +81,21 @@ def generate_magic(cls, attrs): setattr(cls, attr_name, wrapper) -class AsyncPath(metaclass=AsyncAutoWrapperType): - """:class:`trio.AsyncPath` is a :class:`~pathlib.Path` wrapper executes concrete - non-computational Path methods in :meth:`trio.run_in_worker_thread`. +class Path(metaclass=AsyncAutoWrapperType): + """A :class:`~pathlib.Path` wrapper that executes non-computational Path methods in + :meth:`trio.run_in_worker_thread`. """ - _wraps = Path - _forwards = PurePath + _wraps = pathlib.Path + _forwards = pathlib.PurePath _forward_magic = [ '__str__', '__bytes__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__' ] def __new__(cls, *args, **kwargs): - path = Path(*args, **kwargs) + path = pathlib.Path(*args, **kwargs) self = cls._from_wrapped(path) return self @@ -109,7 +109,7 @@ def __dir__(self): return super().__dir__() + self._forward def __repr__(self): - return 'AsyncPath({})'.format(str(self)) + return 'trio.Path({})'.format(str(self)) def __fspath__(self): try: @@ -130,9 +130,9 @@ async def open(self, *args, **kwargs): # not documented upstream -delattr(AsyncPath.absolute, '__doc__') +delattr(Path.absolute, '__doc__') # python3.5 compat if hasattr(os, 'PathLike'): # pragma: no cover - os.PathLike.register(AsyncPath) + os.PathLike.register(Path) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index d83fc27801..ac5f730906 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -10,12 +10,12 @@ @pytest.fixture def path(tmpdir): p = str(tmpdir.join('test')) - return trio.AsyncPath(p) + return trio.Path(p) def method_pair(path, method_name): path = pathlib.Path(path) - async_path = trio.AsyncPath(path) + async_path = trio.Path(path) return getattr(path, method_name), getattr(async_path, method_name) @@ -41,7 +41,7 @@ async def test_async_method_signature(path): # use `resolve` as a representative of wrapped methods assert path.resolve.__name__ == 'resolve' - assert path.resolve.__qualname__ == 'AsyncPath.resolve' + assert path.resolve.__qualname__ == 'Path.resolve' assert 'pathlib.Path.resolve' in path.resolve.__doc__ @@ -70,7 +70,7 @@ async def test_async_methods_rewrap(method_name): result = method() async_result = await async_method() - assert isinstance(async_result, trio.AsyncPath) + assert isinstance(async_result, trio.Path) assert str(result) == str(async_result) @@ -78,16 +78,16 @@ async def test_forward_methods_rewrap(path, tmpdir): with_name = path.with_name('foo') with_suffix = path.with_suffix('.py') - assert isinstance(with_name, trio.AsyncPath) + assert isinstance(with_name, trio.Path) assert with_name == tmpdir.join('foo') - assert isinstance(with_suffix, trio.AsyncPath) + assert isinstance(with_suffix, trio.Path) assert with_suffix == tmpdir.join('test.py') async def test_repr(): - path = trio.AsyncPath('.') + path = trio.Path('.') - assert repr(path) == 'AsyncPath(.)' + assert repr(path) == 'trio.Path(.)' class MockWrapped: From 5034e5ddda05d0016b3c55546f26d58e7206841a Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:47:20 -0500 Subject: [PATCH 40/57] path: replace __new__ with __init__ This slightly duplicates _from_wrapped logic, but is probably cleaner overall. --- trio/_path.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index 6193a00ffe..9423a7384a 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -94,11 +94,8 @@ class Path(metaclass=AsyncAutoWrapperType): '__eq__', '__lt__', '__le__', '__gt__', '__ge__' ] - def __new__(cls, *args, **kwargs): - path = pathlib.Path(*args, **kwargs) - - self = cls._from_wrapped(path) - return self + def __init__(self, *args, **kwargs): + self._wrapped = pathlib.Path(*args, **kwargs) def __getattr__(self, name): if name in self._forward: From d9cac31d4760474759ae56aed917f66645b4e265 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 16:59:04 -0500 Subject: [PATCH 41/57] test_file_io: cleanup This replaces incorrect use of ==/is, makes test_async_iter more obvious, and adds a missing raises assertion in test_close_cancelled. --- trio/tests/test_file_io.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index 70d9ce826b..c1afe18e73 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -31,7 +31,7 @@ def test_wrap_invalid(): def test_wrapped_property(async_file, wrapped): - assert async_file.wrapped == wrapped + assert async_file.wrapped is wrapped def test_dir_matches_wrapped(async_file, wrapped): @@ -61,7 +61,7 @@ def test_sync_attrs_forwarded(async_file, wrapped): if attr_name not in dir(async_file): continue - assert getattr(async_file, attr_name) == getattr(wrapped, attr_name) + assert getattr(async_file, attr_name) is getattr(wrapped, attr_name) def test_sync_attrs_match_wrapper(async_file, wrapped): @@ -81,7 +81,7 @@ def test_async_methods_generated_once(async_file): if meth_name not in dir(async_file): continue - assert getattr(async_file, meth_name) == getattr(async_file, meth_name) + assert getattr(async_file, meth_name) is getattr(async_file, meth_name) def test_async_methods_signature(async_file): @@ -138,14 +138,14 @@ async def test_open_context_manager(path): async def test_async_iter(): async_file = trio.wrap_file(io.StringIO('test\nfoo\nbar')) - expected = iter(list(async_file.wrapped)) + expected = list(async_file.wrapped) + result = [] async_file.wrapped.seek(0) - async for actual in async_file: - assert actual == next(expected) + async for line in async_file: + result.append(line) - with pytest.raises(StopIteration): - next(expected) + assert result == expected async def test_close_cancelled(path): @@ -156,7 +156,8 @@ async def test_close_cancelled(path): with pytest.raises(_core.Cancelled): await f.write('a') - await f.close() + with pytest.raises(_core.Cancelled): + await f.close() assert f.closed @@ -170,4 +171,4 @@ async def test_detach_rewraps_asynciobase(): detached = await async_file.detach() assert isinstance(detached, AsyncIOWrapper) - assert detached.wrapped == raw + assert detached.wrapped is raw From 953db745556dd281905619ed87a8655e26b46369 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 17:14:36 -0500 Subject: [PATCH 42/57] file_io: make _FILE_SYNC_ATTRS and _FILE_SYNC_METHODS sets instead of lists --- trio/_file_io.py | 8 ++++---- trio/tests/test_file_io.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/trio/_file_io.py b/trio/_file_io.py index 478e933d39..5a8657ac4d 100644 --- a/trio/_file_io.py +++ b/trio/_file_io.py @@ -8,23 +8,23 @@ __all__ = ['open_file', 'wrap_file'] -_FILE_SYNC_ATTRS = [ +_FILE_SYNC_ATTRS = { 'closed', 'encoding', 'errors', 'fileno', 'isatty', 'newlines', 'readable', 'seekable', 'writable', # not defined in *IOBase: 'buffer', 'raw', 'line_buffering', 'closefd', 'name', 'mode', 'getvalue', 'getbuffer', -] +} -_FILE_ASYNC_METHODS = [ +_FILE_ASYNC_METHODS = { 'flush', 'read', 'read1', 'readall', 'readinto', 'readline', 'readlines', 'seek', 'tell', 'truncate', 'write', 'writelines', # not defined in *IOBase: 'readinto1', 'peek', -] +} class AsyncIOWrapper: diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index c1afe18e73..cb0d305a5c 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -3,7 +3,7 @@ import pytest from unittest import mock -from unittest.mock import patch, sentinel +from unittest.mock import sentinel import trio from trio import _core @@ -35,7 +35,7 @@ def test_wrapped_property(async_file, wrapped): def test_dir_matches_wrapped(async_file, wrapped): - attrs = _FILE_SYNC_ATTRS + _FILE_ASYNC_METHODS + attrs = _FILE_SYNC_ATTRS.union(_FILE_ASYNC_METHODS) # all supported attrs in wrapped should be available in async_file assert all(attr in dir(async_file) for attr in attrs if attr in dir(wrapped)) From bc8c3179d02716b999591f1134349407289e3251 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 19:14:10 -0500 Subject: [PATCH 43/57] file_io: add support for duck-files --- trio/_file_io.py | 6 +++--- trio/tests/test_file_io.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/trio/_file_io.py b/trio/_file_io.py index 5a8657ac4d..c61246f239 100644 --- a/trio/_file_io.py +++ b/trio/_file_io.py @@ -154,7 +154,7 @@ def wrap_file(file): """ - if isinstance(file, io.IOBase): - return AsyncIOWrapper(file) + if not hasattr(file, 'close') or not callable(file.close): + raise TypeError('{} does not implement required method close()'.format(file)) - raise TypeError(file) + return AsyncIOWrapper(file) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index cb0d305a5c..dfa77b6873 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -30,6 +30,18 @@ def test_wrap_invalid(): trio.wrap_file(str()) +def test_wrap_non_iobase(): + class FakeFile: + def close(self): + pass + + wrapped = FakeFile() + assert not isinstance(wrapped, io.IOBase) + + async_file = trio.wrap_file(wrapped) + assert isinstance(async_file, AsyncIOWrapper) + + def test_wrapped_property(async_file, wrapped): assert async_file.wrapped is wrapped From b70c41376d925e20079a8915a04908678b1bd83d Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 11 Jun 2017 23:18:46 -0500 Subject: [PATCH 44/57] path: add comment for 3.5-compatibility in __fspath__ --- trio/_path.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trio/_path.py b/trio/_path.py index 9423a7384a..cbafddf720 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -111,7 +111,8 @@ def __repr__(self): def __fspath__(self): try: return self._wrapped.__fspath__() - except AttributeError: + # python3.5 compat + except AttributeError: # pragma: no cover return str(self) @classmethod From db69f48f33ef49e4a7a625e89b3e73327834ac33 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 00:55:23 -0500 Subject: [PATCH 45/57] path: fix comparison magic Previously attempts to compare trio.Path with itself would fail, due to the wrapped object not supporting comparisons with trio.Path. --- trio/_path.py | 14 +++++++++++++- trio/tests/test_path.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/trio/_path.py b/trio/_path.py index cbafddf720..3a6d6a3227 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -23,6 +23,18 @@ def wrapper(self, *args, **kwargs): 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 + return attr(self._wrapped, other) + return wrapper + + def thread_wrapper_factory(cls, meth_name): @async_wraps(cls, pathlib.Path, meth_name) async def wrapper(self, *args, **kwargs): @@ -77,7 +89,7 @@ def generate_magic(cls, attrs): # generate wrappers for magic for attr_name in cls._forward_magic: attr = getattr(cls._forwards, attr_name) - wrapper = _forward_factory(cls, attr_name, attr) + wrapper = _forward_magic(cls, attr) setattr(cls, attr_name, wrapper) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index ac5f730906..318b331c3a 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -26,8 +26,21 @@ async def test_open_is_async_context_manager(path): assert f.closed +async def test_cmp_magic(): + a, b = trio.Path(''), trio.Path('') + assert a == b + assert not a != b + + a, b = trio.Path('a'), trio.Path('b') + assert a < b + assert b > a + + assert not a == None + + async def test_forward_magic(path): assert str(path) == str(path._wrapped) + assert bytes(path) == bytes(path._wrapped) async def test_forwarded_properties(path): From 04ed0ad5cb1a088a40d204562e8e8639ab29f3fc Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 01:04:52 -0500 Subject: [PATCH 46/57] path: add tests for paths being initialized from other paths This also removes the _from_wrapped classmethod, and adds an adds an additional test for passing a trio path to trio open_file. --- trio/_path.py | 10 ++-------- trio/tests/test_path.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index 3a6d6a3227..6bcabed49e 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -17,7 +17,7 @@ def wrapper(self, *args, **kwargs): value = attr(*args, **kwargs) if isinstance(value, cls._forwards): # re-wrap methods that return new paths - value = cls._from_wrapped(value) + value = cls(value) return value return wrapper @@ -42,7 +42,7 @@ async def wrapper(self, *args, **kwargs): func = partial(meth, *args, **kwargs) value = await trio.run_in_worker_thread(func) if isinstance(value, cls._wraps): - value = cls._from_wrapped(value) + value = cls(value) return value return wrapper @@ -127,12 +127,6 @@ def __fspath__(self): except AttributeError: # pragma: no cover return str(self) - @classmethod - def _from_wrapped(cls, wrapped): - self = object.__new__(cls) - self._wrapped = wrapped - return self - async def open(self, *args, **kwargs): func = partial(self._wrapped.open, *args, **kwargs) value = await trio.run_in_worker_thread(func) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 318b331c3a..e20e5a27bd 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -133,3 +133,15 @@ async def test_type_wraps_private(): Type.generate_wraps(MockWrapper, {'unsupported': None}) assert not hasattr(MockWrapper, '_private') + + +@pytest.mark.parametrize('other_cls', [pathlib.Path, trio.Path]) +async def test_path_wraps_path(other_cls, path): + other = other_cls(path) + + assert path == other + + +async def test_open_file_can_open_path(path): + async with await trio.open_file(path, 'w') as f: + assert f.name == path.__fspath__() From 00d6eb8a28487e39e2d8579895941cf632d39665 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 01:27:35 -0500 Subject: [PATCH 47/57] path: fix python3.5 compatibility This removes the pathlib.Path(trio.Path()) test, unwraps args in trio.Path(), and manually unwraps paths inside open_file. --- trio/_file_io.py | 4 ++++ trio/_path.py | 7 +++++-- trio/tests/test_path.py | 5 ++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/trio/_file_io.py b/trio/_file_io.py index c61246f239..19a5f608c8 100644 --- a/trio/_file_io.py +++ b/trio/_file_io.py @@ -131,6 +131,10 @@ async def open_file(file, mode='r', buffering=-1, encoding=None, errors=None, assert f.closed """ + # python3.5 compat + if isinstance(file, trio.Path): + file = file.__fspath__() + _file = wrap_file(await trio.run_in_worker_thread(io.open, file, mode, buffering, encoding, errors, newline, closefd, opener)) return _file diff --git a/trio/_path.py b/trio/_path.py index 6bcabed49e..c2319cb5cb 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -106,8 +106,11 @@ class Path(metaclass=AsyncAutoWrapperType): '__eq__', '__lt__', '__le__', '__gt__', '__ge__' ] - def __init__(self, *args, **kwargs): - self._wrapped = pathlib.Path(*args, **kwargs) + def __init__(self, *args): + # python3.5 compat + args = [str(a) for a in args] + + self._wrapped = pathlib.Path(*args) def __getattr__(self, name): if name in self._forward: diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index e20e5a27bd..0fc76b8e23 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -135,9 +135,8 @@ async def test_type_wraps_private(): assert not hasattr(MockWrapper, '_private') -@pytest.mark.parametrize('other_cls', [pathlib.Path, trio.Path]) -async def test_path_wraps_path(other_cls, path): - other = other_cls(path) +async def test_path_wraps_path(path): + other = trio.Path(path) assert path == other From f2b237ff4364b2a0763f265887eba17c8c4ec754 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 01:40:27 -0500 Subject: [PATCH 48/57] test_file_io: skip coverage on fake methods --- trio/tests/test_file_io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index dfa77b6873..d088074be6 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -32,7 +32,7 @@ def test_wrap_invalid(): def test_wrap_non_iobase(): class FakeFile: - def close(self): + def close(self): # pragma: no cover pass wrapped = FakeFile() @@ -57,7 +57,7 @@ def test_dir_matches_wrapped(async_file, wrapped): def test_unsupported_not_forwarded(): class FakeFile(io.IOBase): - def unsupported_attr(self): + def unsupported_attr(self): # pragma: no cover pass async_file = trio.wrap_file(FakeFile()) From d9c09af6db5f26030b239f0f8109ccb99c9c58dc Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 11:56:59 -0500 Subject: [PATCH 49/57] path: improve comments --- trio/_path.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index c2319cb5cb..e778be74ff 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -94,7 +94,7 @@ def generate_magic(cls, attrs): class Path(metaclass=AsyncAutoWrapperType): - """A :class:`~pathlib.Path` wrapper that executes non-computational Path methods in + """A :class:`~pathlib.Path` wrapper that executes blocking Path methods in :meth:`trio.run_in_worker_thread`. """ @@ -136,10 +136,12 @@ async def open(self, *args, **kwargs): return trio.wrap_file(value) -# not documented upstream -delattr(Path.absolute, '__doc__') +# 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__ # python3.5 compat -if hasattr(os, 'PathLike'): # pragma: no cover +if hasattr(os, 'PathLike'): os.PathLike.register(Path) From 7555d3b621ca9355a5695260be02b54c25ce9fb3 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 23:00:26 -0500 Subject: [PATCH 50/57] file_io: make duck-file definition more restrictive duck-files are now required to define either write or read in addition to close --- trio/_file_io.py | 8 ++++++-- trio/tests/test_file_io.py | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/trio/_file_io.py b/trio/_file_io.py index 19a5f608c8..8642c93586 100644 --- a/trio/_file_io.py +++ b/trio/_file_io.py @@ -158,7 +158,11 @@ def wrap_file(file): """ - if not hasattr(file, 'close') or not callable(file.close): - raise TypeError('{} does not implement required method close()'.format(file)) + def has(attr): + return hasattr(file, attr) and callable(getattr(file, attr)) + + if not (has('close') and (has('read') or has('write'))): + raise TypeError('{} does not implement required duck-file methods: ' + 'close and (read or write)'.format(file)) return AsyncIOWrapper(file) diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py index d088074be6..3401229984 100644 --- a/trio/tests/test_file_io.py +++ b/trio/tests/test_file_io.py @@ -35,12 +35,20 @@ class FakeFile: def close(self): # pragma: no cover pass + def write(self): # pragma: no cover + pass + wrapped = FakeFile() assert not isinstance(wrapped, io.IOBase) async_file = trio.wrap_file(wrapped) assert isinstance(async_file, AsyncIOWrapper) + del FakeFile.write + + with pytest.raises(TypeError): + trio.wrap_file(FakeFile()) + def test_wrapped_property(async_file, wrapped): assert async_file.wrapped is wrapped @@ -56,7 +64,7 @@ def test_dir_matches_wrapped(async_file, wrapped): def test_unsupported_not_forwarded(): - class FakeFile(io.IOBase): + class FakeFile(io.RawIOBase): def unsupported_attr(self): # pragma: no cover pass From 66836f34f4056219f8565b94abddac3d19f7c194 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 23:09:56 -0500 Subject: [PATCH 51/57] test_path: improve magic tests This removes references to _wrapped, and adds all possible Path comparisons that affect trio.Path. --- trio/tests/test_path.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 0fc76b8e23..e8bb9fe147 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -26,21 +26,30 @@ async def test_open_is_async_context_manager(path): assert f.closed -async def test_cmp_magic(): - a, b = trio.Path(''), trio.Path('') +async def test_magic(): + path = trio.Path('test') + + assert str(path) == 'test' + assert bytes(path) == b'test' + + +cls_pairs = [(trio.Path, pathlib.Path), (pathlib.Path, trio.Path), (trio.Path, trio.Path)] + + +@pytest.mark.parametrize('cls_a,cls_b', cls_pairs) +async def test_cmp_magic(cls_a, cls_b): + a, b = cls_a(''), cls_b('') assert a == b assert not a != b - a, b = trio.Path('a'), trio.Path('b') + a, b = cls_a('a'), cls_b('b') assert a < b assert b > a + # this is intentionally testing equivalence with none, due to the + # other=sentinel logic in _forward_magic assert not a == None - - -async def test_forward_magic(path): - assert str(path) == str(path._wrapped) - assert bytes(path) == bytes(path._wrapped) + assert not b == None async def test_forwarded_properties(path): From 6042f8b08b1a11ffbae74ba1cc139dc141fa4b12 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 23:17:31 -0500 Subject: [PATCH 52/57] path: use more strict compatibility logic Instead of indiscriminately casting everything to str, including non-PathLikes, just unwrap all trio.Paths inside args. --- trio/_path.py | 8 ++++++-- trio/tests/test_path.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index e778be74ff..c300e63b07 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -108,9 +108,13 @@ class Path(metaclass=AsyncAutoWrapperType): def __init__(self, *args): # python3.5 compat - args = [str(a) for a in args] + new_args = [] + for arg in args: + if isinstance(arg, Path): + arg = arg._wrapped + new_args.append(arg) - self._wrapped = pathlib.Path(*args) + self._wrapped = pathlib.Path(*new_args) def __getattr__(self, name): if name in self._forward: diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index e8bb9fe147..89f603c08d 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -150,6 +150,11 @@ async def test_path_wraps_path(path): assert path == other +async def test_path_nonpath(): + with pytest.raises(TypeError): + trio.Path(1) + + async def test_open_file_can_open_path(path): async with await trio.open_file(path, 'w') as f: assert f.name == path.__fspath__() From 8f076d94d2b0bc664253eff073799354d920d61c Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 23:34:08 -0500 Subject: [PATCH 53/57] path: unwrap any trio.Path argument to any wrapped method This fixes python3.5 compatibility. --- trio/_path.py | 22 +++++++++++++++------- trio/tests/test_path.py | 12 ++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index c300e63b07..2c27c1f5b6 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -10,9 +10,21 @@ __all__ = ['Path'] +# python3.5 compat: __fspath__ does not exist in 3.5, so unwrap any trio.Path +# being passed to any wrapped method +def unwrap_paths(args): + new_args = [] + for arg in args: + if isinstance(arg, Path): + arg = arg._wrapped + new_args.append(arg) + return new_args + + def _forward_factory(cls, attr_name, attr): @wraps(attr) def wrapper(self, *args, **kwargs): + args = unwrap_paths(args) attr = getattr(self._wrapped, attr_name) value = attr(*args, **kwargs) if isinstance(value, cls._forwards): @@ -38,6 +50,7 @@ def wrapper(self, other=sentinel): def thread_wrapper_factory(cls, meth_name): @async_wraps(cls, pathlib.Path, meth_name) async def wrapper(self, *args, **kwargs): + args = unwrap_paths(args) meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) value = await trio.run_in_worker_thread(func) @@ -107,14 +120,9 @@ class Path(metaclass=AsyncAutoWrapperType): ] def __init__(self, *args): - # python3.5 compat - new_args = [] - for arg in args: - if isinstance(arg, Path): - arg = arg._wrapped - new_args.append(arg) + args = unwrap_paths(args) - self._wrapped = pathlib.Path(*new_args) + self._wrapped = pathlib.Path(*args) def __getattr__(self, name): if name in self._forward: diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 89f603c08d..e3c1cb9c64 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -144,10 +144,14 @@ async def test_type_wraps_private(): assert not hasattr(MockWrapper, '_private') -async def test_path_wraps_path(path): - other = trio.Path(path) - - assert path == other +@pytest.mark.parametrize('meth', [trio.Path.__init__, trio.Path.joinpath]) +async def test_path_wraps_path(path, meth): + wrapped = await path.absolute() + result = meth(path, wrapped) + if result is None: + result = path + + assert wrapped == result async def test_path_nonpath(): From e63b4ccc429d70c58c5b0f77cd6b4ca811d140bd Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 12 Jun 2017 23:40:24 -0500 Subject: [PATCH 54/57] path: de-genericify path return value rewrapping logic --- trio/_path.py | 16 +++++++++------- trio/tests/test_path.py | 7 +++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index 2c27c1f5b6..d04606a3c8 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -21,16 +21,20 @@ def unwrap_paths(args): return new_args +# 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): args = unwrap_paths(args) attr = getattr(self._wrapped, attr_name) value = attr(*args, **kwargs) - if isinstance(value, cls._forwards): - # re-wrap methods that return new paths - value = cls(value) - return value + return rewrap_path(value) return wrapper @@ -54,9 +58,7 @@ async def wrapper(self, *args, **kwargs): meth = getattr(self._wrapped, meth_name) func = partial(meth, *args, **kwargs) value = await trio.run_in_worker_thread(func) - if isinstance(value, cls._wraps): - value = cls(value) - return value + return rewrap_path(value) return wrapper diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index e3c1cb9c64..c827ec4eff 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -106,6 +106,13 @@ async def test_forward_methods_rewrap(path, tmpdir): assert with_suffix == tmpdir.join('test.py') +async def test_forward_methods_without_rewrap(path, tmpdir): + path = await path.parent.resolve() + + assert path.as_uri().startswith('file:///') + assert path.as_posix().startswith('/') + + async def test_repr(): path = trio.Path('.') From f0e5fa443058cdbe63bda2b63131b4ab03f3de25 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 13 Jun 2017 00:09:27 -0500 Subject: [PATCH 55/57] path: re-wrap property return values --- trio/_path.py | 3 ++- trio/tests/test_path.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/trio/_path.py b/trio/_path.py index d04606a3c8..9f07c80aa4 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -128,7 +128,8 @@ def __init__(self, *args): def __getattr__(self, name): if name in self._forward: - return getattr(self._wrapped, name) + value = getattr(self._wrapped, name) + return rewrap_path(value) raise AttributeError(name) def __dir__(self): diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index c827ec4eff..a0e3bbbbf7 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -106,6 +106,10 @@ async def test_forward_methods_rewrap(path, tmpdir): assert with_suffix == tmpdir.join('test.py') +async def test_forward_properties_rewrap(path): + assert isinstance(path.parent, trio.Path) + + async def test_forward_methods_without_rewrap(path, tmpdir): path = await path.parent.resolve() From e17ac07501ba3f77c81df09b4059c89e39757a7b Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 13 Jun 2017 01:09:29 -0500 Subject: [PATCH 56/57] path: add support for __truediv__, __rtruediv__ --- trio/_path.py | 5 +++-- trio/tests/test_path.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/trio/_path.py b/trio/_path.py index 9f07c80aa4..2f88f1d9fa 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -47,7 +47,8 @@ def wrapper(self, other=sentinel): return attr(self._wrapped) if isinstance(other, cls): other = other._wrapped - return attr(self._wrapped, other) + value = attr(self._wrapped, other) + return rewrap_path(value) return wrapper @@ -117,7 +118,7 @@ class Path(metaclass=AsyncAutoWrapperType): _wraps = pathlib.Path _forwards = pathlib.PurePath _forward_magic = [ - '__str__', '__bytes__', + '__str__', '__bytes__', '__truediv__', '__rtruediv__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__' ] diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index a0e3bbbbf7..384a144d85 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -52,6 +52,21 @@ async def test_cmp_magic(cls_a, cls_b): assert not b == None +# upstream python3.5 bug: we should also test (pathlib.Path, trio.Path), but +# __*div__ does not properly raise NotImplementedError like the other comparison +# magic, so trio.Path's implementation does not get dispatched +cls_pairs = [(trio.Path, pathlib.Path), (trio.Path, trio.Path), (trio.Path, str), (str, trio.Path)] + + +@pytest.mark.parametrize('cls_a,cls_b', cls_pairs) +async def test_div_magic(cls_a, cls_b): + a, b = cls_a('a'), cls_b('b') + + result = a / b + assert isinstance(result, trio.Path) + assert str(result) == 'a/b' + + async def test_forwarded_properties(path): # use `name` as a representative of forwarded properties From e74396cc5f78d75a9093fca4bead3acd99322c9a Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 13 Jun 2017 01:24:59 -0500 Subject: [PATCH 57/57] test_path: fix windows compatibility --- trio/tests/test_path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py index 384a144d85..346c5ace34 100644 --- a/trio/tests/test_path.py +++ b/trio/tests/test_path.py @@ -1,3 +1,4 @@ +import os import pathlib import pytest @@ -64,7 +65,7 @@ async def test_div_magic(cls_a, cls_b): result = a / b assert isinstance(result, trio.Path) - assert str(result) == 'a/b' + assert str(result) == os.path.join('a', 'b') async def test_forwarded_properties(path): @@ -129,7 +130,6 @@ async def test_forward_methods_without_rewrap(path, tmpdir): path = await path.parent.resolve() assert path.as_uri().startswith('file:///') - assert path.as_posix().startswith('/') async def test_repr():