Repository: python-trio/trio

Issue:
In the async file code, use trio._util.fspath instead of calling __fspath__ directly



Discussion:

Resolving patch:
diff --git a/trio/_file_io.py b/trio/_file_io.py
--- a/trio/_file_io.py
+++ b/trio/_file_io.py
@@ -4,7 +4,7 @@
 import trio
 from . import _core
 from .abc import AsyncResource
-from ._util import aiter_compat, async_wraps
+from ._util import aiter_compat, async_wraps, fspath
 
 __all__ = ['open_file', 'wrap_file']
 
@@ -162,7 +162,7 @@ async def open_file(
     """
     # python3.5 compat
     if isinstance(file, trio.Path):
-        file = file.__fspath__()
+        file = fspath(file)
 
     _file = wrap_file(
         await trio.run_sync_in_worker_thread(
diff --git a/trio/_path.py b/trio/_path.py
--- a/trio/_path.py
+++ b/trio/_path.py
@@ -4,7 +4,7 @@
 import pathlib
 
 import trio
-from trio._util import async_wraps
+from trio._util import async_wraps, fspath
 
 __all__ = ['Path']
 
@@ -141,11 +141,7 @@ def __repr__(self):
         return 'trio.Path({})'.format(repr(str(self)))
 
     def __fspath__(self):
-        try:
-            return self._wrapped.__fspath__()
-        # python3.5 compat
-        except AttributeError:  # pragma: no cover
-            return str(self)
+        return fspath(self._wrapped)
 
     @wraps(pathlib.Path.open)
     async def open(self, *args, **kwargs):
diff --git a/trio/_util.py b/trio/_util.py
--- a/trio/_util.py
+++ b/trio/_util.py
@@ -2,6 +2,7 @@
 
 import os
 import sys
+import pathlib
 from functools import wraps
 import typing as t
 
@@ -257,18 +258,20 @@ def fix_one(obj):
         fix_one(obj)
 
 
-# This is copied from PEP 519 as the implementation of os.fspath for
-# Python 3.5. See: https://www.python.org/dev/peps/pep-0519/#os
+# This is based on the PEP 519 fspath implementation.
+# The function has been adapted to work with pathlib objects on python 3.5
 # The input typehint is removed as there is no os.PathLike on 3.5.
+# See: https://www.python.org/dev/peps/pep-0519/#os
 
 
 def fspath(path) -> t.Union[str, bytes]:
     """Return the string representation of the path.
 
-    If str or bytes is passed in, it is returned unchanged. If __fspath__()
-    returns something other than str or bytes then TypeError is raised. If
-    this function is given something that is not str, bytes, or os.PathLike
-    then TypeError is raised.
+    If str or bytes is passed in, it is returned unchanged. If a pre-python 3.6
+    pathlib object is passed, its string representation is returned. If
+    __fspath__() returns something other than str or bytes then TypeError is
+    raised. If this function is given something that is not str, bytes,
+    pathlib.PurePath or os.PathLike then TypeError is raised.
     """
     if isinstance(path, (str, bytes)):
         return path
@@ -281,6 +284,10 @@ def fspath(path) -> t.Union[str, bytes]:
     except AttributeError:
         if hasattr(path_type, '__fspath__'):
             raise
+        # On python 3.5 pathlib objects don't have an __fspath__ method.
+        # but we still want to get their string representation.
+        if isinstance(path, pathlib.PurePath):
+            return str(path)
     else:
         if isinstance(path, (str, bytes)):
             return path


Test patch:
diff --git a/trio/tests/test_file_io.py b/trio/tests/test_file_io.py
--- a/trio/tests/test_file_io.py
+++ b/trio/tests/test_file_io.py
@@ -7,12 +7,13 @@
 
 import trio
 from trio import _core
+from trio._util import fspath
 from trio._file_io import AsyncIOWrapper, _FILE_SYNC_ATTRS, _FILE_ASYNC_METHODS
 
 
 @pytest.fixture
 def path(tmpdir):
-    return tmpdir.join('test').__fspath__()
+    return fspath(tmpdir.join('test'))
 
 
 @pytest.fixture
diff --git a/trio/tests/test_highlevel_open_unix_stream.py b/trio/tests/test_highlevel_open_unix_stream.py
--- a/trio/tests/test_highlevel_open_unix_stream.py
+++ b/trio/tests/test_highlevel_open_unix_stream.py
@@ -5,6 +5,7 @@
 import pytest
 
 from trio import open_unix_socket, Path
+from trio._util import fspath
 
 try:
     from socket import AF_UNIX
@@ -20,7 +21,7 @@ async def get_server_socket():
         pass
 
     serv_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-    serv_sock.bind(name.__fspath__())  # bpo-32562
+    serv_sock.bind(fspath(name))  # bpo-32562
     serv_sock.listen(1)
 
     return name, serv_sock
@@ -47,7 +48,7 @@ async def test_open_bad_socket():
 
 async def test_open_unix_socket():
     name, serv_sock = await get_server_socket()
-    unix_socket = await open_unix_socket(name.__fspath__())
+    unix_socket = await open_unix_socket(fspath(name))
     await _do_test_on_sock(serv_sock, unix_socket)
 
 
diff --git a/trio/tests/test_path.py b/trio/tests/test_path.py
--- a/trio/tests/test_path.py
+++ b/trio/tests/test_path.py
@@ -5,6 +5,7 @@
 
 import trio
 from trio._path import AsyncAutoWrapperType as Type
+from trio._util import fspath
 from trio._file_io import AsyncIOWrapper
 
 
@@ -196,4 +197,4 @@ async def test_path_nonpath():
 
 async def test_open_file_can_open_path(path):
     async with await trio.open_file(path, 'w') as f:
-        assert f.name == path.__fspath__()
+        assert f.name == fspath(path)