From 0cbb8506459d83e6eb2eb94cd12354aa6deccd9c Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 29 Jun 2026 19:18:59 -0400 Subject: [PATCH 01/10] Make UDS + `SIGUSR1` optional for Windows Windows (and any CPython that doesn't expose `socket.AF_UNIX`) can't import the UDS transport backend nor `signal.SIGUSR1`, so the unconditional imports break `import tractor` outright on those hosts. Guard the platform-specific bits behind capability probes and fall back to a TCP-only runtime when the UDS backend is unavailable. - across `discovery/_addr.py`, `ipc/_server.py` and `ipc/_types.py`, gate on `getattr(socket, 'AF_UNIX', None)` + `platform.system()` and import `UDSAddress` / `MsgpackUDSStream` only when supported, leaving the names as `None` otherwise. - register the `'uds'` key in `_address_types`, its default-loopback addr, and the transport lookup maps only when the backend actually loads, so TCP keeps working standalone. - in `devx/_stackscope.py`, import `SIGUSR1` conditionally and set it to `None` on Windows. Rebased onto the post-reorg tree where `_addr.py` now lives under `tractor/discovery/`; adapt the relocated imports to the package's `..ipc._uds` / `..ipc._tcp` paths (the original single-dot paths would silently disable UDS on POSIX) and drop a duplicated `TYPE_CHECKING` block and dead `import logging` left by the move. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/devx/_stackscope.py | 11 ++- tractor/discovery/_addr.py | 34 ++++++++-- tractor/ipc/_server.py | 14 +++- tractor/ipc/_types.py | 131 +++++++++++++++++++++--------------- 4 files changed, 127 insertions(+), 63 deletions(-) diff --git a/tractor/devx/_stackscope.py b/tractor/devx/_stackscope.py index e6ea00e5..bc791a4e 100644 --- a/tractor/devx/_stackscope.py +++ b/tractor/devx/_stackscope.py @@ -31,12 +31,21 @@ from threading import ( RLock, ) import multiprocessing as mp + +import platform + from signal import ( signal, getsignal, - SIGUSR1, SIGINT, ) + + +if platform.system() != "Windows": + from signal import SIGUSR1 +else: + SIGUSR1 = None + # import traceback from types import ModuleType from typing import ( diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index 8d440bb3..ccd6e51a 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -23,6 +23,8 @@ from typing import ( TYPE_CHECKING, ) +import platform +import socket from trio import ( SocketListener, ) @@ -32,7 +34,6 @@ from ..runtime._state import ( _def_tpt_proto, ) from ..ipc._tcp import TCPAddress -from ..ipc._uds import UDSAddress if TYPE_CHECKING: # ONLY type-annots, the eager import costs ~4.5ms @@ -44,6 +45,20 @@ else: log = get_logger() +HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None +IS_WINDOWS = platform.system() == "Windows" + +UDSAddress = None # so references exist but do nothing on Windows + +if HAS_AF_UNIX and not IS_WINDOWS: + try: + from ..ipc._uds import UDSAddress as _UDSAddress + UDSAddress = _UDSAddress + except Exception as e: + log.warning("UDS backend import failed: %s", e) +else: + log.warning("UDS backend disabled on this platform.") + # TODO, maybe breakout the netns key to a struct? # class NetNs(Struct)[str, int]: # ... @@ -176,20 +191,25 @@ class Address(Protocol): _address_types: dict[str, Type[Address]] = { 'tcp': TCPAddress, - 'uds': UDSAddress } +if UDSAddress is not None: + _address_types['uds'] = UDSAddress +else: + log.warning("Skipping UDS address type: no UDS backend available.") + # TODO! really these are discovery sys default addrs ONLY useful for # when none is provided to a root actor on first boot. -_default_lo_addrs: dict[ - str, - UnwrappedAddress -] = { +_default_lo_addrs: dict[str, UnwrappedAddress] = { 'tcp': TCPAddress.get_root().unwrap(), - 'uds': UDSAddress.get_root().unwrap(), } +if UDSAddress is not None: + _default_lo_addrs['uds'] = UDSAddress.get_root().unwrap() +else: + log.warning("Skipping UDS default loopback address: no UDS backend available.") + def get_address_cls(name: str) -> Type[Address]: return _address_types[name] diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 80f85841..9bd094e9 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -27,6 +27,8 @@ from functools import partial from itertools import chain import inspect import textwrap +import platform +import socket from types import ( ModuleType, ) @@ -62,18 +64,26 @@ from .. import log from ..discovery._addr import Address from ._chan import Channel from ._transport import MsgTransport -from ._uds import UDSAddress -from ._tcp import TCPAddress + if TYPE_CHECKING: from ..runtime._runtime import Actor from ..runtime._supervise import ActorNursery +from ._tcp import TCPAddress + log = log.get_logger() _PRE_REG_HANDSHAKE_TIMEOUT: float = 10 +UDSAddress = None + +if ( + getattr(socket, 'AF_UNIX', None) is not None + and platform.system() != 'Windows' +): + from ._uds import UDSAddress async def maybe_wait_on_canced_subs( uid: tuple[str, str], diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index 59653b17..72b1d3a7 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -18,106 +18,131 @@ IPC subsys type-lookup helpers? ''' -from typing import ( - Type, - # TYPE_CHECKING, -) - -import trio +from typing import Type +import platform import socket +import trio +from tractor.log import get_logger from tractor.ipc._transport import ( MsgTransportKey, - MsgTransport + MsgTransport, ) from tractor.ipc._tcp import ( TCPAddress, MsgpackTCPStream, ) -from tractor.ipc._uds import ( - UDSAddress, - MsgpackUDSStream, -) -# if TYPE_CHECKING: -# from tractor._addr import Address +log = get_logger() +# ------------------------------------------------------------ +# Optional UDS backend (Windows / some Pythons may not have AF_UNIX) +# ------------------------------------------------------------ +HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None +IS_WINDOWS = platform.system() == "Windows" -Address = TCPAddress|UDSAddress +HAS_UDS = False +UDSAddress = None # type: ignore +MsgpackUDSStream = None # type: ignore -# manually updated list of all supported msg transport types -_msg_transports = [ +if HAS_AF_UNIX and not IS_WINDOWS: + try: + from tractor.ipc._uds import ( # type: ignore + UDSAddress as _UDSAddress, + MsgpackUDSStream as _MsgpackUDSStream, + ) + UDSAddress = _UDSAddress # type: ignore + MsgpackUDSStream = _MsgpackUDSStream # type: ignore + HAS_UDS = True + except Exception as e: + log.warning("UDS backend unavailable (%s); continuing without it.", e) +else: + if not HAS_AF_UNIX: + log.warning("AF_UNIX not exposed by this Python; disabling UDS backend.") + elif IS_WINDOWS: + # Even if the Windows kernel supports AF_UNIX, CPython may not expose it, + # and this project currently targets POSIX for the UDS backend. + log.warning("Windows detected; disabling UDS backend.") + +# ------------------------------------------------------------ +# Public types and transport registries +# ------------------------------------------------------------ + +# Address is TCP-only unless UDS is available. +if HAS_UDS: + Address = TCPAddress | UDSAddress # type: ignore +else: + Address = TCPAddress # type: ignore + +# Manually updated list of all supported msg transport types +_msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, - MsgpackUDSStream ] +if HAS_UDS: + _msg_transports.append(MsgpackUDSStream) # type: ignore - -# convert a MsgTransportKey to the corresponding transport type -_key_to_transport: dict[ - MsgTransportKey, - Type[MsgTransport], -] = { - ('msgpack', 'tcp'): MsgpackTCPStream, - ('msgpack', 'uds'): MsgpackUDSStream, +# Map MsgTransportKey -> transport type +_key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { + ("msgpack", "tcp"): MsgpackTCPStream, } +if HAS_UDS: + _key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore -# convert an Address wrapper to its corresponding transport type -_addr_to_transport: dict[ - Type[TCPAddress|UDSAddress], - Type[MsgTransport] -] = { +# Map Address wrapper -> transport type +_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore TCPAddress: MsgpackTCPStream, - UDSAddress: MsgpackUDSStream, } +if HAS_UDS: + _addr_to_transport[UDSAddress] = MsgpackUDSStream # type: ignore +# ------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------ def transport_from_addr( addr: Address, - codec_key: str = 'msgpack', + codec_key: str = "msgpack", ) -> Type[MsgTransport]: - ''' + """ Given a destination address and a desired codec, find the corresponding `MsgTransport` type. - - ''' + """ try: - return _addr_to_transport[type(addr)] - + return _addr_to_transport[type(addr)] # type: ignore[call-arg] except KeyError: raise NotImplementedError( - f'No known transport for address {repr(addr)}' + f"No known transport for address {repr(addr)}" ) def transport_from_stream( stream: trio.abc.Stream, - codec_key: str = 'msgpack' + codec_key: str = "msgpack", ) -> Type[MsgTransport]: - ''' + """ Given an arbitrary `trio.abc.Stream` and a desired codec, find the corresponding `MsgTransport` type. - - ''' + """ transport = None + if isinstance(stream, trio.SocketStream): sock: socket.socket = stream.socket - match sock.family: - case socket.AF_INET | socket.AF_INET6: - transport = 'tcp' + fam = sock.family - case socket.AF_UNIX: - transport = 'uds' + if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)): + transport = "tcp" - case _: - raise NotImplementedError( - f'Unsupported socket family: {sock.family}' - ) + # Only consider AF_UNIX when both Python exposes it and our backend is active + if transport is None and HAS_UDS and HAS_AF_UNIX and fam == socket.AF_UNIX: # type: ignore[attr-defined] + transport = "uds" + + if transport is None: + raise NotImplementedError(f"Unsupported socket family: {fam}") if not transport: raise NotImplementedError( - f'Could not figure out transport type for stream type {type(stream)}' + f"Could not figure out transport type for stream type {type(stream)}" ) key = (codec_key, transport) - return _key_to_transport[key] From 72441124c0cd8d5652a26187193d4a650d9d8661 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 29 Jun 2026 20:32:12 -0400 Subject: [PATCH 02/10] Fix Windows `import tractor` at the UDS root The prior round gated UDS in four modules but `import tractor` still crashed on Windows: `tractor.ipc._uds` does `from socket import AF_UNIX` at module top, and several modules in the import graph (`discovery._api`, `spawn._reap`, `discovery._multiaddr`, `_testing.addr`) import `_uds` unconditionally. Instead of guarding every importer, fix the root and collapse the per-module probes to one capability flag. - in `ipc/_uds.py`, guard the lone `AF_UNIX` import so the module stays importable everywhere; expose `HAS_UDS = trio.has_unix` as the single source of truth (the same predicate that gates `trio.open_unix_socket()`). - `ipc/_types.py`, `discovery/_addr.py` and `ipc/_server.py` now import `UDSAddress`/`MsgpackUDSStream`/`HAS_UDS` directly and gate the transport + address registries on `HAS_UDS`; drop the duplicated `getattr(socket,'AF_UNIX')` / `platform.system()` probes, the dead `HAS_AF_UNIX` conjunct, and the import-time `log.warning()` spam. - `devx/_stackscope.py` `enable_stack_on_sig()` early-returns when `sig is None`, so a missing `SIGUSR1` (Windows) degrades to a no-op instead of a `TypeError` from `getsignal()` / `signal()`. - add a `windows-latest` CI leg (UDS excluded; informational via `continue-on-error` while support matures) plus an `import tractor` smoke step as the hard signal for the import fix. Because `_uds` is importable everywhere `UDSAddress` stays a real class, so `isinstance()` checks and `wrap_address()` no longer `AttributeError` on no-UDS hosts; actual socket use stays gated on `has_unix`. Review: https://github.com/goodboy/tractor/pull/475 (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- .github/workflows/ci.yml | 17 +++++++++ tractor/devx/_stackscope.py | 14 ++++++- tractor/discovery/_addr.py | 31 +++------------- tractor/ipc/_server.py | 11 +----- tractor/ipc/_types.py | 73 +++++++++++-------------------------- tractor/ipc/_uds.py | 19 +++++++++- 6 files changed, 75 insertions(+), 90 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 529f8b11..7871e346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,11 @@ jobs: name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}' timeout-minutes: 16 runs-on: ${{ matrix.os }} + # Windows support is nascent: keep its legs informational so a + # known-incomplete area doesn't block merges. The `import + # tractor` smoke step below is the hard signal for this job; + # promote the whole leg to required once the suite is green. + continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false @@ -98,6 +103,7 @@ jobs: os: [ ubuntu-latest, macos-latest, + windows-latest, ] python-version: [ '3.13', @@ -118,6 +124,11 @@ jobs: 'tcp', 'uds', ] + exclude: + # UDS is POSIX-only; Windows has no `AF_UNIX` so the + # backend is intentionally unavailable there. + - os: windows-latest + tpt_proto: 'uds' steps: - uses: actions/checkout@v4 @@ -145,6 +156,12 @@ jobs: - name: List deps tree run: uv tree + # hard signal for the Windows import-safety fix: `import + # tractor` must succeed everywhere, and `HAS_UDS` reflects + # platform capability (False on Windows, True on POSIX). + - name: 'Smoke: import tractor' + run: uv run python -c "import tractor; from tractor.ipc._uds import HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" + - name: Run tests run: > uv run diff --git a/tractor/devx/_stackscope.py b/tractor/devx/_stackscope.py index bc791a4e..d0caafbd 100644 --- a/tractor/devx/_stackscope.py +++ b/tractor/devx/_stackscope.py @@ -356,8 +356,8 @@ def dump_tree_on_sig( def enable_stack_on_sig( - sig: int = SIGUSR1, -) -> ModuleType: + sig: int|None = SIGUSR1, +) -> ModuleType|None: ''' Enable `stackscope` tracing on reception of a signal; by default this is SIGUSR1. @@ -376,6 +376,16 @@ def enable_stack_on_sig( >> pkill --signal SIGUSR1 -f ''' + # no `SIGUSR1` on this platform (e.g. Windows) -> nothing to + # wire up; degrade gracefully instead of crashing callers that + # only guard against a missing `stackscope` (`ImportError`). + if sig is None: + log.warning( + 'No `SIGUSR1` on this platform;\n' + 'skipping `stackscope` trace-on-signal setup!\n' + ) + return None + try: # NOTE, `stackscope._glue` does intentional async-gen type # introspection at import-time which trips diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index ccd6e51a..1472349d 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -23,8 +23,6 @@ from typing import ( TYPE_CHECKING, ) -import platform -import socket from trio import ( SocketListener, ) @@ -34,6 +32,10 @@ from ..runtime._state import ( _def_tpt_proto, ) from ..ipc._tcp import TCPAddress +from ..ipc._uds import ( + UDSAddress, + HAS_UDS, +) if TYPE_CHECKING: # ONLY type-annots, the eager import costs ~4.5ms @@ -44,21 +46,6 @@ else: log = get_logger() - -HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None -IS_WINDOWS = platform.system() == "Windows" - -UDSAddress = None # so references exist but do nothing on Windows - -if HAS_AF_UNIX and not IS_WINDOWS: - try: - from ..ipc._uds import UDSAddress as _UDSAddress - UDSAddress = _UDSAddress - except Exception as e: - log.warning("UDS backend import failed: %s", e) -else: - log.warning("UDS backend disabled on this platform.") - # TODO, maybe breakout the netns key to a struct? # class NetNs(Struct)[str, int]: # ... @@ -192,11 +179,8 @@ class Address(Protocol): _address_types: dict[str, Type[Address]] = { 'tcp': TCPAddress, } - -if UDSAddress is not None: +if HAS_UDS: _address_types['uds'] = UDSAddress -else: - log.warning("Skipping UDS address type: no UDS backend available.") # TODO! really these are discovery sys default addrs ONLY useful for @@ -204,11 +188,8 @@ else: _default_lo_addrs: dict[str, UnwrappedAddress] = { 'tcp': TCPAddress.get_root().unwrap(), } - -if UDSAddress is not None: +if HAS_UDS: _default_lo_addrs['uds'] = UDSAddress.get_root().unwrap() -else: - log.warning("Skipping UDS default loopback address: no UDS backend available.") def get_address_cls(name: str) -> Type[Address]: diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 9bd094e9..1775bd92 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -27,8 +27,6 @@ from functools import partial from itertools import chain import inspect import textwrap -import platform -import socket from types import ( ModuleType, ) @@ -72,19 +70,12 @@ if TYPE_CHECKING: from ._tcp import TCPAddress +from ._uds import UDSAddress log = log.get_logger() _PRE_REG_HANDSHAKE_TIMEOUT: float = 10 -UDSAddress = None - -if ( - getattr(socket, 'AF_UNIX', None) is not None - and platform.system() != 'Windows' -): - from ._uds import UDSAddress - async def maybe_wait_on_canced_subs( uid: tuple[str, str], chan: Channel, diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index 72b1d3a7..f07d8cfa 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -19,11 +19,9 @@ IPC subsys type-lookup helpers? ''' from typing import Type -import platform import socket import trio -from tractor.log import get_logger from tractor.ipc._transport import ( MsgTransportKey, MsgTransport, @@ -32,68 +30,37 @@ from tractor.ipc._tcp import ( TCPAddress, MsgpackTCPStream, ) +from tractor.ipc._uds import ( + UDSAddress, + MsgpackUDSStream, + HAS_UDS, +) -log = get_logger() +# the UDS backend is importable everywhere but only *usable* where +# `trio` reports `has_unix` (i.e. POSIX). On Windows / no-`AF_UNIX` +# hosts `HAS_UDS` is `False` and the runtime registers TCP only. +Address = TCPAddress|UDSAddress -# ------------------------------------------------------------ -# Optional UDS backend (Windows / some Pythons may not have AF_UNIX) -# ------------------------------------------------------------ -HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None -IS_WINDOWS = platform.system() == "Windows" - -HAS_UDS = False -UDSAddress = None # type: ignore -MsgpackUDSStream = None # type: ignore - -if HAS_AF_UNIX and not IS_WINDOWS: - try: - from tractor.ipc._uds import ( # type: ignore - UDSAddress as _UDSAddress, - MsgpackUDSStream as _MsgpackUDSStream, - ) - UDSAddress = _UDSAddress # type: ignore - MsgpackUDSStream = _MsgpackUDSStream # type: ignore - HAS_UDS = True - except Exception as e: - log.warning("UDS backend unavailable (%s); continuing without it.", e) -else: - if not HAS_AF_UNIX: - log.warning("AF_UNIX not exposed by this Python; disabling UDS backend.") - elif IS_WINDOWS: - # Even if the Windows kernel supports AF_UNIX, CPython may not expose it, - # and this project currently targets POSIX for the UDS backend. - log.warning("Windows detected; disabling UDS backend.") - -# ------------------------------------------------------------ -# Public types and transport registries -# ------------------------------------------------------------ - -# Address is TCP-only unless UDS is available. -if HAS_UDS: - Address = TCPAddress | UDSAddress # type: ignore -else: - Address = TCPAddress # type: ignore - -# Manually updated list of all supported msg transport types +# manually updated list of all supported msg transport types _msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, ] if HAS_UDS: - _msg_transports.append(MsgpackUDSStream) # type: ignore + _msg_transports.append(MsgpackUDSStream) -# Map MsgTransportKey -> transport type +# map a `MsgTransportKey` to its `MsgTransport` type _key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { - ("msgpack", "tcp"): MsgpackTCPStream, + ('msgpack', 'tcp'): MsgpackTCPStream, } if HAS_UDS: - _key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore + _key_to_transport[('msgpack', 'uds')] = MsgpackUDSStream -# Map Address wrapper -> transport type -_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore +# map an `Address`-wrapper to its `MsgTransport` type +_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { TCPAddress: MsgpackTCPStream, } if HAS_UDS: - _addr_to_transport[UDSAddress] = MsgpackUDSStream # type: ignore + _addr_to_transport[UDSAddress] = MsgpackUDSStream # ------------------------------------------------------------ @@ -132,8 +99,10 @@ def transport_from_stream( if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)): transport = "tcp" - # Only consider AF_UNIX when both Python exposes it and our backend is active - if transport is None and HAS_UDS and HAS_AF_UNIX and fam == socket.AF_UNIX: # type: ignore[attr-defined] + # only consider `AF_UNIX` when the UDS backend is active; + # `HAS_UDS` short-circuits before `socket.AF_UNIX` so this + # stays safe on hosts where that constant is absent. + if transport is None and HAS_UDS and fam == socket.AF_UNIX: # type: ignore[attr-defined] transport = "uds" if transport is None: diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 1986ce57..3d5a3c8e 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -26,11 +26,21 @@ from pathlib import Path import os import sys from socket import ( - AF_UNIX, SOCK_STREAM, SOL_SOCKET, error as socket_error, ) +# NOTE, `AF_UNIX` is absent on Windows / any CPython built without +# unix-domain-socket support. Keep this module importable +# everywhere (so `UDSAddress` stays referenceable for type and +# `isinstance()` checks plus registry lookups); the `AF_UNIX`-using +# code paths below are runtime-only and are never reached when the +# UDS backend is unusable (gated on `trio`'s `has_unix`, see +# `HAS_UDS`). +try: + from socket import AF_UNIX +except ImportError: + AF_UNIX = None import struct from typing import ( Any, @@ -104,6 +114,13 @@ _SUN_PATH_LIMIT: int = ( ) +# single source of truth for "is the UDS backend usable on this +# host?" — reuse `trio`'s `has_unix` (the same predicate that gates +# `trio.open_unix_socket()`) rather than re-deriving an `AF_UNIX` +# probe in every consumer module. +HAS_UDS: bool = has_unix + + def unwrap_sockpath( sockpath: Path, ) -> tuple[Path, Path]: From 36ad1f3dd013994f6dc3476febcd2ccf5444a1ca Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 29 Jun 2026 23:33:36 -0400 Subject: [PATCH 03/10] Skip `test_ringbuf` at collection off-linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tests/test_ringbuf.py` imports `tractor.ipc._ringbuf` at module top, which pulls in `tractor.ipc._linux` whose module-level `ffi.dlopen(None)` raises `OSError` on Windows (and any non-linux host). That fires at COLLECTION, before the module's existing `pytestmark = pytest.mark.skip` can apply, so it aborts the whole pytest session — the new `windows-latest` CI leg never gets past collection. - guard the module with `pytest.skip(allow_module_level=True)` gated on `platform.system() != 'Linux'`, placed before the crashing import — same idiom as `tests/devx/test_debugger.py`. - the `eventfd`-based ringbuf backend is linux-only by design, so macOS skips cleanly too (previously it only skipped incidentally via the absent `cffi` optional dep). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_ringbuf.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_ringbuf.py b/tests/test_ringbuf.py index e55a87b9..2c739568 100644 --- a/tests/test_ringbuf.py +++ b/tests/test_ringbuf.py @@ -1,10 +1,22 @@ import time +import platform import trio import pytest import tractor +# `tractor.ipc._ringbuf` is built on linux `eventfd(2)`; importing +# it pulls in `tractor.ipc._linux` whose module-level +# `ffi.dlopen(None)` raises on non-linux. Skip the whole module at +# COLLECTION before that crashing import runs (a `pytestmark` skip +# is too late — markers apply only after the import succeeds). +if platform.system() != 'Linux': + pytest.skip( + 'ringbuf (eventfd) IPC is linux-only', + allow_module_level=True, + ) + # XXX `cffi` dun build on py3.14 yet.. pytest.importorskip("cffi") From 1691be96fdd99b17bc7ab4db303ad592f9bad76b Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 30 Jun 2026 02:45:23 -0400 Subject: [PATCH 04/10] Scale `test_lifetime_stack` deadline for CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_lifetime_stack_wipes_tmpfile` guards spawn+teardown with a hard-coded `trio.move_on_after()` (1.6s / 1s) that isn't scaled for slow CI. On a noisy macOS runner the `error_in_child=True` case times out before the child error propagates, so the scope cancels and `assert not cs.cancel_called` flips — reddening the (required) macOS leg. Same unscaled-deadline class `main` already fixed for `test_dynamic_pub_sub`. - multiply the budget by `cpu_perf_headroom()` (`tests/conftest`), the established deadline-headroom helper (3x on macOS CI, a 1.0 no-op locally / on un-throttled linux). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_runtime.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 782af81e..8a73d6aa 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -73,6 +73,11 @@ async def test_lifetime_stack_wipes_tmpfile( 1.6 if error_in_child else 1 ) + # scale for slow/noisy CI (esp. macOS) so the child error + # propagates before the deadline; otherwise `move_on_after` + # cancels first and flips the `error_in_child=True` assert. + from .conftest import cpu_perf_headroom + timeout *= cpu_perf_headroom() try: with trio.move_on_after(timeout) as cs: async with tractor.open_nursery( From 34d81adef391009d6297f7dfb9873bea0a2872b3 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 30 Jun 2026 02:45:50 -0400 Subject: [PATCH 05/10] Skip `infect_asyncio` tests on Windows `tractor`'s infect-asyncio mode runs an `asyncio` loop under `trio` guest-mode; on Windows the default `ProactorEventLoop` is incompatible and the suite hangs/crashes mid-run (orphaned py procs), so the `windows-latest` CI leg never finishes reporting. - add a module-level `pytest.skip(allow_module_level=True)` gated on `platform.system() == 'Windows'` to `test_infected_asyncio` and `test_root_infect_asyncio`, before their asyncio-interop imports. macOS/linux are unaffected (they run these fine). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_infected_asyncio.py | 12 ++++++++++++ tests/test_root_infect_asyncio.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index bdb1b852..4787fbbb 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -20,6 +20,18 @@ from typing import ( import pytest import trio import tractor + +# `infect_asyncio` mode is unsupported on Windows (asyncio's +# `ProactorEventLoop` is incompatible with our `trio` guest-mode +# interop and currently hangs/crashes the run). Skip the module on +# Windows so the CI leg completes + reports the rest of the suite. +import platform +if platform.system() == 'Windows': + pytest.skip( + 'infect_asyncio mode is unsupported on Windows', + allow_module_level=True, + ) + from tractor import ( current_actor, Actor, diff --git a/tests/test_root_infect_asyncio.py b/tests/test_root_infect_asyncio.py index e7a307bc..cde5577f 100644 --- a/tests/test_root_infect_asyncio.py +++ b/tests/test_root_infect_asyncio.py @@ -9,6 +9,17 @@ from functools import partial import pytest import trio import tractor + +# `infect_asyncio` mode is unsupported on Windows (see +# `test_infected_asyncio`); skip at COLLECTION before the +# asyncio-interop imports below so the CI leg completes. +import platform +if platform.system() == 'Windows': + pytest.skip( + 'infect_asyncio mode is unsupported on Windows', + allow_module_level=True, + ) + from tractor import ( to_asyncio, ) From 75385e448dc759f0d65cad3518835d5f8ceb8086 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 30 Jun 2026 02:46:00 -0400 Subject: [PATCH 06/10] Derive transport registries from one list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five transport/address lookup maps each hand-guarded `uds` with its own `if HAS_UDS:` block (3 in `ipc/_types`, 2 in `discovery/_addr`) — easy to let drift so a backend half-registers (known by address but not by key, listed but unroutable, &c). - build one `_msg_transports` / `_address_protos` list per module (TCP always, UDS only when `HAS_UDS`), then DERIVE every map from it via each backend's ClassVars (`codec_key`, `address_type`, `proto_key`). Adding a backend now touches one list, and the maps can't disagree. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/discovery/_addr.py | 43 +++++++++++++++++++++++++++----------- tractor/ipc/_types.py | 20 ++++++++++-------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index 1472349d..cf2f8a85 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -176,24 +176,36 @@ class Address(Protocol): ... -_address_types: dict[str, Type[Address]] = { - 'tcp': TCPAddress, -} +# the address types available on this host: TCP always, UDS only +# where usable (`HAS_UDS`). Both registries derive from this single +# list via each type's `proto_key`. +_address_protos: list[Type[Address]] = [TCPAddress] if HAS_UDS: - _address_types['uds'] = UDSAddress + _address_protos.append(UDSAddress) + +_address_types: dict[str, Type[Address]] = { + cls.proto_key: cls + for cls in _address_protos +} # TODO! really these are discovery sys default addrs ONLY useful for # when none is provided to a root actor on first boot. _default_lo_addrs: dict[str, UnwrappedAddress] = { - 'tcp': TCPAddress.get_root().unwrap(), + cls.proto_key: cls.get_root().unwrap() + for cls in _address_protos } -if HAS_UDS: - _default_lo_addrs['uds'] = UDSAddress.get_root().unwrap() def get_address_cls(name: str) -> Type[Address]: - return _address_types[name] + try: + return _address_types[name] + except KeyError: + raise NotImplementedError( + f'No IPC transport backend for {name!r} on this ' + f'platform!\n' + f'(available: {list(_address_types)})\n' + ) def is_wrapped_addr(addr: any) -> bool: @@ -291,7 +303,14 @@ def default_lo_addrs( for an input transport key set. ''' - return [ - _default_lo_addrs[transport] - for transport in transports - ] + lo_addrs: list[UnwrappedAddress] = [] + for transport in transports: + try: + lo_addrs.append(_default_lo_addrs[transport]) + except KeyError: + raise NotImplementedError( + f'No default loopback addr for transport ' + f'{transport!r} on this platform!\n' + f'(available: {list(_default_lo_addrs)})\n' + ) + return lo_addrs diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index f07d8cfa..50403d7a 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -41,26 +41,28 @@ from tractor.ipc._uds import ( # hosts `HAS_UDS` is `False` and the runtime registers TCP only. Address = TCPAddress|UDSAddress -# manually updated list of all supported msg transport types +# the available msg-transport backends on this host: TCP always, +# UDS only where usable (`HAS_UDS`). The lookup maps below are all +# DERIVED from this single list via each backend's ClassVars +# (`codec_key`, `address_type`) — register a backend here and every +# map picks it up; no per-map `if HAS_UDS` to keep in sync. _msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, ] if HAS_UDS: _msg_transports.append(MsgpackUDSStream) -# map a `MsgTransportKey` to its `MsgTransport` type +# map a `MsgTransportKey` -> `MsgTransport` type _key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { - ('msgpack', 'tcp'): MsgpackTCPStream, + (t.codec_key, t.address_type.proto_key): t + for t in _msg_transports } -if HAS_UDS: - _key_to_transport[('msgpack', 'uds')] = MsgpackUDSStream -# map an `Address`-wrapper to its `MsgTransport` type +# map an `Address`-wrapper -> `MsgTransport` type _addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { - TCPAddress: MsgpackTCPStream, + t.address_type: t + for t in _msg_transports } -if HAS_UDS: - _addr_to_transport[UDSAddress] = MsgpackUDSStream # ------------------------------------------------------------ From ca4582b003c5a24c292f2f90639ba035730a6852 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 30 Jun 2026 15:38:01 -0400 Subject: [PATCH 07/10] Skip Windows-hanging shm IPC test `test_parent_writer_child_reader` deadlocks on Windows (the parent/child shared-mem transfer hangs at the larger frame size), so the `windows-latest` CI leg ran to the 16-min job cap instead of completing. It's a genuine nascent-Windows shm bug, not a clean "unsupported", so it's `skipif`'d (not removed) and tracked under #404; `test_child_attaches_alot` still runs on Windows. - `@pytest.mark.skipif(platform.system() == 'Windows', ...)` on the parametrized `test_parent_writer_child_reader` so the leg completes + reports. linux/macOS unaffected (all 6 variants still run). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_shm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_shm.py b/tests/test_shm.py index 84d0988e..e63aef07 100644 --- a/tests/test_shm.py +++ b/tests/test_shm.py @@ -120,6 +120,13 @@ async def child_read_shm_list( print(f'(child): reading frame: {frame}') +@pytest.mark.skipif( + platform.system() == 'Windows', + reason=( + 'parent/child shm IPC deadlocks on Windows ' + '(frame-size dependent hang); nascent — see #404' + ), +) @pytest.mark.parametrize( 'use_str', [False, True], From 40be587ce256469ccca9a2752aa6c6777aaa3686 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 17 Aug 2026 12:09:52 -0400 Subject: [PATCH 08/10] Keep `HAS_UDS` false on Windows Modern Windows Python can expose `AF_UNIX`, so `trio.has_unix` alone can register the UDS backend even though its credential and lifecycle paths remain POSIX-only. Gate `HAS_UDS` on `sys.platform` and make the Windows import smoke step assert the TCP-only capability contract. (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .github/workflows/ci.yml | 2 +- tractor/ipc/_uds.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7871e346..0a973ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: # tractor` must succeed everywhere, and `HAS_UDS` reflects # platform capability (False on Windows, True on POSIX). - name: 'Smoke: import tractor' - run: uv run python -c "import tractor; from tractor.ipc._uds import HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" + run: uv run python -c "import sys; import tractor; from tractor.ipc._uds import HAS_UDS; assert sys.platform != 'win32' or not HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" - name: Run tests run: > diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 3d5a3c8e..262cc2e1 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -115,10 +115,12 @@ _SUN_PATH_LIMIT: int = ( # single source of truth for "is the UDS backend usable on this -# host?" — reuse `trio`'s `has_unix` (the same predicate that gates -# `trio.open_unix_socket()`) rather than re-deriving an `AF_UNIX` -# probe in every consumer module. -HAS_UDS: bool = has_unix +# host?" Windows can expose `AF_UNIX`, but this backend remains +# POSIX-only until its credential and lifecycle paths are supported. +HAS_UDS: bool = ( + sys.platform != 'win32' + and has_unix +) def unwrap_sockpath( From 359fe75ced677ce2bfaee054f99dc2f1ddbc243c Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 17 Aug 2026 17:04:20 -0400 Subject: [PATCH 09/10] Tolerate only Windows `pytest` failures Job-level `continue-on-error` made setup and import-smoke failures non-blocking even though the smoke is the hard Windows support signal. Move tolerance to the `pytest` step so the incomplete suite remains informational while install, dependency, and `HAS_UDS` smoke failures fail the job. When only the known suite fails, the job and PR rollup remain green with the failed step still visible. (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a973ff0..d07ebf49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,11 +91,10 @@ jobs: name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}' timeout-minutes: 16 runs-on: ${{ matrix.os }} - # Windows support is nascent: keep its legs informational so a - # known-incomplete area doesn't block merges. The `import - # tractor` smoke step below is the hard signal for this job; - # promote the whole leg to required once the suite is green. - continue-on-error: ${{ matrix.os == 'windows-latest' }} + # Windows support is nascent: its full test suite remains + # informational, while setup and the `import tractor` smoke below + # are hard signals. Promote the test step to required once the + # suite is green. strategy: fail-fast: false @@ -163,6 +162,7 @@ jobs: run: uv run python -c "import sys; import tractor; from tractor.ipc._uds import HAS_UDS; assert sys.platform != 'win32' or not HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" - name: Run tests + continue-on-error: ${{ matrix.os == 'windows-latest' }} run: > uv run pytest From 93322405a10c09c30cf899db8ce9379e0f6c0f71 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 17 Aug 2026 19:37:44 -0400 Subject: [PATCH 10/10] Restore IPC transport style conventions Bring the transport helpers back in line with project style: - restore single-quote strings and docstrings; - drop the oversized helper divider and simplify comments; - keep guarded `match` dispatch so a missing `socket.AF_UNIX` remains safe when `HAS_UDS` is false. Also, format the `HAS_UDS` conjunction with the project's multiline branch convention. Review: PR #475 (goodboy) https://github.com/goodboy/tractor/pull/475 Prompt-IO: ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .../20260817T231825Z_359fe75c_prompt_io.md | 39 +++++++++++ ...20260817T231825Z_359fe75c_prompt_io.raw.md | 45 ++++++++++++ tractor/ipc/_types.py | 68 ++++++++++--------- tractor/ipc/_uds.py | 7 +- 4 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md diff --git a/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md new file mode 100644 index 00000000..81173fed --- /dev/null +++ b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md @@ -0,0 +1,39 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: pr475-review-fixes-20260817 +timestamp: 2026-08-17T23:18:25Z +git_ref: 359fe75c +scope: code +substantive: true +raw_file: 20260817T231825Z_359fe75c_prompt_io.raw.md +--- + +## Prompt + +Continue the `/code-review-changes` pass for PR #475 in its isolated +worktree. Address the seven accepted manual-review findings in +`tractor/ipc/_types.py` and `tractor/ipc/_uds.py`, preserve the existing +Windows capability behavior, verify the result, and prepare the work for +human-controlled commit and review-reply steps. Do not publish replies, +stage, commit, or push without the required explicit authorization. + +## Response summary + +Restored project quote, docstring, multiline-expression, and +`match/case` conventions while retaining the Windows-safe UDS guard. +Removed unnecessary structural and comment churn, then verified the +focused transport, discovery, and lazy-import paths plus the missing +`AF_UNIX` behavior. + +## Files changed + +- `tractor/ipc/_types.py` - restore project style and guarded + socket-family dispatch. +- `tractor/ipc/_uds.py` - format the UDS capability gate + consistently. + +## Human edits + +None - the generated patch remains uncommitted and awaits human +review. diff --git a/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md new file mode 100644 index 00000000..f9a2dcdd --- /dev/null +++ b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md @@ -0,0 +1,45 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-17T23:18:25Z +git_ref: 359fe75c +diff_cmd: git diff HEAD~1..HEAD +--- + +Applied the seven accepted manual-review fixes for PR #475 while +preserving the Windows transport capability behavior. + +> `git diff HEAD~1..HEAD -- tractor/ipc/_types.py` + +The generated changes restore the project's single-quote docstring and +string conventions, remove the unnecessary helper divider, simplify the +transport-registry comments, and restore `match/case` socket-family +dispatch. The UDS case retains a `HAS_UDS` guard that short-circuits +before `socket.AF_UNIX` is evaluated on unsupported hosts. Nearby error +messages are wrapped without changing their content. + +> `git diff HEAD~1..HEAD -- tractor/ipc/_uds.py` + +The generated change reformats the `HAS_UDS` conjunction according to +the project's multiline boolean-expression convention and simplifies +the adjacent capability comment. + +Verification: + +`/home/goodboy/repos/tractor/py313/bin/pytest -q tests/test_lazy_imports.py tests/discovery tests/ipc/test_server.py` + +Result: `66 passed, 2 xpassed in 60.62s`. + +`ruff check --no-cache --output-format=json tractor/ipc/_types.py tractor/ipc/_uds.py` + +Result: no findings. + +`git diff --check` + +Result: no whitespace errors. + +An explicit missing-`AF_UNIX` probe set `HAS_UDS = False`, removed the +socket constant, and exercised an unsupported socket family. It raised +the expected `NotImplementedError` instead of `AttributeError`. + +No review replies, commits, or pushes were published. diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index 50403d7a..244da48b 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -36,16 +36,14 @@ from tractor.ipc._uds import ( HAS_UDS, ) -# the UDS backend is importable everywhere but only *usable* where -# `trio` reports `has_unix` (i.e. POSIX). On Windows / no-`AF_UNIX` -# hosts `HAS_UDS` is `False` and the runtime registers TCP only. +# the UDS backend is importable everywhere but only *usable* when +# `HAS_UDS` is `True`; otherwise the runtime registers TCP only. Address = TCPAddress|UDSAddress # the available msg-transport backends on this host: TCP always, -# UDS only where usable (`HAS_UDS`). The lookup maps below are all -# DERIVED from this single list via each backend's ClassVars -# (`codec_key`, `address_type`) — register a backend here and every -# map picks it up; no per-map `if HAS_UDS` to keep in sync. +# UDS only where usable (`HAS_UDS`). The lookup maps below derive +# from this single list via each backend's `codec_key` and +# `address_type`: register a backend here and every map picks it up. _msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, ] @@ -65,55 +63,63 @@ _addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { } -# ------------------------------------------------------------ -# Helpers -# ------------------------------------------------------------ def transport_from_addr( addr: Address, - codec_key: str = "msgpack", + codec_key: str = 'msgpack', ) -> Type[MsgTransport]: - """ + ''' Given a destination address and a desired codec, find the corresponding `MsgTransport` type. - """ + + ''' try: - return _addr_to_transport[type(addr)] # type: ignore[call-arg] + addr_type = type(addr) + return _addr_to_transport[addr_type] + except KeyError: raise NotImplementedError( - f"No known transport for address {repr(addr)}" + f'No known transport for address ' + f'{addr!r}' ) def transport_from_stream( stream: trio.abc.Stream, - codec_key: str = "msgpack", + codec_key: str = 'msgpack', ) -> Type[MsgTransport]: - """ + ''' Given an arbitrary `trio.abc.Stream` and a desired codec, find the corresponding `MsgTransport` type. - """ - transport = None + + ''' + transport: str|None = None if isinstance(stream, trio.SocketStream): sock: socket.socket = stream.socket - fam = sock.family + match sock.family: + case socket.AF_INET | socket.AF_INET6: + transport = 'tcp' - if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)): - transport = "tcp" + # `HAS_UDS` short-circuits before `socket.AF_UNIX` on + # hosts where that constant is absent. + case fam if ( + HAS_UDS + and + fam == socket.AF_UNIX + ): + transport = 'uds' - # only consider `AF_UNIX` when the UDS backend is active; - # `HAS_UDS` short-circuits before `socket.AF_UNIX` so this - # stays safe on hosts where that constant is absent. - if transport is None and HAS_UDS and fam == socket.AF_UNIX: # type: ignore[attr-defined] - transport = "uds" - - if transport is None: - raise NotImplementedError(f"Unsupported socket family: {fam}") + case fam: + raise NotImplementedError( + f'Unsupported socket family: {fam}' + ) if not transport: raise NotImplementedError( - f"Could not figure out transport type for stream type {type(stream)}" + f'Could not figure out transport type for stream type ' + f'{type(stream)}' ) key = (codec_key, transport) + return _key_to_transport[key] diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 262cc2e1..49a9322d 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -114,12 +114,13 @@ _SUN_PATH_LIMIT: int = ( ) -# single source of truth for "is the UDS backend usable on this -# host?" Windows can expose `AF_UNIX`, but this backend remains +# single source of truth for whether the UDS backend is usable on this +# host. Windows can expose `AF_UNIX`, but this backend remains # POSIX-only until its credential and lifecycle paths are supported. HAS_UDS: bool = ( sys.platform != 'win32' - and has_unix + and + has_unix )