Move `Address` types to `discovery.types`
Keep address descriptors and structural protocols independent of concrete IPC, actor runtime, tunnel and optional dependency mods. Use `AddressDeclaration` for plain and tunnelled address values, while concrete TCP/UDS factory dispatch remains in `._addr`. Drop runtime model-name aliases to `Any` and the self-referential `get_type_hints()` regression. Also, - preserve lazy `multiaddr` and net imports - verify canonical type identity across discovery, IPC and tunnels - route random and bound address reconstruction through concrete types Prompt-IO: ai/prompt-io/opencode/20260903T201530Z_d097090f_discovery_types.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/runtime_net_scopes
parent
41f34de3a4
commit
212a8f82ac
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,19 +15,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
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',
|
||||
)
|
||||
|
|
@ -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]:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue