python-trio / trio-typing

Type hints for Trio and related projects
Other
27 stars 14 forks source link

Unable to .write() to an opened trio.Path #28

Closed altendky closed 2 years ago

altendky commented 3 years ago

Problem

You can't (per hints) .write() to a file you have opened. Though you can .writelines() and .flush()...

Opening a trio.Path...

https://github.com/python-trio/trio-typing/blob/f32f17b0f242daf2d42407f383ca581d64b6c299/trio-stubs/__init__.pyi#L397-L404

results in an _AsyncIOBase.

https://github.com/python-trio/trio-typing/blob/f32f17b0f242daf2d42407f383ca581d64b6c299/trio-stubs/__init__.pyi#L309-L325

But, .write() is located on _AsyncRawIOBase.

https://github.com/python-trio/trio-typing/blob/f32f17b0f242daf2d42407f383ca581d64b6c299/trio-stubs/__init__.pyi#L327-L331

Cause

It appears that Trio actually returns an AsyncIOWrapper, not the hinted _AsyncIOBase, and only after verifying that the object being wrapped does in fact have .write() (and more).

https://github.com/python-trio/trio/blob/master/trio/_path.py#L174-L183

    @wraps(pathlib.Path.open)
    async def open(self, *args, **kwargs):
        """Open the file pointed to by the path, like the :func:`trio.open_file`
        function does.
        """

        func = partial(self._wrapped.open, *args, **kwargs)
        value = await trio.to_thread.run_sync(func)
        return trio.wrap_file(value)

https://github.com/python-trio/trio/blob/d203b30f807e43dc9b101de2e52355ebd10b757a/trio/_file_io.py#L164-L191

    def wrap_file(file):

<snip>

    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)

Also, AsyncIOWrapper does look to specifically list .write() as a (trio.to_thread.run_sync) pass-through method.

https://github.com/python-trio/trio/blob/d203b30f807e43dc9b101de2e52355ebd10b757a/trio/_file_io.py#L70-L85

    def __getattr__(self, name):
        if name in _FILE_SYNC_ATTRS:
            return getattr(self._wrapped, name)
        if name in _FILE_ASYNC_METHODS:
            meth = getattr(self._wrapped, name)

            @async_wraps(self.__class__, self._wrapped.__class__, name)
            async def wrapper(*args, **kwargs):
                func = partial(meth, *args, **kwargs)
                return await trio.to_thread.run_sync(func)

            # cache the generated method
            setattr(self, name, wrapper)
            return wrapper

        raise AttributeError(name)

Solution

Per a GitHub search, trio-typing just doesn't have AsyncIOWrapper. I guess it should get added to trio-typing and used as the return hint for trio.Path.open(). Let me know if there's agreement here, or if I have missed or am just unaware of something that makes this change wrong.

belm0 commented 3 years ago

We also see mypy error for read():

async with await path.open('rb') as f:
    buf = await f.read()
error: "_AsyncIOBase" has no attribute "read" 
oremanj commented 3 years ago

I think both of these would resolve if you use the plugin — in general the set of valid methods on a file object depend on the mode you used to open the file. But it would probably be sensible to update trio-typing to:

I’ll leave this issue open to track that.