diff --git a/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.md b/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.md
new file mode 100644
index 00000000..d87a7c6a
--- /dev/null
+++ b/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.md
@@ -0,0 +1,70 @@
+---
+model: openai/gpt-5.6-sol
+service: opencode
+session: dependency-neutral-discovery-types
+timestamp: 2026-09-03T20:15:30Z
+git_ref: d097090f
+scope: code
+substantive: true
+raw_file: 20260903T201530Z_d097090f_discovery_types.raw.md
+---
+
+## Prompt
+
+The maintainer rejected annotation-only model names rebound to `Any`,
+questioned the self-referential `get_type_hints()` regression, selected a
+dependency-neutral `tractor.discovery.types` module as the preferred
+direction, and then requested implementation of that first design layer.
+
+The maintainer separately asked how to handle the disliked staged
+`tractor/net/_bootstrap.py` implementation. That file was excluded from
+this implementation boundary and retained as review evidence.
+
+## Response summary
+
+Added a dependency-neutral module for canonical primitive address
+descriptors and separate structural contracts for concrete transport
+addresses and tunnel-capable address declarations. Migrated discovery,
+IPC, root, runtime, and tunnel annotations to import those real types,
+removing every runtime model-name assignment to `Any`.
+
+Kept parser table aliases beside `parse_endpoints()` instead of turning
+the new module into a general alias registry. Kept transport-specific
+factories on TCP and UDS classes because their accepted descriptor and
+bindspace types differ. Replaced the old `get_type_hints()` assertions
+with cold-import and canonical-identity coverage.
+
+## Files changed
+
+- `tractor/discovery/types.py` - dependency-neutral descriptors and
+ address protocols.
+- `tractor/discovery/_addr.py` - imports and re-exports canonical types;
+ retains concrete factory registry.
+- `tractor/discovery/_multiaddr.py` - uses the structural declaration
+ type and runtime-safe parsed table alias.
+- `tractor/ipc/_tcp.py` - imports the real TCP descriptor alias.
+- `tractor/ipc/_uds.py` - imports the real Unix descriptor alias.
+- `tractor/ipc/_transport.py` - imports the concrete-address protocol
+ without cycling through `_addr`.
+- `tractor/ipc/_chan.py` - accepts structural address declarations
+ without a `TunnelledAddress = Any` fallback.
+- `tractor/ipc/_server.py` - retains declaration metadata structurally
+ and routes reconstruction through the concrete address registry.
+- `tractor/net/_tunnel.py` - imports real dependency-neutral address
+ types while retaining dynamic `Any` only for external netlink data.
+- `tractor/_root.py` - removes the `Bindspace = Any` fallback and uses
+ concrete address-factory dispatch.
+- `tractor/runtime/_runtime.py` - infers concrete registry address types
+ for random listener allocation.
+- `tests/test_lazy_imports.py` - replaces workaround introspection with
+ dependency and canonical-identity checks.
+
+## Human edits
+
+The maintainer identified the `Any` aliases and their introspection test
+as an unjustified circular contract, directed investigation of the real
+cycle/import-performance cause, selected `tractor.discovery.types`, and
+approved beginning implementation. The maintainer also required that the
+current bootstrap implementation not be treated as acceptable design and
+provided direct review annotations which remain preserved in the
+worktree.
diff --git a/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.raw.md b/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.raw.md
new file mode 100644
index 00000000..a0d641d7
--- /dev/null
+++ b/ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.raw.md
@@ -0,0 +1,34 @@
+---
+model: openai/gpt-5.6-sol
+service: opencode
+timestamp: 2026-09-03T20:15:30Z
+git_ref: d097090f
+diff_cmd: git diff HEAD~1..HEAD
+---
+
+Implemented a dependency-neutral discovery typing layer after reviewing
+the address, transport, tunnel, and lazy-import dependency graph.
+
+> `git diff HEAD~1..HEAD -- tractor/discovery/types.py tractor/discovery/_addr.py tractor/discovery/_multiaddr.py tractor/ipc/_tcp.py tractor/ipc/_uds.py tractor/ipc/_transport.py tractor/ipc/_chan.py tractor/ipc/_server.py tractor/net/_tunnel.py tractor/_root.py tractor/runtime/_runtime.py tests/test_lazy_imports.py`
+
+The generated changes move primitive address descriptors and structural
+address protocols into `tractor.discovery.types`. Concrete transport
+factories remain outside the shared protocol because TCP and UDS accept
+different descriptor and bindspace inputs. Runtime names are no longer
+rebound to `Any` merely to satisfy `typing.get_type_hints()`.
+
+The old annotation-introspection regression was replaced with coverage
+that checks dependency-neutral cold imports and canonical type identity
+across discovery, IPC, and tunnel modules.
+
+Verification completed with the worktree-local Python 3.13 environment:
+
+- focused address/lazy/tunnel coverage: 69 passed;
+- discovery, IPC, and local coverage: 77 passed, 2 xpassed;
+- root/runtime coverage: 6 passed with 24 multiprocessing fork
+ deprecation warnings.
+
+Ruff was unavailable in the existing `py313` environment. Scoped
+`git diff --check` passed. The global whitespace check reports only a
+pre-existing maintainer annotation in `tractor/net/_bootstrap.py`, which
+was deliberately left untouched.
diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py
index b2b61f35..02361544 100644
--- a/tests/test_lazy_imports.py
+++ b/tests/test_lazy_imports.py
@@ -7,21 +7,6 @@ import os
from statistics import median
import subprocess
import sys
-from typing import (
- Any,
- get_type_hints,
-)
-
-import tractor
-from tractor.discovery import (
- _addr,
- _multiaddr,
-)
-from tractor.ipc import (
- _tcp,
- _uds,
-)
-
def run_cold_import(code: str) -> dict[str, object]:
result = subprocess.run(
@@ -260,28 +245,56 @@ def test_net_root_export_and_old_discovery_surface():
}
-def test_lazy_annotation_names_resolve():
+def test_discovery_types_are_dependency_neutral():
'''
- Resolve annotations without importing optional dependencies.
+ Keep canonical address types independent of runtime implementations.
- Moving annotation-only third-party names under `TYPE_CHECKING`
- left their runtime globals undefined, causing
- `typing.get_type_hints()` to raise `NameError`. Resolve every
- affected API and prove the lazy aliases retain import-free runtime
- introspection.
+ Annotation-only imports previously rebound unavailable model names
+ to `Any` so `get_type_hints()` could resolve them. That test only
+ verified its own workaround and hid the `_addr`/transport import
+ cycle. Import the canonical declarations in a cold interpreter and
+ prove they do not pull in optional networking modules. Then prove
+ each implementation imports the same real declarations instead of
+ substituting `Any` under the model names.
'''
- assert get_type_hints(_multiaddr.mk_maddr)['return'] is Any
- assert get_type_hints(_tcp.MsgpackTCPStream.maddr.fget)[
- 'return'
- ] is Any
- assert get_type_hints(_uds.MsgpackUDSStream.maddr.fget)[
- 'return'
- ] == Any|str
- assert get_type_hints(_addr.Address.get_random)[
- 'current_actor'
- ] is Any
- assert get_type_hints(tractor.open_root_actor)[
- 'bindspace'
- ] == Any|None
- assert _addr.__annotations__['_address_types'].startswith('dict')
+ cold = run_cold_import(
+ 'import json, sys; '
+ 'from tractor.discovery import types; '
+ 'blocked = ('
+ '"multiaddr", "multibase", "pyroute2", '
+ '"tractor.discovery._multiaddr", "tractor.net", '
+ '"tractor.net._bindspace", "tractor.net._tunnel"); '
+ 'print(json.dumps({'
+ '"address_module": types.Address.__module__, '
+ '"blocked": [name for name in blocked '
+ 'if name in sys.modules]}))'
+ )
+ assert cold == {
+ 'address_module': 'tractor.discovery.types',
+ 'blocked': [],
+ }
+
+ from tractor.discovery import (
+ _addr,
+ _multiaddr,
+ types,
+ )
+ from tractor.ipc import (
+ _chan,
+ _server,
+ _tcp,
+ _transport,
+ _uds,
+ )
+ from tractor.net import _tunnel
+
+ assert _addr.Address is types.Address
+ assert _addr.UnwrappedAddress is types.UnwrappedAddress
+ assert _multiaddr.AddressDeclaration is types.AddressDeclaration
+ assert _tcp.TaggedTCPAddress is types.TaggedTCPAddress
+ assert _uds.TaggedUnixAddress is types.TaggedUnixAddress
+ assert _transport.Address is types.Address
+ assert _chan.AddressDeclaration is types.AddressDeclaration
+ assert _server.AddressDeclaration is types.AddressDeclaration
+ assert _tunnel.AddressDeclaration is types.AddressDeclaration
diff --git a/tractor/_root.py b/tractor/_root.py
index 13c9132a..5c48f42e 100644
--- a/tractor/_root.py
+++ b/tractor/_root.py
@@ -53,12 +53,15 @@ from .runtime import _state
from . import log
from .discovery._api import _probe_registry_addrs
from .discovery._addr import (
- Address,
- UnwrappedAddress,
default_lo_addrs,
+ get_address_cls,
mk_uuid,
wrap_address,
)
+from .discovery.types import (
+ Address,
+ UnwrappedAddress,
+)
from .trionics import (
is_multi_cancelled,
collapse_eg,
@@ -69,8 +72,6 @@ from ._exceptions import (
if TYPE_CHECKING:
from .net._bindspace import Bindspace
-else:
- Bindspace = Any
logger = log.get_logger('tractor')
@@ -187,7 +188,7 @@ async def open_root_actor(
bindspace: Bindspace|None = None,
tpt_bind_addrs: list[
- Address # `Address.get_random()` case
+ Address # concrete transport address case
|UnwrappedAddress # registrar case `= uw_reg_addrs`
]|None = None,
@@ -561,6 +562,9 @@ async def open_root_actor(
for addr in ponged_addrs:
bindable_addr: Address = strip_tunnels(addr)
+ address_type = get_address_cls(
+ bindable_addr.proto_key
+ )
tpt_bind_addrs.append(
# XXX, these are `Address` NOT `UnwrappedAddress`.
#
@@ -568,7 +572,7 @@ async def open_root_actor(
# protos we allocate port=0 such that the system
# allocates a random value at bind time; this
# happens in the `.ipc.*` stack's backend.
- bindable_addr.get_random(
+ address_type.get_random(
bindspace=bindable_addr.bindspace,
)
)
diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py
index ed080690..a7ea862f 100644
--- a/tractor/discovery/_addr.py
+++ b/tractor/discovery/_addr.py
@@ -15,19 +15,7 @@
# along with this program. If not, see .
from __future__ import annotations
from uuid import uuid4
-from typing import (
- Any,
- Protocol,
- ClassVar,
- Literal,
- Type,
- TypeAlias,
- TYPE_CHECKING,
-)
-
-from trio import (
- SocketListener,
-)
+from typing import TypeAlias
from ..log import get_logger
from ..runtime._state import (
@@ -38,70 +26,19 @@ from ..ipc._uds import (
UDSAddress,
HAS_UDS,
)
-
-if TYPE_CHECKING:
- # ONLY type-annots, the eager import costs ~4.5ms
- # of `import tractor` wall-time (gh #470).
- from tractor.net._tunnel import (
- TunnelledAddress,
- )
- from ..runtime._runtime import Actor
-else:
- Actor = Any
- TunnelledAddress = Any
+from .types import (
+ Address as Address,
+ AddressDeclaration,
+ LegacyUnwrappedAddress,
+ TaggedAddress,
+ TaggedUDSAlias,
+ UnwrappedAddress,
+)
log = get_logger()
-# TODO, maybe breakout the netns key to a struct?
-# class NetNs(Struct)[str, int]:
-# ...
+_AddressType: TypeAlias = type[TCPAddress]|type[UDSAddress]
-# TODO, can't we just use a type alias
-# for this? namely just some `tuple[str, int, str, str]`?
-#
-# -[ ] would also just be simpler to keep this as SockAddr[tuple]
-# or something, implying it's just a simple pair of values which can
-# presumably be mapped to all transports?
-# -[ ] `pydoc socket.socket.getsockname()` delivers a 4-tuple for
-# ipv6 `(hostaddr, port, flowinfo, scope_id)`.. so how should we
-# handle that?
-# -[ ] as a further alternative to this wrap()/unwrap() approach we
-# could just implement `enc/dec_hook()`s for the `Address`-types
-# and just deal with our internal objs directly and always and
-# leave it to the codec layer to figure out marshalling?
-# |_ would mean only one spot to do the `.unwrap()` (which we may
-# end up needing to call from the hook()s anyway?)
-# -[x] rename to `UnwrappedAddress[Descriptor]` ??
-# seems like the right name as per,
-# https://www.geeksforgeeks.org/introduction-to-address-descriptor/
-#
-TaggedTCPAddress: TypeAlias = tuple[
- Literal['tcp'],
- str,
- int,
-]
-TaggedUnixAddress: TypeAlias = tuple[
- Literal['unix'],
- str,
-]
-TaggedUDSAlias: TypeAlias = tuple[
- Literal['uds'],
- str,
-]
-TaggedAddress: TypeAlias = (
- TaggedTCPAddress
- |TaggedUnixAddress
-)
-
-# Input-only compatibility forms retained for older callers and
-# serialized payloads.
-LegacyTCPAddress: TypeAlias = tuple[str, int]
-LegacyUDSAddress: TypeAlias = tuple[str, str]
-LegacyUnwrappedAddress: TypeAlias = (
- LegacyTCPAddress
- |LegacyUDSAddress
-)
-UnwrappedAddress = TaggedAddress
# ?TODO? should we also include another 2 fields from our `Aid` msg
# such that we include the runtime `Actor.uid` of `.name` and `.uuid`?
# - would ensure uniqueness across entire net?
@@ -109,106 +46,14 @@ UnwrappedAddress = TaggedAddress
# name"
-# TODO, maybe rename to `SocketAddress`?
-class Address(Protocol):
- proto_key: ClassVar[str]
- unwrapped_type: ClassVar[type]
-
- # TODO, i feel like an `.is_bound()` is a better thing to
- # support?
- # Lke, what use does this have besides a noop and if it's not
- # valid why aren't we erroring on creation/use?
- @property
- def is_valid(self) -> bool:
- ...
-
- # TODO, maybe `.netns` is a better name?
- @property
- def namespace(self) -> tuple[str, str|int]|None:
- '''
- The if-available, OS-specific "network namespace" key.
-
- '''
- ...
-
- @property
- def bindspace(self) -> str:
- '''
- Deliver the socket address' "bindable space" from
- a `socket.socket.bind()` and thus from the perspective of
- specific transport protocol domain.
-
- I.e. for most (layer-4) network-socket protocols this is
- normally the ipv4/6 address, for UDS this is normally
- a filesystem (sub-directory).
-
- For (distributed) network protocols this is normally the routing
- layer's domain/(ip-)address, though it might also include a "network namespace"
- key different then the default.
-
- For local-host-only transports this is either an explicit
- namespace (with types defined by the OS: netns, Cgroup, IPC,
- pid, etc. on linux) or failing that the sub-directory in the
- filesys in which socket/shm files are located *under*.
-
- '''
- ...
-
- @classmethod
- def from_addr(cls, addr: UnwrappedAddress) -> Address:
- ...
-
- def unwrap(self) -> UnwrappedAddress:
- '''
- Deliver the underying minimum field set in
- a primitive python data type-structure.
- '''
- ...
-
- @classmethod
- def get_random(
- cls,
- current_actor: Actor,
- bindspace: str|None = None,
- ) -> Address:
- ...
-
- # TODO, this should be something like a `.get_def_registar_addr()`
- # or similar since,
- # - it should be a **host singleton** (not root/tree singleton)
- # - we **only need this value** when one isn't provided to the
- # runtime at boot and we want to implicitly provide a host-wide
- # registrar.
- # - each rooted-actor-tree should likely have its own
- # micro-registry (likely the root being it), also see
- @classmethod
- def get_root(cls) -> Address:
- ...
-
- def __repr__(self) -> str:
- ...
-
- def __eq__(self, other) -> bool:
- ...
-
- async def open_listener(
- self,
- **kwargs,
- ) -> SocketListener:
- ...
-
- async def close_listener(self):
- ...
-
-
# 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]
+_address_protos: list[_AddressType] = [TCPAddress]
if HAS_UDS:
_address_protos.append(UDSAddress)
-_address_types: dict[str, Type[Address]] = {
+_address_types: dict[str, _AddressType] = {
cls.proto_key: cls
for cls in _address_protos
}
@@ -216,13 +61,22 @@ _address_types: dict[str, Type[Address]] = {
# TODO! really these are discovery sys default addrs ONLY useful for
# when none is provided to a root actor on first boot.
+#
+# TODO, this should be something like a `.get_def_registar_addr()`
+# or similar since,
+# - it should be a **host singleton** (not root/tree singleton)
+# - we **only need this value** when one isn't provided to the
+# runtime at boot and we want to implicitly provide a host-wide
+# registrar.
+# - each rooted-actor-tree should likely have its own
+# micro-registry (likely the root being it), also see
_default_lo_addrs: dict[str, UnwrappedAddress] = {
cls.proto_key: cls.get_root().unwrap()
for cls in _address_protos
}
-def get_address_cls(name: str) -> Type[Address]:
+def get_address_cls(name: str) -> _AddressType:
try:
return _address_types[name]
except KeyError:
@@ -233,7 +87,7 @@ def get_address_cls(name: str) -> Type[Address]:
)
-def is_wrapped_addr(addr: any) -> bool:
+def is_wrapped_addr(addr: object) -> bool:
# XXX NOTE, a `TunnelledAddress` is genuinely "wrapped" but is
# deliberately NOT in `_address_types`: it has no
# `MsgTransport` of its own (a tunnel is transparent to
@@ -264,10 +118,9 @@ def wrap_address(
|LegacyUnwrappedAddress
|list[str|int]
|str
- |Address
- |TunnelledAddress
+ |AddressDeclaration
),
-) -> Address|TunnelledAddress:
+) -> AddressDeclaration:
'''
Wrap an `UnwrappedAddress` as an `Address`-type based
on matching builtin python data-structures which we adhoc
@@ -284,7 +137,7 @@ def wrap_address(
if is_wrapped_addr(addr):
return addr
- cls: Type|None = None
+ cls: _AddressType|None = None
# if 'sock' in addr[0]:
# import pdbp; pdbp.set_trace()
match addr:
@@ -328,7 +181,7 @@ def wrap_address(
|
[None, None]
):
- cls: Type[Address] = get_address_cls(_def_tpt_proto)
+ cls = get_address_cls(_def_tpt_proto)
addr: UnwrappedAddress = cls.get_root().unwrap()
# multiaddr-format string, e.g.
@@ -354,7 +207,7 @@ def wrap_address(
def default_lo_addrs(
transports: list[str],
-) -> list[Type[Address]]:
+) -> list[UnwrappedAddress]:
'''
Return the default, host-singleton, registry address
for an input transport key set.
diff --git a/tractor/discovery/_multiaddr.py b/tractor/discovery/_multiaddr.py
index 2fd7899d..b10a97a3 100644
--- a/tractor/discovery/_multiaddr.py
+++ b/tractor/discovery/_multiaddr.py
@@ -27,24 +27,15 @@ Multiaddress support using the upstream `py-multiaddr` lib
from __future__ import annotations
import ipaddress
from pathlib import Path
-from typing import (
- Any,
- TYPE_CHECKING,
-)
+from typing import TYPE_CHECKING
+
+from .types import AddressDeclaration
if TYPE_CHECKING:
# NOTE, `multiaddr` is lazy-imported at first use
# (in the fns below) to keep it off the eager
# `import tractor` path (gh #470).
from multiaddr import Multiaddr
- from tractor.discovery._addr import Address
- from tractor.net._tunnel import (
- TunnelledAddress,
- )
-else:
- Multiaddr = Any
- Address = Any
- TunnelledAddress = Any
# map from tractor-internal `proto_key` identifiers
# to the standard multiaddr protocol name strings.
@@ -61,7 +52,7 @@ _maddr_to_tpt_proto: dict[str, str] = {
def mk_maddr(
- addr: 'Address|TunnelledAddress',
+ addr: AddressDeclaration,
) -> Multiaddr:
'''
Construct a `Multiaddr` from a tractor `Address` instance,
@@ -111,7 +102,7 @@ def mk_maddr(
def parse_maddr(
maddr_str: str,
-) -> 'Address|TunnelledAddress':
+) -> AddressDeclaration:
'''
Parse a multiaddr string into a tractor `Address`.
@@ -173,15 +164,15 @@ def parse_maddr(
# or raw unwrapped-address tuples (as accepted by
# `wrap_address()`).
EndpointsTable = dict[
- str, # actor/service name
- list[str|tuple], # maddr strs or UnwrappedAddress
+ str, # actor/service name
+ list[str|tuple|AddressDeclaration],
]
# output table: actor/service name -> list of wrapped address
# declarations ready for bindspace handling.
ParsedEndpoints = dict[
- str, # actor/service name
- list['Address|TunnelledAddress'],
+ str, # actor/service name
+ list[AddressDeclaration],
]
diff --git a/tractor/discovery/types.py b/tractor/discovery/types.py
new file mode 100644
index 00000000..571f8e58
--- /dev/null
+++ b/tractor/discovery/types.py
@@ -0,0 +1,176 @@
+# tractor: structured concurrent "actors".
+# Copyright 2018-eternity Tyler Goodlet.
+
+# This program is free software: you can redistribute it and/or
+# modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# .
+
+'''
+Dependency-neutral address typing declarations.
+
+Keep this module independent of concrete IPC transports, actor
+runtime models, network resources, tunnels, and optional
+dependencies.
+
+'''
+from __future__ import annotations
+
+from pathlib import Path
+from typing import (
+ ClassVar,
+ Literal,
+ Protocol,
+ TypeAlias,
+)
+
+
+# TODO, maybe breakout the netns key to a struct?
+# class NetNs(Struct)[str, int]:
+# ...
+
+# TODO, can't we just use a type alias
+# for this? namely just some `tuple[str, int, str, str]`?
+#
+# -[ ] would also just be simpler to keep this as
+# SockAddr[tuple] or something, implying it's just a simple pair
+# of values which can presumably be mapped to all transports?
+# -[ ] `pydoc socket.socket.getsockname()` delivers a 4-tuple for
+# ipv6 `(hostaddr, port, flowinfo, scope_id)`.. so how should we
+# handle that?
+# -[ ] as a further alternative to this wrap()/unwrap() approach we
+# could just implement `enc/dec_hook()`s for the `Address`-types
+# and just deal with our internal objs directly and always and
+# leave it to the codec layer to figure out marshalling?
+# |_ would mean only one spot to do the `.unwrap()` (which we may
+# end up needing to call from the hook()s anyway?)
+# -[x] rename to `UnwrappedAddress[Descriptor]` ??
+# seems like the right name as per the GeeksForGeeks article,
+# "Introduction to Address Descriptor".
+#
+TaggedTCPAddress: TypeAlias = tuple[
+ Literal['tcp'],
+ str,
+ int,
+]
+TaggedUnixAddress: TypeAlias = tuple[
+ Literal['unix'],
+ str,
+]
+TaggedUDSAlias: TypeAlias = tuple[
+ Literal['uds'],
+ str,
+]
+TaggedAddress: TypeAlias = (
+ TaggedTCPAddress
+ |TaggedUnixAddress
+)
+
+# Input-only compatibility forms retained for older callers and
+# serialized payloads.
+LegacyTCPAddress: TypeAlias = tuple[str, int]
+LegacyUDSAddress: TypeAlias = tuple[str, str]
+LegacyUnwrappedAddress: TypeAlias = (
+ LegacyTCPAddress
+ |LegacyUDSAddress
+)
+UnwrappedAddress = TaggedAddress
+
+
+class AddressDeclaration(Protocol):
+ '''
+ Common instance shape of plain and tunnelled addresses.
+
+ This protocol deliberately excludes transport registration and
+ listener operations. A tunnel declaration delegates these address
+ properties to its innermost concrete transport address.
+
+ '''
+ @property
+ def proto_key(self) -> str:
+ ...
+
+ @property
+ def is_valid(self) -> bool:
+ ...
+
+ @property
+ def namespace(self) -> tuple[str, str|int]|None:
+ ...
+
+ @property
+ def bindspace(self) -> str|Path:
+ ...
+
+ def unwrap(self) -> UnwrappedAddress:
+ ...
+
+
+# TODO, maybe rename to `SocketAddress`?
+class Address(Protocol):
+ '''
+ Concrete address contract used by IPC transport backends.
+
+ Unlike `AddressDeclaration`, transport registries require
+ class-level protocol metadata. Transport-specific factories stay
+ on their concrete address classes because their descriptor and
+ bindspace inputs differ.
+
+ '''
+ proto_key: ClassVar[str]
+ unwrapped_type: ClassVar[type]
+
+ # TODO, i feel like an `.is_bound()` is a better thing to
+ # support?
+ # Lke, what use does this have besides a noop and if it's not
+ # valid why aren't we erroring on creation/use?
+ @property
+ def is_valid(self) -> bool:
+ ...
+
+ # TODO, maybe `.netns` is a better name?
+ @property
+ def namespace(self) -> tuple[str, str|int]|None:
+ '''
+ The if-available, OS-specific "network namespace" key.
+
+ '''
+ ...
+
+ @property
+ def bindspace(self) -> str|Path:
+ '''
+ Deliver the address' transport-specific bindable space.
+
+ '''
+ ...
+
+ def unwrap(self) -> UnwrappedAddress:
+ '''
+ Deliver the underlying primitive address descriptor.
+
+ '''
+ ...
+
+__all__ = (
+ 'Address',
+ 'AddressDeclaration',
+ 'LegacyTCPAddress',
+ 'LegacyUDSAddress',
+ 'LegacyUnwrappedAddress',
+ 'TaggedAddress',
+ 'TaggedTCPAddress',
+ 'TaggedUDSAlias',
+ 'TaggedUnixAddress',
+ 'UnwrappedAddress',
+)
diff --git a/tractor/ipc/_chan.py b/tractor/ipc/_chan.py
index 13aa0244..750ff1c6 100644
--- a/tractor/ipc/_chan.py
+++ b/tractor/ipc/_chan.py
@@ -43,7 +43,10 @@ from ._types import (
from tractor.discovery._addr import (
is_wrapped_addr,
wrap_address,
+)
+from tractor.discovery.types import (
Address,
+ AddressDeclaration,
UnwrappedAddress,
)
from tractor.log import get_logger
@@ -59,9 +62,6 @@ from tractor.msg import (
if TYPE_CHECKING:
from ._transport import MsgTransport
- from tractor.net._tunnel import TunnelledAddress
-else:
- TunnelledAddress = Any
log = get_logger()
@@ -185,7 +185,7 @@ class Channel:
@classmethod
async def from_addr(
cls,
- addr: UnwrappedAddress|Address|TunnelledAddress,
+ addr: UnwrappedAddress|AddressDeclaration,
**kwargs
) -> Channel:
@@ -557,7 +557,7 @@ class Channel:
@acm
async def _connect_chan(
- addr: UnwrappedAddress|Address|TunnelledAddress,
+ addr: UnwrappedAddress|AddressDeclaration,
close_timeout: float|None = None,
) -> typing.AsyncGenerator[Channel, None]:
'''
diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py
index 84f5ebc1..dd3d8b69 100644
--- a/tractor/ipc/_server.py
+++ b/tractor/ipc/_server.py
@@ -59,8 +59,10 @@ from ..msg import (
from ..trionics import maybe_open_nursery
from ..runtime import _state
from .. import log
-from ..discovery._addr import (
+from ..discovery._addr import get_address_cls
+from ..discovery.types import (
Address,
+ AddressDeclaration,
UnwrappedAddress,
)
from ._chan import Channel
@@ -68,7 +70,6 @@ from ._transport import MsgTransport
if TYPE_CHECKING:
- from ..net._tunnel import TunnelledAddress
from ..runtime._runtime import Actor
from ..runtime._supervise import ActorNursery
@@ -631,7 +632,7 @@ class Endpoint(Struct):
'''
addr: Address
- declared_addr: Address|TunnelledAddress
+ declared_addr: AddressDeclaration
listen_tn: Nursery
stream_handler_tn: Nursery|None = None
@@ -689,7 +690,8 @@ class Endpoint(Struct):
!=
self.addr.unwrap()
):
- self.addr=self.addr.from_addr(unwrapped)
+ address_type = get_address_cls(self.addr.proto_key)
+ self.addr = address_type.from_addr(unwrapped)
self._listener = lstnr
return lstnr
@@ -991,7 +993,7 @@ class Server(Struct):
self,
*,
accept_addrs: list[
- UnwrappedAddress|Address|TunnelledAddress
+ UnwrappedAddress|AddressDeclaration
]|None = None,
stream_handler_nursery: Nursery|None = None,
) -> list[Endpoint]:
@@ -1075,7 +1077,7 @@ async def _serve_ipc_eps(
*,
server: IPCServer,
stream_handler_tn: Nursery,
- listen_addrs: list[Address|TunnelledAddress],
+ listen_addrs: list[AddressDeclaration],
task_status: TaskStatus[
Nursery,
diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py
index 40cb8e81..fe3e525e 100644
--- a/tractor/ipc/_tcp.py
+++ b/tractor/ipc/_tcp.py
@@ -20,7 +20,6 @@ TCP implementation of tractor.ipc._transport.MsgTransport protocol
from __future__ import annotations
import ipaddress
from typing import (
- Any,
ClassVar,
TYPE_CHECKING,
)
@@ -41,15 +40,12 @@ from tractor.ipc._transport import (
MsgTransport,
MsgpackTransport,
)
+from tractor.discovery.types import TaggedTCPAddress
if TYPE_CHECKING:
# ONLY type-annots, the eager import costs
# `import tractor` wall-time (gh #470).
from multiaddr import Multiaddr
- from tractor.discovery._addr import TaggedTCPAddress
-else:
- Multiaddr = Any
- TaggedTCPAddress = Any
log = get_logger()
diff --git a/tractor/ipc/_transport.py b/tractor/ipc/_transport.py
index 018b70c4..5afdde27 100644
--- a/tractor/ipc/_transport.py
+++ b/tractor/ipc/_transport.py
@@ -55,9 +55,7 @@ from tractor.msg import (
types as msgtypes,
pretty_struct,
)
-
-if TYPE_CHECKING:
- from tractor.discovery._addr import Address
+from tractor.discovery.types import Address
log = get_logger()
diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py
index cfba9519..387733d0 100644
--- a/tractor/ipc/_uds.py
+++ b/tractor/ipc/_uds.py
@@ -43,7 +43,6 @@ except ImportError:
AF_UNIX = None
import struct
from typing import (
- Any,
Type,
TYPE_CHECKING,
ClassVar,
@@ -71,17 +70,13 @@ from tractor.runtime._state import (
current_actor,
is_root_process,
)
+from tractor.discovery.types import TaggedUnixAddress
if TYPE_CHECKING:
# ONLY type-annots, the eager import costs
# `import tractor` wall-time (gh #470).
from multiaddr import Multiaddr
- from tractor.discovery._addr import TaggedUnixAddress
from tractor.runtime._runtime import Actor
-else:
- Multiaddr = Any
- Actor = Any
- TaggedUnixAddress = Any
# Platform-specific credential passing constants
diff --git a/tractor/net/_tunnel.py b/tractor/net/_tunnel.py
index 34dfe79a..c76e64eb 100644
--- a/tractor/net/_tunnel.py
+++ b/tractor/net/_tunnel.py
@@ -93,6 +93,11 @@ import multibase
import trio
from ..msg._local import ProcessLocal
+from ..discovery.types import (
+ Address,
+ AddressDeclaration,
+ UnwrappedAddress,
+)
from ._bindspace import (
Bindspace,
BindspaceRef,
@@ -103,16 +108,6 @@ from ._bindspace import (
if TYPE_CHECKING:
from multiaddr import Multiaddr
- from ..discovery._addr import (
- Address,
- UnwrappedAddress,
- )
-else:
- Address = Any
- Multiaddr = Any
- UnwrappedAddress = Any
-
-
class WGTunnelSpec(
msgspec.Struct,
frozen=True,
@@ -879,7 +874,7 @@ class TunnelledAddress(
`strip_tunnels()`.
'''
- overlay: Address|TunnelledAddress
+ overlay: AddressDeclaration
tunnel: TunnelSpec
bindspace_ref: BindspaceRef|None = None
@@ -1057,7 +1052,7 @@ def parse_wg_maddr(
match overlay_names:
case [('ip4' | 'ip6'), 'tcp']:
from ..discovery._multiaddr import parse_maddr
- overlay: Address|TunnelledAddress = parse_maddr(
+ overlay: AddressDeclaration = parse_maddr(
str(overlay_ma)
)
@@ -1172,7 +1167,7 @@ def mk_wg_maddr(
def strip_tunnels(
- addr: Address|TunnelledAddress,
+ addr: AddressDeclaration,
) -> Address:
'''
Deliver the bindable `Address`, peeling any tunnel
@@ -1191,7 +1186,7 @@ def strip_tunnels(
def tunnels_of(
- addr: Address|TunnelledAddress,
+ addr: AddressDeclaration,
) -> tuple[TunnelSpec, ...]:
'''
Deliver every tunnel spec wrapping `addr`, outermost first.
diff --git a/tractor/runtime/_runtime.py b/tractor/runtime/_runtime.py
index 27e861f8..dcc187a3 100644
--- a/tractor/runtime/_runtime.py
+++ b/tractor/runtime/_runtime.py
@@ -60,7 +60,6 @@ import sys
from typing import (
Any,
Callable,
- Type,
TYPE_CHECKING,
)
import uuid
@@ -1684,10 +1683,10 @@ async def async_main(
[_state._def_tpt_proto]
)
for transport_key in enable_transports:
- transport_cls: Type[Address] = get_address_cls(
+ address_type = get_address_cls(
transport_key
)
- addr: Address = transport_cls.get_random()
+ addr: Address = address_type.get_random()
accept_addrs.append(addr.unwrap())
# XXX, either passed in by caller or delivered