Repository: python-trio/trio

Issue:
trio.Path.home raises "AttributeError: _flavour"
I get the following on Mac and Linux:
```
>>> import trio
>>> trio.Path.home()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.7.2_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/pathlib.py", line 1055, in home
    return cls(cls()._flavour.gethomedir(None))
  File "/Users/zach/.allotropic/python/default/lib/python3.7/site-packages/trio/_path.py", line 157, in __getattr__
    raise AttributeError(name)
AttributeError: _flavour
```
As the traceback states, ``pathlib.Path`` is instantiating itself to dertermine its ``_flavour``, which is a class to delegate filesystem calls to differing Windows and Posix environments.

I think the explanation is that trio.Path does not override ``__new__``, so it doesn't get a chance to turn into a ``PosixPath`` or ``WindowsPath`` object like ``pathlib.Path`` objects do.  The ``_flavour`` attribute is only available from these system-specific classes, and the ``AsyncAutoWrapperType`` doesn't explicitly wrap these classes to make it an attribute of ``trio.Path``.

One workaround is to override ``home`` in ``trio.Path``:
```
    @classmethod
    def home(cls):
        return cls(cls()._wraps()._flavour.gethomedir(None))
```

There are other approaches and I'd like to check in with the team here.  Any thoughts?


Discussion:
Hmm, looks like a silly bug in `generate_wraps`:

```python
            if isinstance(attr, classmethod):
                setattr(cls, attr_name, attr)
```

So we're just setting `trio.Path.home = pathlib.Path.home`. Obviously this is not going to work, since we pass the wrong `cls` argument, and also it probably needs to be asyncified? (Not 100% sure.) Ultimately I guess we should do something like:

```python
@classmethod
async def home(cls):
    return await run_sync_in_worker_thread(pathlib.Path.home)
```

Except maybe we should generate this instead of writing it manually?

See also: #917, which points out an issue with `.cwd()`. `pathlib.Path` has two classmethods: `home` and `cwd`. So these overlap.
Thanks for the pointer to ``generate_wraps`` - I hadn't thought about it that way.

I was also considering posting to #917 about making ``home`` async because it could end up with i/o reading the password database in ``pathlib.py``:
```
return pwd.getpwuid(os.getuid()).pw_dir
```
I'll take a look at generating both ``home`` and ``cwd`` classmethods, and for now I'll assume that both of these methods will be async.

Resolving patch:
diff --git a/trio/_path.py b/trio/_path.py
--- a/trio/_path.py
+++ b/trio/_path.py
@@ -54,7 +54,7 @@ def wrapper(self, other=sentinel):
 
 
 def thread_wrapper_factory(cls, meth_name):
-    @async_wraps(cls, pathlib.Path, meth_name)
+    @async_wraps(cls, cls._wraps, meth_name)
     async def wrapper(self, *args, **kwargs):
         args = unwrap_paths(args)
         meth = getattr(self._wrapped, meth_name)
@@ -65,6 +65,19 @@ async def wrapper(self, *args, **kwargs):
     return wrapper
 
 
+def classmethod_wrapper_factory(cls, meth_name):
+    @classmethod
+    @async_wraps(cls, cls._wraps, meth_name)
+    async def wrapper(cls, *args, **kwargs):
+        args = unwrap_paths(args)
+        meth = getattr(cls._wraps, meth_name)
+        func = partial(meth, *args, **kwargs)
+        value = await trio.run_sync_in_worker_thread(func)
+        return rewrap_path(value)
+
+    return wrapper
+
+
 class AsyncAutoWrapperType(type):
     def __init__(cls, name, bases, attrs):
         super().__init__(name, bases, attrs)
@@ -93,9 +106,9 @@ def generate_wraps(cls, attrs):
         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)
+                wrapper = classmethod_wrapper_factory(cls, attr_name)
+                setattr(cls, attr_name, wrapper)
             elif isinstance(attr, types.FunctionType):
                 wrapper = thread_wrapper_factory(cls, attr_name)
                 setattr(cls, attr_name, wrapper)


Test patch:
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
@@ -209,3 +209,14 @@ async def test_iterdir(path):
         entries.add(entry.name)
 
     assert entries == {'bar.txt', 'foo'}
+
+
+async def test_classmethods():
+    assert isinstance(await trio.Path.home(), trio.Path)
+
+    # pathlib.Path has only two classmethods
+    assert str(await trio.Path.home()) == os.path.expanduser('~')
+    assert str(await trio.Path.cwd()) == os.getcwd()
+
+    # Wrapped method has docstring
+    assert trio.Path.home.__doc__