Repository: python-trio/trio (Python)

Issue:
Regression: unix socket bind does not work with a PathLike argument
trio.socket.SocketType.bind no longer works with a PathLike (e.g. pathlib.Path) argument.

Simple test program:
```python
import socket, trio.socket
from pathlib import Path

async def main():
    sock = trio.socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    await sock.bind(Path('/tmp/x.sock'))

trio.run(main)
```

Results in:
```
Traceback (most recent call last):
  File "/tmp/x.py", line 8, in <module>
    trio.run(main)
  File "/usr/lib/python3.12/site-packages/trio/_core/_run.py", line 2305, in run
    raise runner.main_task_outcome.error
  File "/tmp/x.py", line 6, in main
    await sock.bind(Path('/tmp/x.sock'))
  File "/usr/lib/python3.12/site-packages/trio/_socket.py", line 874, in bind
    address = await self._resolve_address_nocp(address, local=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/trio/_socket.py", line 922, in _resolve_address_nocp
    return await _resolve_address_nocp(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/trio/_socket.py", line 472, in _resolve_address_nocp
    assert isinstance(address, (str, bytes))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
```

This bugs seems to have been introduced in [cf1f3c745](https://github.com/python-trio/trio/commit/cf1f3c745#diff-300b958089de6b62596dad8182b7dfb234ac01d922fdbce3456f641c5a9964e1R466), released in v0.23.0.


Discussion:
You can fix this by doing this:
```python
import socket, trio.socket
from pathlib import Path

async def main():
    sock = trio.socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    await sock.bind(str(Path('/tmp/x.sock')))

trio.run(main)
```
> You can fix this by doing this:

I am of course aware of that. But still: (1) it broke my existing code,
(2) it makes for a much less nice API when the caller has to think of
this, (3) the code was clearly meant to accept Path objects, as the
surrounding comments and the call to os.fspath suggest.

I don't think this passed, or passes, typing? The method is typed as

```python
Address: TypeAlias = Union[
    str, bytes, Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]
]
async def bind(self, address: Address) -> None:
  ...
```

and was previously
```python
async def bind(self, address: tuple[object, ...] | str | bytes) -> None:
```

But yeah you're totally right that the comment mentions "path-likes", and as such the typing should be updated to include `Path` and/or `PathLike` and the assertion changed. Thank you!



Patch:
diff --git a/newsfragments/3041.bugfix.rst b/newsfragments/3041.bugfix.rst
new file mode 100644
index 0000000000..b21851e5b3
--- /dev/null
+++ b/newsfragments/3041.bugfix.rst
@@ -0,0 +1,1 @@
+Allow sockets to bind any ``os.PathLike`` object.
diff --git a/src/trio/_socket.py b/src/trio/_socket.py
index 0a3bd1cba1..69a4d773d4 100644
--- a/src/trio/_socket.py
+++ b/src/trio/_socket.py
@@ -469,7 +469,7 @@ async def _resolve_address_nocp(
             )
     elif hasattr(_stdlib_socket, "AF_UNIX") and family == _stdlib_socket.AF_UNIX:
         # unwrap path-likes
-        assert isinstance(address, (str, bytes))
+        assert isinstance(address, (str, bytes, os.PathLike))
         return os.fspath(address)
     else:
         return address


Test patch:
diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py
index b98b3246e9..9397733c72 100644
--- a/src/trio/_tests/test_socket.py
+++ b/src/trio/_tests/test_socket.py
@@ -6,6 +6,7 @@
 import socket as stdlib_socket
 import sys
 import tempfile
+from pathlib import Path
 from socket import AddressFamily, SocketKind
 from typing import TYPE_CHECKING, Any, Callable, List, Tuple, Union
 
@@ -1077,7 +1078,7 @@ async def test_unix_domain_socket() -> None:
     # Bind has a special branch to use a thread, since it has to do filesystem
     # traversal. Maybe connect should too? Not sure.
 
-    async def check_AF_UNIX(path: str | bytes) -> None:
+    async def check_AF_UNIX(path: str | bytes | os.PathLike[str]) -> None:
         with tsocket.socket(family=tsocket.AF_UNIX) as lsock:
             await lsock.bind(path)
             lsock.listen(10)
@@ -1091,8 +1092,11 @@ async def check_AF_UNIX(path: str | bytes) -> None:
     # Can't use tmpdir fixture, because we can exceed the maximum AF_UNIX path
     # length on macOS.
     with tempfile.TemporaryDirectory() as tmpdir:
-        path = f"{tmpdir}/sock"
-        await check_AF_UNIX(path)
+        # Test passing various supported types as path
+        # Must use different filenames to prevent "address already in use"
+        await check_AF_UNIX(f"{tmpdir}/sock")
+        await check_AF_UNIX(Path(f"{tmpdir}/sock1"))
+        await check_AF_UNIX(os.fsencode(f"{tmpdir}/sock2"))
 
     try:
         cookie = os.urandom(20).hex().encode("ascii")