Compare commits
13 Commits
2616f4b976
...
7b77d877a6
| Author | SHA1 | Date |
|---|---|---|
|
|
7b77d877a6 | |
|
|
3befe0940e | |
|
|
13c8e753d5 | |
|
|
f6d3415b53 | |
|
|
f34537aa70 | |
|
|
ac9228a101 | |
|
|
b5765d8221 | |
|
|
3ef6bc769a | |
|
|
bfc6caf2e7 | |
|
|
2e0ba81f98 | |
|
|
7ccd7aa227 | |
|
|
6d16c6347f | |
|
|
f8b24082b9 |
|
|
@ -17,7 +17,6 @@ from tractor import (
|
|||
MsgStream,
|
||||
_testing,
|
||||
trionics,
|
||||
TransportClosed,
|
||||
)
|
||||
import trio
|
||||
import pytest
|
||||
|
|
@ -209,16 +208,12 @@ async def main(
|
|||
# TODO: is this needed or no?
|
||||
raise
|
||||
|
||||
except (
|
||||
trio.ClosedResourceError,
|
||||
TransportClosed,
|
||||
) as _tpt_err:
|
||||
except trio.ClosedResourceError:
|
||||
# NOTE: don't send if we already broke the
|
||||
# connection to avoid raising a closed-error
|
||||
# such that we drop through to the ctl-c
|
||||
# mashing by user.
|
||||
with trio.CancelScope(shield=True):
|
||||
await trio.sleep(0.01)
|
||||
await trio.sleep(0.01)
|
||||
|
||||
# timeout: int = 1
|
||||
# with trio.move_on_after(timeout) as cs:
|
||||
|
|
@ -252,7 +247,6 @@ async def main(
|
|||
await stream.send(i)
|
||||
pytest.fail('stream not closed?')
|
||||
except (
|
||||
TransportClosed,
|
||||
trio.ClosedResourceError,
|
||||
trio.EndOfChannel,
|
||||
) as send_err:
|
||||
|
|
|
|||
20
flake.nix
20
flake.nix
|
|
@ -2,7 +2,7 @@
|
|||
# https://pyproject-nix.github.io/pyproject.nix/templates.html#impure
|
||||
# https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix
|
||||
{
|
||||
description = "An impure overlay (w dev-shell) using `uv`";
|
||||
description = "An impure overlay using `uv` with Nix(OS)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
|
@ -29,17 +29,17 @@
|
|||
{
|
||||
default = pkgs.mkShell {
|
||||
|
||||
packages = [
|
||||
packages = with pkgs; [
|
||||
# XXX, ensure sh completions activate!
|
||||
pkgs.bashInteractive
|
||||
pkgs.bash-completion
|
||||
bashInteractive
|
||||
bash-completion
|
||||
|
||||
# XXX, on nix(os), use pkgs version to avoid
|
||||
# build/sys-sh-integration issues
|
||||
pkgs.ruff
|
||||
# on nixos, use pkg(s)
|
||||
ruff
|
||||
pypkgs.ruff
|
||||
|
||||
pkgs.uv
|
||||
pkgs.${cpython}# ?TODO^ how to set from `cpython` above?
|
||||
uv
|
||||
python313 # ?TODO^ how to set from `cpython` above?
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
# - always use the ./py313/ venv-subdir
|
||||
# - sync env with all extras
|
||||
export UV_PROJECT_ENVIRONMENT=${venv_dir}
|
||||
uv sync --dev --all-extras
|
||||
uv sync --dev --all-extras --no-group lint
|
||||
|
||||
# ------ TIPS ------
|
||||
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ name = "tractor"
|
|||
version = "0.1.0a6dev0"
|
||||
description = 'structured concurrent `trio`-"actors"'
|
||||
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
|
||||
requires-python = ">= 3.11"
|
||||
requires-python = ">=3.11, <3.14"
|
||||
readme = "docs/README.rst"
|
||||
license = "AGPL-3.0-or-later"
|
||||
keywords = [
|
||||
|
|
@ -47,6 +47,7 @@ dependencies = [
|
|||
"msgspec>=0.19.0",
|
||||
"cffi>=1.17.1",
|
||||
"bidict>=0.23.1",
|
||||
"platformdirs>=4.4.0",
|
||||
]
|
||||
|
||||
# ------ project ------
|
||||
|
|
@ -59,7 +60,8 @@ dev = [
|
|||
]
|
||||
devx = [
|
||||
# `tractor.devx` tooling
|
||||
"greenback>=1.2.1,<2",
|
||||
"greenback>=1.3,<2",
|
||||
# ^XXX, `greenlet` borked on py314?
|
||||
"stackscope>=0.2.2,<0.3",
|
||||
# ^ requires this?
|
||||
"typing-extensions>=4.14.1",
|
||||
|
|
|
|||
|
|
@ -65,11 +65,7 @@ def loglevel(request):
|
|||
import tractor
|
||||
orig = tractor.log._default_loglevel
|
||||
level = tractor.log._default_loglevel = request.config.option.loglevel
|
||||
log = tractor.log.get_console_log(
|
||||
level=level,
|
||||
name='tractor', # <- enable root logger
|
||||
)
|
||||
log.info(f'Test-harness logging level: {level}\n')
|
||||
tractor.log.get_console_log(level)
|
||||
yield level
|
||||
tractor.log._default_loglevel = orig
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
'''
|
||||
from __future__ import annotations
|
||||
import time
|
||||
import signal
|
||||
from typing import (
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
|
|
@ -35,10 +34,7 @@ if TYPE_CHECKING:
|
|||
|
||||
# a fn that sub-instantiates a `pexpect.spawn()`
|
||||
# and returns it.
|
||||
type PexpectSpawner = Callable[
|
||||
[str],
|
||||
pty_spawn.spawn,
|
||||
]
|
||||
type PexpectSpawner = Callable[[str], pty_spawn.spawn]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -70,15 +66,12 @@ def spawn(
|
|||
import os
|
||||
os.environ['PYTHON_COLORS'] = '0'
|
||||
|
||||
spawned: PexpectSpawner|None = None
|
||||
|
||||
def _spawn(
|
||||
cmd: str,
|
||||
**mkcmd_kwargs,
|
||||
) -> pty_spawn.spawn:
|
||||
nonlocal spawned
|
||||
unset_colors()
|
||||
spawned = testdir.spawn(
|
||||
return testdir.spawn(
|
||||
cmd=mk_cmd(
|
||||
cmd,
|
||||
**mkcmd_kwargs,
|
||||
|
|
@ -88,35 +81,9 @@ def spawn(
|
|||
# ^TODO? get `pytest` core to expose underlying
|
||||
# `pexpect.spawn()` stuff?
|
||||
)
|
||||
return spawned
|
||||
|
||||
# such that test-dep can pass input script name.
|
||||
yield _spawn # the `PexpectSpawner`, type alias.
|
||||
|
||||
if (
|
||||
spawned
|
||||
and
|
||||
(ptyproc := spawned.ptyproc)
|
||||
):
|
||||
start: float = time.time()
|
||||
timeout: float = 5
|
||||
while (
|
||||
ptyproc.isalive()
|
||||
and
|
||||
(
|
||||
(_time_took := (time.time() - start))
|
||||
<
|
||||
timeout
|
||||
)
|
||||
):
|
||||
ptyproc.kill(signal.SIGINT)
|
||||
time.sleep(0.01)
|
||||
|
||||
if ptyproc.isalive():
|
||||
ptyproc.kill(signal.SIGKILL)
|
||||
|
||||
# TODO? ensure we've cleaned up any UDS-paths?
|
||||
# breakpoint()
|
||||
return _spawn # the `PexpectSpawner`, type alias.
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
|
|
@ -142,11 +109,7 @@ def ctlc(
|
|||
'https://github.com/goodboy/tractor/issues/320'
|
||||
)
|
||||
|
||||
if (
|
||||
mark.name == 'ctlcs_bish'
|
||||
and
|
||||
use_ctlc
|
||||
):
|
||||
if mark.name == 'ctlcs_bish':
|
||||
pytest.skip(
|
||||
f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n'
|
||||
f'The test and/or underlying example script can *sometimes* run fine '
|
||||
|
|
|
|||
|
|
@ -1138,10 +1138,7 @@ def test_ctxep_pauses_n_maybe_ipc_breaks(
|
|||
['peer IPC channel closed abruptly?',
|
||||
'another task closed this fd',
|
||||
'Debug lock request was CANCELLED?',
|
||||
"'MsgpackUDSStream' was already closed locally?",
|
||||
"TransportClosed: 'MsgpackUDSStream' was already closed 'by peer'?",
|
||||
# ?TODO^? match depending on `tpt_proto(s)`?
|
||||
]
|
||||
"TransportClosed: 'MsgpackUDSStream' was already closed locally ?",]
|
||||
|
||||
# XXX races on whether these show/hit?
|
||||
# 'Failed to REPl via `_pause()` You called `tractor.pause()` from an already cancelled scope!',
|
||||
|
|
|
|||
|
|
@ -98,8 +98,7 @@ def test_ipc_channel_break_during_stream(
|
|||
expect_final_exc = TransportClosed
|
||||
|
||||
mod: ModuleType = import_path(
|
||||
examples_dir()
|
||||
/ 'advanced_faults'
|
||||
examples_dir() / 'advanced_faults'
|
||||
/ 'ipc_failure_during_stream.py',
|
||||
root=examples_dir(),
|
||||
consider_namespace_packages=False,
|
||||
|
|
@ -114,9 +113,8 @@ def test_ipc_channel_break_during_stream(
|
|||
if (
|
||||
# only expect EoC if trans is broken on the child side,
|
||||
ipc_break['break_child_ipc_after'] is not False
|
||||
and
|
||||
# AND we tell the child to call `MsgStream.aclose()`.
|
||||
pre_aclose_msgstream
|
||||
and pre_aclose_msgstream
|
||||
):
|
||||
# expect_final_exc = trio.EndOfChannel
|
||||
# ^XXX NOPE! XXX^ since now `.open_stream()` absorbs this
|
||||
|
|
@ -162,8 +160,7 @@ def test_ipc_channel_break_during_stream(
|
|||
ipc_break['break_child_ipc_after'] is not False
|
||||
and (
|
||||
ipc_break['break_parent_ipc_after']
|
||||
>
|
||||
ipc_break['break_child_ipc_after']
|
||||
> ipc_break['break_child_ipc_after']
|
||||
)
|
||||
):
|
||||
if pre_aclose_msgstream:
|
||||
|
|
@ -251,15 +248,8 @@ def test_ipc_channel_break_during_stream(
|
|||
# get raw instance from pytest wrapper
|
||||
value = excinfo.value
|
||||
if isinstance(value, ExceptionGroup):
|
||||
excs: tuple[Exception] = value.exceptions
|
||||
assert (
|
||||
len(excs) <= 2
|
||||
and
|
||||
all(
|
||||
isinstance(exc, TransportClosed)
|
||||
for exc in excs
|
||||
)
|
||||
)
|
||||
excs = value.exceptions
|
||||
assert len(excs) == 1
|
||||
final_exc = excs[0]
|
||||
assert isinstance(final_exc, expect_final_exc)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ import trio
|
|||
import tractor
|
||||
from tractor import ( # typing
|
||||
Actor,
|
||||
Context,
|
||||
ContextCancelled,
|
||||
MsgStream,
|
||||
Portal,
|
||||
RemoteActorError,
|
||||
current_actor,
|
||||
open_nursery,
|
||||
Portal,
|
||||
Context,
|
||||
ContextCancelled,
|
||||
RemoteActorError,
|
||||
)
|
||||
from tractor._testing import (
|
||||
# tractor_test,
|
||||
|
|
@ -797,8 +796,8 @@ async def basic_echo_server(
|
|||
|
||||
) -> None:
|
||||
'''
|
||||
Just the simplest `MsgStream` echo server which resays what you
|
||||
told it but with its uid in front ;)
|
||||
Just the simplest `MsgStream` echo server which resays what
|
||||
you told it but with its uid in front ;)
|
||||
|
||||
'''
|
||||
actor: Actor = tractor.current_actor()
|
||||
|
|
@ -967,14 +966,9 @@ async def tell_little_bro(
|
|||
|
||||
caller: str = '',
|
||||
err_after: float|None = None,
|
||||
rng_seed: int = 100,
|
||||
# NOTE, ensure ^ is large enough (on fast hw anyway)
|
||||
# to ensure the peer cancel req arrives before the
|
||||
# echoing dialog does itself Bp
|
||||
rng_seed: int = 50,
|
||||
):
|
||||
# contact target actor, do a stream dialog.
|
||||
lb: Portal
|
||||
echo_ipc: MsgStream
|
||||
async with (
|
||||
tractor.wait_for_actor(
|
||||
name=actor_name
|
||||
|
|
@ -989,6 +983,7 @@ async def tell_little_bro(
|
|||
else None
|
||||
),
|
||||
) as (sub_ctx, first),
|
||||
|
||||
sub_ctx.open_stream() as echo_ipc,
|
||||
):
|
||||
actor: Actor = current_actor()
|
||||
|
|
@ -999,7 +994,6 @@ async def tell_little_bro(
|
|||
i,
|
||||
)
|
||||
await echo_ipc.send(msg)
|
||||
await trio.sleep(0.001)
|
||||
resp = await echo_ipc.receive()
|
||||
print(
|
||||
f'{caller} => {actor_name}: {msg}\n'
|
||||
|
|
@ -1012,9 +1006,6 @@ async def tell_little_bro(
|
|||
assert sub_uid != uid
|
||||
assert _i == i
|
||||
|
||||
# XXX, usually should never get here!
|
||||
# await tractor.pause()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'raise_client_error',
|
||||
|
|
@ -1029,9 +1020,6 @@ def test_peer_spawns_and_cancels_service_subactor(
|
|||
raise_client_error: str,
|
||||
reg_addr: tuple[str, int],
|
||||
raise_sub_spawn_error_after: float|None,
|
||||
loglevel: str,
|
||||
# ^XXX, set to 'warning' to see masked-exc warnings
|
||||
# that may transpire during actor-nursery teardown.
|
||||
):
|
||||
# NOTE: this tests for the modden `mod wks open piker` bug
|
||||
# discovered as part of implementing workspace ctx
|
||||
|
|
@ -1061,7 +1049,6 @@ def test_peer_spawns_and_cancels_service_subactor(
|
|||
# NOTE: to halt the peer tasks on ctxc, uncomment this.
|
||||
debug_mode=debug_mode,
|
||||
registry_addrs=[reg_addr],
|
||||
loglevel=loglevel,
|
||||
) as an:
|
||||
server: Portal = await an.start_actor(
|
||||
(server_name := 'spawn_server'),
|
||||
|
|
|
|||
|
|
@ -4,14 +4,10 @@
|
|||
'''
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
import tractor
|
||||
from tractor import (
|
||||
_code_load,
|
||||
log,
|
||||
)
|
||||
from tractor import _code_load
|
||||
|
||||
|
||||
def test_root_pkg_not_duplicated_in_logger_name():
|
||||
|
|
@ -24,15 +20,12 @@ def test_root_pkg_not_duplicated_in_logger_name():
|
|||
project_name: str = 'pylib'
|
||||
pkg_path: str = 'pylib.subpkg.mod'
|
||||
|
||||
assert not tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
)
|
||||
proj_log = log.get_logger(
|
||||
proj_log = tractor.log.get_logger(
|
||||
pkg_name=project_name,
|
||||
mk_sublog=False,
|
||||
)
|
||||
|
||||
sublog = log.get_logger(
|
||||
sublog = tractor.log.get_logger(
|
||||
pkg_name=project_name,
|
||||
name=pkg_path,
|
||||
)
|
||||
|
|
@ -58,86 +51,56 @@ def test_implicit_mod_name_applied_for_child(
|
|||
mod_code: str = (
|
||||
f'import tractor\n'
|
||||
f'\n'
|
||||
# if you need to trace `testdir` stuff @ import-time..
|
||||
# f'breakpoint()\n'
|
||||
f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n'
|
||||
)
|
||||
|
||||
# create a sub-module for each pkg layer
|
||||
_lib = testdir.mkpydir(proj_name)
|
||||
pkg: Path = Path(_lib)
|
||||
pkg_init_mod: Path = pkg / "__init__.py"
|
||||
pkg_init_mod.write_text(mod_code)
|
||||
|
||||
subpkg: Path = pkg / 'subpkg'
|
||||
subpkg.mkdir()
|
||||
subpkgmod: Path = subpkg / "__init__.py"
|
||||
subpkgmod.touch()
|
||||
subpkgmod.write_text(mod_code)
|
||||
|
||||
pkgmod: Path = subpkg / "__init__.py"
|
||||
pkgmod.touch()
|
||||
|
||||
_submod: Path = testdir.makepyfile(
|
||||
_mod=mod_code,
|
||||
)
|
||||
|
||||
pkg_submod = pkg / 'mod.py'
|
||||
pkg_mod = pkg / 'mod.py'
|
||||
pkg_subpkg_submod = subpkg / 'submod.py'
|
||||
shutil.copyfile(
|
||||
_submod,
|
||||
pkg_submod,
|
||||
pkg_mod,
|
||||
)
|
||||
shutil.copyfile(
|
||||
_submod,
|
||||
pkg_subpkg_submod,
|
||||
)
|
||||
testdir.chdir()
|
||||
# NOTE, to introspect the py-file-module-layout use (in .xsh
|
||||
# syntax): `ranger @str(testdir)`
|
||||
|
||||
# XXX NOTE, once the "top level" pkg mod has been
|
||||
# imported, we can then use `import` syntax to
|
||||
# import it's sub-pkgs and modules.
|
||||
subpkgmod: ModuleType = _code_load.load_module_from_path(
|
||||
pkgmod = _code_load.load_module_from_path(
|
||||
Path(pkg / '__init__.py'),
|
||||
module_name=proj_name,
|
||||
)
|
||||
|
||||
pkg_root_log = log.get_logger(
|
||||
pkg_root_log = tractor.log.get_logger(
|
||||
pkg_name=proj_name,
|
||||
mk_sublog=False,
|
||||
)
|
||||
# the top level pkg-mod, created just now,
|
||||
# by above API call.
|
||||
assert pkg_root_log.name == proj_name
|
||||
assert not pkg_root_log.logger.getChildren()
|
||||
#
|
||||
# ^TODO! test this same output but created via a `get_logger()`
|
||||
# call in the `snakelib.__init__py`!!
|
||||
|
||||
# NOTE, the pkg-level "init mod" should of course
|
||||
# have the same name as the package ns-path.
|
||||
import snakelib as init_mod
|
||||
assert init_mod.log.name == proj_name
|
||||
|
||||
# NOTE, a first-pkg-level sub-module should only
|
||||
# use the package-name since the leaf-node-module
|
||||
# will be included in log headers by default.
|
||||
from snakelib import mod
|
||||
assert mod.log.name == proj_name
|
||||
|
||||
from snakelib import subpkg
|
||||
assert (
|
||||
subpkg.log.name
|
||||
==
|
||||
subpkg.__package__
|
||||
==
|
||||
f'{proj_name}.subpkg'
|
||||
)
|
||||
|
||||
from snakelib.subpkg import submod
|
||||
assert (
|
||||
submod.log.name
|
||||
==
|
||||
submod.__package__
|
||||
submod.__package__ # ?TODO, use this in `.get_logger()` instead?
|
||||
==
|
||||
f'{proj_name}.subpkg'
|
||||
)
|
||||
|
|
@ -146,6 +109,8 @@ def test_implicit_mod_name_applied_for_child(
|
|||
assert len(sub_logs) == 1 # only one nested sub-pkg module
|
||||
assert submod.log.logger in sub_logs
|
||||
|
||||
# breakpoint()
|
||||
|
||||
|
||||
# TODO, moar tests against existing feats:
|
||||
# ------ - ------
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
"""
|
||||
Multiple python programs invoking the runtime.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
|
|
@ -15,29 +10,14 @@ import tractor
|
|||
from tractor._testing import (
|
||||
tractor_test,
|
||||
)
|
||||
from tractor import (
|
||||
current_actor,
|
||||
_state,
|
||||
Actor,
|
||||
Context,
|
||||
Portal,
|
||||
)
|
||||
from .conftest import (
|
||||
sig_prog,
|
||||
_INT_SIGNAL,
|
||||
_INT_RETURN_CODE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tractor.msg import Aid
|
||||
from tractor._addr import (
|
||||
UnwrappedAddress,
|
||||
)
|
||||
|
||||
|
||||
def test_abort_on_sigint(
|
||||
daemon: subprocess.Popen,
|
||||
):
|
||||
def test_abort_on_sigint(daemon):
|
||||
assert daemon.returncode is None
|
||||
time.sleep(0.1)
|
||||
sig_prog(daemon, _INT_SIGNAL)
|
||||
|
|
@ -50,11 +30,8 @@ def test_abort_on_sigint(
|
|||
|
||||
|
||||
@tractor_test
|
||||
async def test_cancel_remote_arbiter(
|
||||
daemon: subprocess.Popen,
|
||||
reg_addr: UnwrappedAddress,
|
||||
):
|
||||
assert not current_actor().is_arbiter
|
||||
async def test_cancel_remote_arbiter(daemon, reg_addr):
|
||||
assert not tractor.current_actor().is_arbiter
|
||||
async with tractor.get_registry(reg_addr) as portal:
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
|
@ -68,106 +45,24 @@ async def test_cancel_remote_arbiter(
|
|||
pass
|
||||
|
||||
|
||||
def test_register_duplicate_name(
|
||||
daemon: subprocess.Popen,
|
||||
reg_addr: UnwrappedAddress,
|
||||
):
|
||||
def test_register_duplicate_name(daemon, reg_addr):
|
||||
|
||||
async def main():
|
||||
|
||||
async with tractor.open_nursery(
|
||||
registry_addrs=[reg_addr],
|
||||
) as an:
|
||||
) as n:
|
||||
|
||||
assert not current_actor().is_arbiter
|
||||
assert not tractor.current_actor().is_arbiter
|
||||
|
||||
p1 = await an.start_actor('doggy')
|
||||
p2 = await an.start_actor('doggy')
|
||||
p1 = await n.start_actor('doggy')
|
||||
p2 = await n.start_actor('doggy')
|
||||
|
||||
async with tractor.wait_for_actor('doggy') as portal:
|
||||
assert portal.channel.uid in (p2.channel.uid, p1.channel.uid)
|
||||
|
||||
await an.cancel()
|
||||
await n.cancel()
|
||||
|
||||
# XXX, run manually since we want to start this root **after**
|
||||
# the other "daemon" program with it's own root.
|
||||
trio.run(main)
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def get_root_portal(
|
||||
ctx: Context,
|
||||
):
|
||||
'''
|
||||
Connect back to the root actor manually (using `._discovery` API)
|
||||
and ensure it's contact info is the same as our immediate parent.
|
||||
|
||||
'''
|
||||
sub: Actor = current_actor()
|
||||
rtvs: dict = _state._runtime_vars
|
||||
raddrs: list[UnwrappedAddress] = rtvs['_root_addrs']
|
||||
|
||||
# await tractor.pause()
|
||||
# XXX, in case the sub->root discovery breaks you might need
|
||||
# this (i know i did Xp)!!
|
||||
# from tractor.devx import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
assert (
|
||||
len(raddrs) == 1
|
||||
and
|
||||
list(sub._parent_chan.raddr.unwrap()) in raddrs
|
||||
)
|
||||
|
||||
# connect back to our immediate parent which should also
|
||||
# be the actor-tree's root.
|
||||
from tractor._discovery import get_root
|
||||
ptl: Portal
|
||||
async with get_root() as ptl:
|
||||
root_aid: Aid = ptl.chan.aid
|
||||
parent_ptl: Portal = current_actor().get_parent()
|
||||
assert (
|
||||
root_aid.name == 'root'
|
||||
and
|
||||
parent_ptl.chan.aid == root_aid
|
||||
)
|
||||
await ctx.started()
|
||||
|
||||
|
||||
def test_non_registrar_spawns_child(
|
||||
daemon: subprocess.Popen,
|
||||
reg_addr: UnwrappedAddress,
|
||||
loglevel: str,
|
||||
debug_mode: bool,
|
||||
):
|
||||
'''
|
||||
Ensure a non-regristar (serving) root actor can spawn a sub and
|
||||
that sub can connect back (manually) to it's rent that is the
|
||||
root without issue.
|
||||
|
||||
More or less this audits the global contact info in
|
||||
`._state._runtime_vars`.
|
||||
|
||||
'''
|
||||
async def main():
|
||||
async with tractor.open_nursery(
|
||||
registry_addrs=[reg_addr],
|
||||
loglevel=loglevel,
|
||||
debug_mode=debug_mode,
|
||||
) as an:
|
||||
|
||||
actor: Actor = tractor.current_actor()
|
||||
assert not actor.is_registrar
|
||||
sub_ptl: Portal = await an.start_actor(
|
||||
name='sub',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
|
||||
async with sub_ptl.open_context(
|
||||
get_root_portal,
|
||||
) as (ctx, _):
|
||||
print('Waiting for `sub` to connect back to us..')
|
||||
|
||||
await an.cancel()
|
||||
|
||||
# XXX, run manually since we want to start this root **after**
|
||||
# the other "daemon" program with it's own root.
|
||||
# run it manually since we want to start **after**
|
||||
# the other "daemon" program
|
||||
trio.run(main)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ from tractor.log import (
|
|||
get_console_log,
|
||||
get_logger,
|
||||
)
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
||||
_resource: int = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from .ipc._uds import UDSAddress
|
|||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
# TODO, maybe breakout the netns key to a struct?
|
||||
|
|
@ -259,8 +259,6 @@ def wrap_address(
|
|||
|
||||
case _:
|
||||
# import pdbp; pdbp.set_trace()
|
||||
# from tractor.devx import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
raise TypeError(
|
||||
f'Can not wrap unwrapped-address ??\n'
|
||||
f'type(addr): {type(addr)!r}\n'
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ from ._exceptions import (
|
|||
MsgTypeError,
|
||||
RemoteActorError,
|
||||
StreamOverrun,
|
||||
TransportClosed,
|
||||
pack_from_raise,
|
||||
unpack_error,
|
||||
)
|
||||
|
|
@ -114,7 +113,7 @@ if TYPE_CHECKING:
|
|||
CallerInfo,
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class Unresolved:
|
||||
|
|
@ -2392,18 +2391,16 @@ async def open_context_from_portal(
|
|||
case trio.Cancelled():
|
||||
logmeth = log.cancel
|
||||
cause: str = 'cancelled'
|
||||
msg: str = (
|
||||
f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
||||
f'{ctx.repr_outcome()!r}\n'
|
||||
)
|
||||
|
||||
# XXX explicitly report on any non-graceful-taskc cases
|
||||
case _:
|
||||
cause: str = 'errored'
|
||||
logmeth = log.exception
|
||||
msg: str = f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
||||
|
||||
logmeth(msg)
|
||||
logmeth(
|
||||
f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
||||
f'{ctx.repr_outcome()!r}\n'
|
||||
)
|
||||
|
||||
if debug_mode():
|
||||
# async with debug.acquire_debug_lock(portal.actor.uid):
|
||||
|
|
@ -2429,7 +2426,10 @@ async def open_context_from_portal(
|
|||
try:
|
||||
# await pause(shield=True)
|
||||
await ctx.cancel()
|
||||
except TransportClosed:
|
||||
except (
|
||||
trio.BrokenResourceError,
|
||||
trio.ClosedResourceError,
|
||||
):
|
||||
log.warning(
|
||||
'IPC connection for context is broken?\n'
|
||||
f'task: {ctx.cid}\n'
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ if TYPE_CHECKING:
|
|||
from ._runtime import Actor
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
@acm
|
||||
|
|
@ -91,13 +91,10 @@ async def get_registry(
|
|||
|
||||
|
||||
@acm
|
||||
async def get_root(**kwargs) -> AsyncGenerator[Portal, None]:
|
||||
'''
|
||||
Deliver the current actor's "root process" actor (yes in actor
|
||||
and proc tree terms) by delivering a `Portal` from the spawn-time
|
||||
provided contact address.
|
||||
async def get_root(
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[Portal, None]:
|
||||
|
||||
'''
|
||||
# TODO: rename mailbox to `_root_maddr` when we finally
|
||||
# add and impl libp2p multi-addrs?
|
||||
addr = _runtime_vars['_root_mailbox']
|
||||
|
|
@ -196,11 +193,6 @@ async def maybe_open_portal(
|
|||
addr: UnwrappedAddress,
|
||||
name: str,
|
||||
):
|
||||
'''
|
||||
Open a `Portal` to the actor serving @ `addr` or `None` if no
|
||||
peer can be contacted or found.
|
||||
|
||||
'''
|
||||
async with query_actor(
|
||||
name=name,
|
||||
regaddr=addr,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ if TYPE_CHECKING:
|
|||
from ._spawn import SpawnMethodKey
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def _mp_main(
|
||||
|
|
@ -72,15 +72,11 @@ def _mp_main(
|
|||
spawn_ctx: mp.context.BaseContext = try_set_start_method(start_method)
|
||||
assert spawn_ctx
|
||||
|
||||
# XXX, enable root log at level
|
||||
if actor.loglevel is not None:
|
||||
log.info(
|
||||
f'Setting loglevel for {actor.uid} to {actor.loglevel!r}'
|
||||
)
|
||||
get_console_log(
|
||||
level=actor.loglevel,
|
||||
name='tractor',
|
||||
f'Setting loglevel for {actor.uid} to {actor.loglevel}'
|
||||
)
|
||||
get_console_log(actor.loglevel)
|
||||
|
||||
# TODO: use scops headers like for `trio` below!
|
||||
# (well after we libify it maybe..)
|
||||
|
|
@ -130,12 +126,8 @@ def _trio_main(
|
|||
parent_addr=parent_addr
|
||||
)
|
||||
|
||||
# XXX, enable root log at level
|
||||
if actor.loglevel is not None:
|
||||
get_console_log(
|
||||
level=actor.loglevel,
|
||||
name='tractor',
|
||||
)
|
||||
get_console_log(actor.loglevel)
|
||||
log.info(
|
||||
f'Starting `trio` subactor from parent @ '
|
||||
f'{parent_addr}\n'
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ from ._streaming import (
|
|||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class Portal:
|
||||
|
|
@ -329,7 +329,18 @@ class Portal:
|
|||
# if we get here some weird cancellation case happened
|
||||
return False
|
||||
|
||||
except TransportClosed as tpt_err:
|
||||
except (
|
||||
# XXX, should never really get raised unless we aren't
|
||||
# wrapping them in the below type by mistake?
|
||||
#
|
||||
# Leaving the catch here for now until we're very sure
|
||||
# all the cases (for various tpt protos) have indeed been
|
||||
# re-wrapped ;p
|
||||
trio.ClosedResourceError,
|
||||
trio.BrokenResourceError,
|
||||
|
||||
TransportClosed,
|
||||
) as tpt_err:
|
||||
ipc_borked_report: str = (
|
||||
f'IPC for actor already closed/broken?\n\n'
|
||||
f'\n'
|
||||
|
|
|
|||
|
|
@ -88,8 +88,7 @@ async def maybe_block_bp(
|
|||
bp_blocked: bool
|
||||
if (
|
||||
debug_mode
|
||||
and
|
||||
maybe_enable_greenback
|
||||
and maybe_enable_greenback
|
||||
and (
|
||||
maybe_mod := await debug.maybe_init_greenback(
|
||||
raise_not_found=False,
|
||||
|
|
@ -290,12 +289,10 @@ async def open_root_actor(
|
|||
for uw_addr in uw_reg_addrs
|
||||
]
|
||||
|
||||
loglevel: str = (
|
||||
loglevel = (
|
||||
loglevel
|
||||
or
|
||||
log._default_loglevel
|
||||
)
|
||||
loglevel: str = loglevel.upper()
|
||||
or log._default_loglevel
|
||||
).upper()
|
||||
|
||||
if (
|
||||
debug_mode
|
||||
|
|
@ -326,10 +323,7 @@ async def open_root_actor(
|
|||
)
|
||||
|
||||
assert loglevel
|
||||
_log = log.get_console_log(
|
||||
level=loglevel,
|
||||
name='tractor',
|
||||
)
|
||||
_log = log.get_console_log(loglevel)
|
||||
assert _log
|
||||
|
||||
# TODO: factor this into `.devx._stackscope`!!
|
||||
|
|
@ -386,13 +380,10 @@ async def open_root_actor(
|
|||
addr,
|
||||
)
|
||||
|
||||
tpt_bind_addrs: list[
|
||||
Address # `Address.get_random()` case
|
||||
|UnwrappedAddress # registrar case `= uw_reg_addrs`
|
||||
] = []
|
||||
trans_bind_addrs: list[UnwrappedAddress] = []
|
||||
|
||||
# ------ NON-REGISTRAR ------
|
||||
# create a new root-actor instance.
|
||||
# Create a new local root-actor instance which IS NOT THE
|
||||
# REGISTRAR
|
||||
if ponged_addrs:
|
||||
if ensure_registry:
|
||||
raise RuntimeError(
|
||||
|
|
@ -419,21 +410,12 @@ async def open_root_actor(
|
|||
# XXX INSTEAD, bind random addrs using the same tpt
|
||||
# proto.
|
||||
for addr in ponged_addrs:
|
||||
tpt_bind_addrs.append(
|
||||
# XXX, these are `Address` NOT `UnwrappedAddress`.
|
||||
#
|
||||
# NOTE, in the case of posix/berkley socket
|
||||
# protos we allocate port=0 such that the system
|
||||
# allocates a random value at bind time; this
|
||||
# happens in the `.ipc.*` stack's backend.
|
||||
trans_bind_addrs.append(
|
||||
addr.get_random(
|
||||
bindspace=addr.bindspace,
|
||||
)
|
||||
)
|
||||
|
||||
# ------ REGISTRAR ------
|
||||
# create a new "registry providing" root-actor instance.
|
||||
#
|
||||
# Start this local actor as the "registrar", aka a regular
|
||||
# actor who manages the local registry of "mailboxes" of
|
||||
# other process-tree-local sub-actors.
|
||||
|
|
@ -442,7 +424,7 @@ async def open_root_actor(
|
|||
# following init steps are taken:
|
||||
# - the tranport layer server is bound to each addr
|
||||
# pair defined in provided registry_addrs, or the default.
|
||||
tpt_bind_addrs = uw_reg_addrs
|
||||
trans_bind_addrs = uw_reg_addrs
|
||||
|
||||
# - it is normally desirable for any registrar to stay up
|
||||
# indefinitely until either all registered (child/sub)
|
||||
|
|
@ -462,10 +444,20 @@ async def open_root_actor(
|
|||
enable_modules=enable_modules,
|
||||
)
|
||||
# XXX, in case the root actor runtime was actually run from
|
||||
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOT
|
||||
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOt
|
||||
# `.trio.run()`.
|
||||
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
||||
|
||||
# NOTE, only set the loopback addr for the
|
||||
# process-tree-global "root" mailbox since all sub-actors
|
||||
# should be able to speak to their root actor over that
|
||||
# channel.
|
||||
raddrs: list[Address] = _state._runtime_vars['_root_addrs']
|
||||
raddrs.extend(trans_bind_addrs)
|
||||
# TODO, remove once we have also removed all usage;
|
||||
# eventually all (root-)registry apis should expect > 1 addr.
|
||||
_state._runtime_vars['_root_mailbox'] = raddrs[0]
|
||||
|
||||
# Start up main task set via core actor-runtime nurseries.
|
||||
try:
|
||||
# assign process-local actor
|
||||
|
|
@ -502,39 +494,14 @@ async def open_root_actor(
|
|||
# "actor runtime" primitives are SC-compat and thus all
|
||||
# transitively spawned actors/processes must be as
|
||||
# well.
|
||||
accept_addrs: list[UnwrappedAddress]
|
||||
reg_addrs: list[UnwrappedAddress]
|
||||
(
|
||||
accept_addrs,
|
||||
reg_addrs,
|
||||
) = await root_tn.start(
|
||||
await root_tn.start(
|
||||
partial(
|
||||
_runtime.async_main,
|
||||
actor,
|
||||
accept_addrs=tpt_bind_addrs,
|
||||
accept_addrs=trans_bind_addrs,
|
||||
parent_addr=None
|
||||
)
|
||||
)
|
||||
# NOTE, only set a local-host addr (i.e. like
|
||||
# `lo`-loopback for TCP) for the process-tree-global
|
||||
# "root"-process (its tree-wide "mailbox") since all
|
||||
# sub-actors should be able to speak to their root
|
||||
# actor over that channel.
|
||||
#
|
||||
# ?TODO, per-OS non-network-proto alt options?
|
||||
# -[ ] on linux we should be able to always use UDS?
|
||||
#
|
||||
raddrs: list[UnwrappedAddress] = _state._runtime_vars['_root_addrs']
|
||||
raddrs.extend(
|
||||
accept_addrs,
|
||||
)
|
||||
# TODO, remove once we have also removed all usage;
|
||||
# eventually all (root-)registry apis should expect > 1 addr.
|
||||
_state._runtime_vars['_root_mailbox'] = raddrs[0]
|
||||
# if 'chart' in actor.aid.name:
|
||||
# from tractor.devx import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
try:
|
||||
yield actor
|
||||
except (
|
||||
|
|
@ -616,13 +583,6 @@ async def open_root_actor(
|
|||
):
|
||||
_state._runtime_vars['_debug_mode'] = False
|
||||
|
||||
# !XXX, clear ALL prior contact info state, this is MEGA
|
||||
# important if you are opening the runtime multiple times
|
||||
# from the same parent process (like in our test
|
||||
# harness)!
|
||||
_state._runtime_vars['_root_addrs'].clear()
|
||||
_state._runtime_vars['_root_mailbox'] = None
|
||||
|
||||
_state._current_actor = None
|
||||
_state._last_actor_terminated = actor
|
||||
|
||||
|
|
|
|||
|
|
@ -284,14 +284,9 @@ async def _errors_relayed_via_ipc(
|
|||
try:
|
||||
yield # run RPC invoke body
|
||||
|
||||
# NOTE, never REPL any pseudo-expected tpt-disconnect.
|
||||
except TransportClosed as err:
|
||||
rpc_err = err
|
||||
log.warning(
|
||||
f'Tpt disconnect during remote-exc relay due to,\n'
|
||||
f'{err!r}\n'
|
||||
)
|
||||
raise err
|
||||
except TransportClosed:
|
||||
log.exception('Tpt disconnect during remote-exc relay?')
|
||||
raise
|
||||
|
||||
# box and ship RPC errors for wire-transit via
|
||||
# the task's requesting parent IPC-channel.
|
||||
|
|
@ -328,6 +323,9 @@ async def _errors_relayed_via_ipc(
|
|||
and debug_kbis
|
||||
)
|
||||
)
|
||||
# TODO? better then `debug_filter` below?
|
||||
and
|
||||
not isinstance(err, TransportClosed)
|
||||
):
|
||||
# XXX QUESTION XXX: is there any case where we'll
|
||||
# want to debug IPC disconnects as a default?
|
||||
|
|
@ -348,6 +346,13 @@ async def _errors_relayed_via_ipc(
|
|||
entered_debug = await debug._maybe_enter_pm(
|
||||
err,
|
||||
api_frame=inspect.currentframe(),
|
||||
|
||||
# don't REPL any psuedo-expected tpt-disconnect
|
||||
# debug_filter=lambda exc: (
|
||||
# type (exc) not in {
|
||||
# TransportClosed,
|
||||
# }
|
||||
# ),
|
||||
)
|
||||
if not entered_debug:
|
||||
# if we prolly should have entered the REPL but
|
||||
|
|
@ -433,7 +438,7 @@ async def _errors_relayed_via_ipc(
|
|||
# cancel scope will not have been inserted yet
|
||||
if is_rpc:
|
||||
log.warning(
|
||||
'RPC task likely crashed or cancelled before start?\n'
|
||||
'RPC task likely errored or cancelled before start?\n'
|
||||
f'|_{ctx._task}\n'
|
||||
f' >> {ctx.repr_rpc}\n'
|
||||
)
|
||||
|
|
@ -689,6 +694,22 @@ async def _invoke(
|
|||
f'{pretty_struct.pformat(return_msg)}\n'
|
||||
)
|
||||
await chan.send(return_msg)
|
||||
# ?TODO, remove the below since .send() already
|
||||
# doesn't raise on tpt-closed?
|
||||
# try:
|
||||
# await chan.send(return_msg)
|
||||
# except TransportClosed:
|
||||
# log.exception(
|
||||
# f"Failed send final result to 'parent'-side of IPC-ctx!\n"
|
||||
# f'\n'
|
||||
# f'{chan}\n'
|
||||
# f'Channel already disconnected ??\n'
|
||||
# f'\n'
|
||||
# f'{pretty_struct.pformat(return_msg)}'
|
||||
# )
|
||||
# # ?TODO? will this ever be true though?
|
||||
# if chan.connected():
|
||||
# raise
|
||||
|
||||
# NOTE: this happens IFF `ctx._scope.cancel()` is
|
||||
# called by any of,
|
||||
|
|
@ -876,9 +897,9 @@ async def _invoke(
|
|||
)
|
||||
|
||||
logmeth(
|
||||
f'{message}'
|
||||
f'{message}\n'
|
||||
f'\n'
|
||||
f'{descr_str}'
|
||||
f'{descr_str}\n'
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -914,11 +935,6 @@ async def try_ship_error_to_remote(
|
|||
|
||||
# XXX NOTE XXX in SC terms this is one of the worst things
|
||||
# that can happen and provides for a 2-general's dilemma..
|
||||
#
|
||||
# FURHTER, we should never really have to handle these
|
||||
# lowlevel excs from `trio` since the `Channel.send()` layers
|
||||
# downward should be mostly wrapping such cases in a
|
||||
# tpt-closed; the `.critical()` usage is warranted.
|
||||
except (
|
||||
trio.ClosedResourceError,
|
||||
trio.BrokenResourceError,
|
||||
|
|
|
|||
|
|
@ -147,8 +147,6 @@ def get_mod_nsps2fps(mod_ns_paths: list[str]) -> dict[str, str]:
|
|||
return nsp2fp
|
||||
|
||||
|
||||
_bp = False
|
||||
|
||||
class Actor:
|
||||
'''
|
||||
The fundamental "runtime" concurrency primitive.
|
||||
|
|
@ -183,14 +181,6 @@ class Actor:
|
|||
def is_registrar(self) -> bool:
|
||||
return self.is_arbiter
|
||||
|
||||
@property
|
||||
def is_root(self) -> bool:
|
||||
'''
|
||||
This actor is the parent most in the tree?
|
||||
|
||||
'''
|
||||
return _state.is_root_process()
|
||||
|
||||
msg_buffer_size: int = 2**6
|
||||
|
||||
# nursery placeholders filled in by `async_main()`,
|
||||
|
|
@ -282,9 +272,7 @@ class Actor:
|
|||
stacklevel=2,
|
||||
)
|
||||
|
||||
registry_addrs: list[Address] = [
|
||||
wrap_address(arbiter_addr)
|
||||
]
|
||||
registry_addrs: list[Address] = [wrap_address(arbiter_addr)]
|
||||
|
||||
# marked by the process spawning backend at startup
|
||||
# will be None for the parent most process started manually
|
||||
|
|
@ -971,21 +959,6 @@ class Actor:
|
|||
|
||||
rvs['_is_root'] = False # obvi XD
|
||||
|
||||
# TODO, remove! left in just while protoing init fix!
|
||||
# global _bp
|
||||
# if (
|
||||
# 'chart' in self.aid.name
|
||||
# and
|
||||
# isinstance(
|
||||
# rvs['_root_addrs'][0],
|
||||
# dict,
|
||||
# )
|
||||
# and
|
||||
# not _bp
|
||||
# ):
|
||||
# _bp = True
|
||||
# breakpoint()
|
||||
|
||||
_state._runtime_vars.update(rvs)
|
||||
|
||||
# `SpawnSpec.reg_addrs`
|
||||
|
|
@ -1482,12 +1455,7 @@ async def async_main(
|
|||
# be False when running as root actor and True when as
|
||||
# a subactor.
|
||||
parent_addr: UnwrappedAddress|None = None,
|
||||
task_status: TaskStatus[
|
||||
tuple[
|
||||
list[UnwrappedAddress], # accept_addrs
|
||||
list[UnwrappedAddress], # reg_addrs
|
||||
]
|
||||
] = trio.TASK_STATUS_IGNORED,
|
||||
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -1666,7 +1634,6 @@ async def async_main(
|
|||
# if addresses point to the same actor..
|
||||
# So we need a way to detect that? maybe iterate
|
||||
# only on unique actor uids?
|
||||
addr: UnwrappedAddress
|
||||
for addr in actor.reg_addrs:
|
||||
try:
|
||||
waddr = wrap_address(addr)
|
||||
|
|
@ -1675,9 +1642,7 @@ async def async_main(
|
|||
await debug.pause()
|
||||
|
||||
# !TODO, get rid of the local-portal crap XD
|
||||
reg_portal: Portal
|
||||
async with get_registry(addr) as reg_portal:
|
||||
accept_addr: UnwrappedAddress
|
||||
for accept_addr in accept_addrs:
|
||||
accept_addr = wrap_address(accept_addr)
|
||||
|
||||
|
|
@ -1693,12 +1658,8 @@ async def async_main(
|
|||
|
||||
is_registered: bool = True
|
||||
|
||||
# init steps complete, deliver IPC-server and
|
||||
# registrar addrs back to caller.
|
||||
task_status.started((
|
||||
accept_addrs,
|
||||
actor.reg_addrs,
|
||||
))
|
||||
# init steps complete
|
||||
task_status.started()
|
||||
|
||||
# Begin handling our new connection back to our
|
||||
# parent. This is done last since we don't want to
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from __future__ import annotations
|
|||
from contextvars import (
|
||||
ContextVar,
|
||||
)
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
|
|
@ -30,6 +29,7 @@ from typing import (
|
|||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import platformdirs
|
||||
from trio.lowlevel import current_task
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -172,23 +172,32 @@ def current_ipc_ctx(
|
|||
return ctx
|
||||
|
||||
|
||||
# std ODE (mutable) app state location
|
||||
_rtdir: Path = Path(os.environ['XDG_RUNTIME_DIR'])
|
||||
|
||||
|
||||
def get_rt_dir(
|
||||
subdir: str = 'tractor'
|
||||
subdir: str|Path|None = None,
|
||||
) -> Path:
|
||||
'''
|
||||
Return the user "runtime dir" where most userspace apps stick
|
||||
their IPC and cache related system util-files; we take hold
|
||||
of a `'XDG_RUNTIME_DIR'/tractor/` subdir by default.
|
||||
Return the user "runtime dir", the file-sys location where most
|
||||
userspace apps stick their IPC and cache related system
|
||||
util-files.
|
||||
|
||||
On linux we take use a `'${XDG_RUNTIME_DIR}/tractor/` subdir by
|
||||
default but equivalents are mapped for each platform using
|
||||
the lovely `platformdirs`.
|
||||
|
||||
'''
|
||||
rtdir: Path = _rtdir / subdir
|
||||
if not rtdir.is_dir():
|
||||
rtdir.mkdir()
|
||||
return rtdir
|
||||
rt_dir: Path = Path(
|
||||
platformdirs.user_runtime_dir(
|
||||
appname='tractor',
|
||||
),
|
||||
)
|
||||
if subdir:
|
||||
rt_dir: Path = rt_dir / subdir
|
||||
|
||||
if not rt_dir.is_dir():
|
||||
rt_dir.mkdir()
|
||||
|
||||
return rt_dir
|
||||
|
||||
|
||||
def current_ipc_protos() -> list[str]:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import trio
|
|||
from ._exceptions import (
|
||||
ContextCancelled,
|
||||
RemoteActorError,
|
||||
TransportClosed,
|
||||
)
|
||||
from .log import get_logger
|
||||
from .trionics import (
|
||||
|
|
@ -60,7 +59,7 @@ if TYPE_CHECKING:
|
|||
from .ipc import Channel
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: the list
|
||||
|
|
@ -410,8 +409,10 @@ class MsgStream(trio.abc.Channel):
|
|||
# it).
|
||||
with trio.CancelScope(shield=True):
|
||||
await self._ctx.send_stop()
|
||||
|
||||
except (
|
||||
TransportClosed,
|
||||
trio.BrokenResourceError,
|
||||
trio.ClosedResourceError
|
||||
) as re:
|
||||
# the underlying channel may already have been pulled
|
||||
# in which case our stop message is meaningless since
|
||||
|
|
@ -592,8 +593,9 @@ class MsgStream(trio.abc.Channel):
|
|||
),
|
||||
)
|
||||
except (
|
||||
trio.ClosedResourceError,
|
||||
trio.BrokenResourceError,
|
||||
BrokenPipeError,
|
||||
TransportClosed,
|
||||
) as _trans_err:
|
||||
trans_err = _trans_err
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ if TYPE_CHECKING:
|
|||
from .ipc import IPCServer
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class ActorNursery:
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ from tractor.msg import (
|
|||
import wrapt
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
# TODO: yeah, i don't love this and we should prolly just
|
||||
# write a decorator that actually keeps a stupid ref to the func
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ from tractor import (
|
|||
)
|
||||
from tractor.devx import debug
|
||||
|
||||
log = logmod.get_logger()
|
||||
log = logmod.get_logger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ from ._sigint import (
|
|||
_ctlc_ignore_header as _ctlc_ignore_header
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
# ----------------
|
||||
# XXX PKG TODO XXX
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ _crash_msg: str = (
|
|||
'Opening a pdb REPL in crashed actor'
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__package__)
|
||||
|
||||
|
||||
class BoxedMaybeException(Struct):
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ if TYPE_CHECKING:
|
|||
Actor,
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
_ctlc_ignore_header: str = (
|
||||
'Ignoring SIGINT while debug REPL in use'
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ from ._sigint import (
|
|||
_ctlc_ignore_header as _ctlc_ignore_header
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__package__)
|
||||
|
||||
|
||||
async def maybe_wait_for_debugger(
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ if TYPE_CHECKING:
|
|||
# from ._post_mortem import BoxedMaybeException
|
||||
from ._repl import PdbREPL
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__package__)
|
||||
|
||||
_pause_msg: str = 'Opening a pdb REPL in paused actor'
|
||||
_repl_fail_msg: str|None = (
|
||||
|
|
@ -561,6 +561,9 @@ async def _pause(
|
|||
return
|
||||
|
||||
elif isinstance(pause_err, trio.Cancelled):
|
||||
__tracebackhide__: bool = False
|
||||
# XXX, unmask to REPL it.
|
||||
# mk_pdb().set_trace(frame=inspect.currentframe())
|
||||
_repl_fail_report += (
|
||||
'You called `tractor.pause()` from an already cancelled scope!\n\n'
|
||||
'Consider `await tractor.pause(shield=True)` to make it work B)\n'
|
||||
|
|
@ -628,7 +631,7 @@ def _set_trace(
|
|||
log.pdb(
|
||||
f'{_pause_msg}\n'
|
||||
f'>(\n'
|
||||
f'|_{actor.aid.uid}\n'
|
||||
f'|_{actor.uid}\n'
|
||||
f' |_{task}\n' # @ {actor.uid}\n'
|
||||
# f'|_{task}\n'
|
||||
# ^-TODO-^ more compact pformating?
|
||||
|
|
@ -1257,26 +1260,3 @@ async def breakpoint(
|
|||
api_frame=inspect.currentframe(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
async def maybe_pause_bp():
|
||||
'''
|
||||
Internal (ONLY for now) `breakpoint()`-er fn which only tries to
|
||||
use the multi-actor `.pause()` API when the current actor is the
|
||||
root.
|
||||
|
||||
?! BUT WHY !?
|
||||
-------
|
||||
|
||||
This is useful when debugging cases where the tpt layer breaks
|
||||
(or is intentionally broken, say during resiliency testing) in
|
||||
the case where a child can no longer contact the root process to
|
||||
acquire the process-tree-singleton TTY lock.
|
||||
|
||||
'''
|
||||
import tractor
|
||||
actor = tractor.current_actor()
|
||||
if actor.aid.name == 'root':
|
||||
await tractor.pause(shield=True)
|
||||
else:
|
||||
tractor.devx.mk_pdb().set_trace()
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ if TYPE_CHECKING:
|
|||
BoxedMaybeException,
|
||||
)
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class LockStatus(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ if TYPE_CHECKING:
|
|||
from ._transport import MsgTransport
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
_is_windows = platform.system() == 'Windows'
|
||||
|
||||
|
|
@ -307,12 +307,7 @@ class Channel:
|
|||
|
||||
) -> None:
|
||||
'''
|
||||
Send a coded msg-blob over the underlying IPC transport.
|
||||
|
||||
This fn raises `TransportClosed` on comms failures and is
|
||||
normally handled by higher level runtime machinery for the
|
||||
expected-graceful cases, normally ephemercal
|
||||
(re/dis)connects.
|
||||
Send a coded msg-blob over the transport.
|
||||
|
||||
'''
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
|
@ -339,10 +334,9 @@ class Channel:
|
|||
except KeyError:
|
||||
raise err
|
||||
case TransportClosed():
|
||||
src_exc_str: str = err.repr_src_exc()
|
||||
log.transport(
|
||||
f'Transport stream closed due to,\n'
|
||||
f'{src_exc_str}'
|
||||
f'Transport stream closed due to\n'
|
||||
f'{err.repr_src_exc()}\n'
|
||||
)
|
||||
|
||||
case _:
|
||||
|
|
@ -351,11 +345,6 @@ class Channel:
|
|||
raise
|
||||
|
||||
async def recv(self) -> Any:
|
||||
'''
|
||||
Receive the latest (queued) msg-blob from the underlying IPC
|
||||
transport.
|
||||
|
||||
'''
|
||||
assert self._transport
|
||||
return await self._transport.recv()
|
||||
|
||||
|
|
@ -429,18 +418,16 @@ class Channel:
|
|||
self
|
||||
) -> AsyncGenerator[Any, None]:
|
||||
'''
|
||||
Yield `MsgType` IPC msgs decoded and deliverd from an
|
||||
underlying `MsgTransport` protocol.
|
||||
Yield `MsgType` IPC msgs decoded and deliverd from
|
||||
an underlying `MsgTransport` protocol.
|
||||
|
||||
This is a streaming routine alo implemented as an
|
||||
async-generator func (same a `MsgTransport._iter_pkts()`)
|
||||
gets allocated by a `.__call__()` inside `.__init__()` where
|
||||
it is assigned to the `._aiter_msgs` attr.
|
||||
This is a streaming routine alo implemented as an async-gen
|
||||
func (same a `MsgTransport._iter_pkts()`) gets allocated by
|
||||
a `.__call__()` inside `.__init__()` where it is assigned to
|
||||
the `._aiter_msgs` attr.
|
||||
|
||||
'''
|
||||
if not self._transport:
|
||||
raise RuntimeError('No IPC transport initialized!?')
|
||||
|
||||
assert self._transport
|
||||
while True:
|
||||
try:
|
||||
async for msg in self._transport:
|
||||
|
|
@ -475,15 +462,7 @@ class Channel:
|
|||
# continue
|
||||
|
||||
def connected(self) -> bool:
|
||||
'''
|
||||
Predicate whether underlying IPC tpt is connected.
|
||||
|
||||
'''
|
||||
return (
|
||||
self._transport.connected()
|
||||
if self._transport
|
||||
else False
|
||||
)
|
||||
return self._transport.connected() if self._transport else False
|
||||
|
||||
async def _do_handshake(
|
||||
self,
|
||||
|
|
@ -514,11 +493,8 @@ async def _connect_chan(
|
|||
addr: UnwrappedAddress
|
||||
) -> typing.AsyncGenerator[Channel, None]:
|
||||
'''
|
||||
Create and connect a `Channel` to the provided `addr`, disconnect
|
||||
it on cm exit.
|
||||
|
||||
NOTE, this is a lowlevel, normally internal-only iface. You
|
||||
should likely use `.open_portal()` instead.
|
||||
Create and connect a channel with disconnect on context manager
|
||||
teardown.
|
||||
|
||||
'''
|
||||
chan = await Channel.from_addr(addr)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ if TYPE_CHECKING:
|
|||
from .._supervise import ActorNursery
|
||||
|
||||
|
||||
log = log.get_logger()
|
||||
log = log.get_logger(__name__)
|
||||
|
||||
|
||||
async def maybe_wait_on_canced_subs(
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ except ImportError:
|
|||
pass
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
SharedMemory = disable_mantracker()
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ from tractor.ipc._transport import (
|
|||
)
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class TCPAddress(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ from tractor.msg import (
|
|||
if TYPE_CHECKING:
|
||||
from tractor._addr import Address
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
# (codec, transport)
|
||||
|
|
@ -154,6 +154,7 @@ class MsgTransport(Protocol):
|
|||
# ...
|
||||
|
||||
|
||||
|
||||
class MsgpackTransport(MsgTransport):
|
||||
|
||||
# TODO: better naming for this?
|
||||
|
|
@ -277,18 +278,14 @@ class MsgpackTransport(MsgTransport):
|
|||
except trio.ClosedResourceError as cre:
|
||||
closure_err = cre
|
||||
|
||||
# await tractor.devx._trace.maybe_pause_bp()
|
||||
|
||||
raise TransportClosed(
|
||||
message=(
|
||||
f'{tpt_name} was already closed locally?'
|
||||
f'{tpt_name} was already closed locally ?\n'
|
||||
),
|
||||
src_exc=closure_err,
|
||||
loglevel='error',
|
||||
raise_on_report=(
|
||||
'another task closed this fd'
|
||||
in
|
||||
closure_err.args
|
||||
'another task closed this fd' in closure_err.args
|
||||
),
|
||||
) from closure_err
|
||||
|
||||
|
|
@ -438,11 +435,6 @@ class MsgpackTransport(MsgTransport):
|
|||
trans_err = _re
|
||||
tpt_name: str = f'{type(self).__name__!r}'
|
||||
|
||||
trans_err_msg: str = trans_err.args[0]
|
||||
by_whom: str = {
|
||||
'another task closed this fd': 'locally',
|
||||
'this socket was already closed': 'by peer',
|
||||
}.get(trans_err_msg)
|
||||
match trans_err:
|
||||
|
||||
# XXX, specifc to UDS transport and its,
|
||||
|
|
@ -454,42 +446,38 @@ class MsgpackTransport(MsgTransport):
|
|||
case trio.BrokenResourceError() if (
|
||||
'[Errno 32] Broken pipe'
|
||||
in
|
||||
trans_err_msg
|
||||
trans_err.args[0]
|
||||
):
|
||||
tpt_closed = TransportClosed.from_src_exc(
|
||||
message=(
|
||||
f'{tpt_name} already closed by peer\n'
|
||||
),
|
||||
body=f'{self}',
|
||||
body=f'{self}\n',
|
||||
src_exc=trans_err,
|
||||
raise_on_report=True,
|
||||
loglevel='transport',
|
||||
)
|
||||
raise tpt_closed from trans_err
|
||||
|
||||
# ??TODO??, what case in piker does this and HOW
|
||||
# CAN WE RE-PRODUCE IT?!?!?
|
||||
case trio.ClosedResourceError() if (
|
||||
by_whom
|
||||
):
|
||||
tpt_closed = TransportClosed.from_src_exc(
|
||||
message=(
|
||||
f'{tpt_name} was already closed {by_whom!r}?\n'
|
||||
),
|
||||
body=f'{self}',
|
||||
src_exc=trans_err,
|
||||
raise_on_report=True,
|
||||
loglevel='transport',
|
||||
)
|
||||
# case trio.ClosedResourceError() if (
|
||||
# 'this socket was already closed'
|
||||
# in
|
||||
# trans_err.args[0]
|
||||
# ):
|
||||
# tpt_closed = TransportClosed.from_src_exc(
|
||||
# message=(
|
||||
# f'{tpt_name} already closed by peer\n'
|
||||
# ),
|
||||
# body=f'{self}\n',
|
||||
# src_exc=trans_err,
|
||||
# raise_on_report=True,
|
||||
# loglevel='transport',
|
||||
# )
|
||||
# raise tpt_closed from trans_err
|
||||
|
||||
# await tractor.devx._trace.maybe_pause_bp()
|
||||
raise tpt_closed from trans_err
|
||||
|
||||
# XXX, unless the disconnect condition falls
|
||||
# under "a normal/expected operating breakage"
|
||||
# (per the `trans_err_msg` guards in the cases
|
||||
# above) we usualy console-error about it and
|
||||
# raise-thru. about it.
|
||||
# unless the disconnect condition falls under "a
|
||||
# normal operation breakage" we usualy console warn
|
||||
# about it.
|
||||
case _:
|
||||
log.exception(
|
||||
f'{tpt_name} layer failed pre-send ??\n'
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ if TYPE_CHECKING:
|
|||
from ._runtime import Actor
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def unwrap_sockpath(
|
||||
|
|
@ -166,10 +166,6 @@ class UDSAddress(
|
|||
)
|
||||
if actor:
|
||||
sockname: str = '::'.join(actor.uid) + f'@{pid}'
|
||||
# ?^TODO, for `multiaddr`'s parser we can't use the `::`
|
||||
# above^, SO maybe a `.` or something else here?
|
||||
# sockname: str = '.'.join(actor.uid) + f'@{pid}'
|
||||
# -[ ] CURRENTLY using `.` BREAKS TEST SUITE tho..
|
||||
else:
|
||||
prefix: str = '<unknown-actor>'
|
||||
if is_root_process():
|
||||
|
|
|
|||
245
tractor/log.py
245
tractor/log.py
|
|
@ -25,7 +25,6 @@ built on `tractor`.
|
|||
|
||||
'''
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
from inspect import (
|
||||
FrameInfo,
|
||||
getmodule,
|
||||
|
|
@ -42,14 +41,12 @@ from types import ModuleType
|
|||
import warnings
|
||||
|
||||
import colorlog # type: ignore
|
||||
# ?TODO, some other (modern) alt libs?
|
||||
# import coloredlogs
|
||||
# import colored_traceback.auto # ?TODO, need better config?
|
||||
import trio
|
||||
|
||||
from ._state import current_actor
|
||||
|
||||
|
||||
_proj_name: str = 'tractor'
|
||||
_default_loglevel: str = 'ERROR'
|
||||
|
||||
# Super sexy formatting thanks to ``colorlog``.
|
||||
|
|
@ -127,16 +124,6 @@ class StackLevelAdapter(LoggerAdapter):
|
|||
A (software) stack oriented logger "adapter".
|
||||
|
||||
'''
|
||||
@property
|
||||
def level(self) -> str:
|
||||
'''
|
||||
The currently set `str` emit level (in lowercase).
|
||||
|
||||
'''
|
||||
return logging.getLevelName(
|
||||
self.getEffectiveLevel()
|
||||
).lower()
|
||||
|
||||
def at_least_level(
|
||||
self,
|
||||
level: str,
|
||||
|
|
@ -284,14 +271,9 @@ def pformat_task_uid(
|
|||
return f'{task.name}[{tid_part}]'
|
||||
|
||||
|
||||
_curr_actor_no_exc = partial(
|
||||
current_actor,
|
||||
err_on_no_runtime=False,
|
||||
)
|
||||
|
||||
_conc_name_getters = {
|
||||
'task': pformat_task_uid,
|
||||
'actor': lambda: _curr_actor_no_exc(),
|
||||
'actor': lambda: current_actor(),
|
||||
'actor_name': lambda: current_actor().name,
|
||||
'actor_uid': lambda: current_actor().uid[1][:6],
|
||||
}
|
||||
|
|
@ -323,13 +305,8 @@ class ActorContextInfo(Mapping):
|
|||
return f'no {key} context'
|
||||
|
||||
|
||||
_proj_name: str = 'tractor'
|
||||
|
||||
|
||||
def get_logger(
|
||||
name: str|None = None,
|
||||
# ^NOTE, setting `name=_proj_name=='tractor'` enables the "root
|
||||
# logger" for `tractor` itself.
|
||||
pkg_name: str = _proj_name,
|
||||
# XXX, deprecated, use ^
|
||||
_root_name: str|None = None,
|
||||
|
|
@ -342,7 +319,6 @@ def get_logger(
|
|||
# |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
||||
subsys_spec: str|None = None,
|
||||
mk_sublog: bool = True,
|
||||
_strict_debug: bool = False,
|
||||
|
||||
) -> StackLevelAdapter:
|
||||
'''
|
||||
|
|
@ -372,162 +348,79 @@ def get_logger(
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
pkg_name: str = _root_name or pkg_name
|
||||
log: Logger
|
||||
log = rlog = logger or logging.getLogger(pkg_name)
|
||||
|
||||
pkg_name: str = _root_name
|
||||
|
||||
def get_caller_mod(
|
||||
frames_up:int = 2
|
||||
):
|
||||
'''
|
||||
Attempt to get the module which called `tractor.get_logger()`.
|
||||
|
||||
'''
|
||||
callstack: list[FrameInfo] = stack()
|
||||
caller_fi: FrameInfo = callstack[frames_up]
|
||||
caller_mod: ModuleType = getmodule(caller_fi.frame)
|
||||
return caller_mod
|
||||
|
||||
# --- Auto--naming-CASE ---
|
||||
# -------------------------
|
||||
# Implicitly introspect the caller's module-name whenever `name`
|
||||
# if left as the null default.
|
||||
#
|
||||
# When the `pkg_name` is `in` in the `mod.__name__` we presume
|
||||
# this instance can be created as a sub-`StackLevelAdapter` and
|
||||
# that the intention is to get free module-path tracing and
|
||||
# that the intention is get free module-path tracing and
|
||||
# filtering (well once we implement that) oriented around the
|
||||
# py-module code hierarchy of the consuming project.
|
||||
#
|
||||
if (
|
||||
mk_sublog
|
||||
pkg_name != _proj_name
|
||||
and
|
||||
name is None
|
||||
and
|
||||
pkg_name
|
||||
mk_sublog
|
||||
):
|
||||
if (caller_mod := get_caller_mod()):
|
||||
# ?XXX how is this `caller_mod.__name__` defined?
|
||||
# => well by how the mod is imported.. XD
|
||||
callstack: list[FrameInfo] = stack()
|
||||
caller_fi: FrameInfo = callstack[1]
|
||||
caller_mod: ModuleType = getmodule(caller_fi.frame)
|
||||
if caller_mod:
|
||||
# ?how is this `mod.__name__` defined?
|
||||
# -> well by how the mod is imported..
|
||||
# |_https://stackoverflow.com/a/15883682
|
||||
#
|
||||
mod_name: str = caller_mod.__name__
|
||||
mod_pkg: str = caller_mod.__package__
|
||||
log.info(
|
||||
f'Generating sub-logger name,\n'
|
||||
f'{mod_pkg}.{mod_name}\n'
|
||||
)
|
||||
# if pkg_name in caller_mod.__package__:
|
||||
# from tractor.devx.debug import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
mod_ns_path: str = caller_mod.__name__
|
||||
mod_pkg_ns_path: str = caller_mod.__package__
|
||||
if (
|
||||
mod_pkg_ns_path in mod_ns_path
|
||||
or
|
||||
pkg_name in mod_ns_path
|
||||
):
|
||||
# proper_mod_name = mod_ns_path.lstrip(
|
||||
proper_mod_name = mod_pkg_ns_path.removeprefix(
|
||||
f'{pkg_name}.'
|
||||
)
|
||||
name = proper_mod_name
|
||||
|
||||
elif (
|
||||
pkg_name
|
||||
# and
|
||||
# pkg_name in mod_ns_path
|
||||
# pkg_name in mod_name
|
||||
):
|
||||
name = mod_ns_path
|
||||
|
||||
if _strict_debug:
|
||||
msg: str = (
|
||||
f'@ {get_caller_mod()}\n'
|
||||
f'Generating sub-logger name,\n'
|
||||
f'{pkg_name}.{name}\n'
|
||||
)
|
||||
if _curr_actor_no_exc():
|
||||
_root_log.debug(msg)
|
||||
elif pkg_name != _proj_name:
|
||||
print(
|
||||
f'=> tractor.log.get_logger():\n'
|
||||
f'{msg}\n'
|
||||
)
|
||||
|
||||
# build a root logger instance
|
||||
log: Logger
|
||||
rlog = log = (
|
||||
logger
|
||||
or
|
||||
logging.getLogger(pkg_name)
|
||||
)
|
||||
name = mod_name
|
||||
|
||||
# XXX, lowlevel debuggin..
|
||||
# if pkg_name != _proj_name:
|
||||
# from tractor.devx.debug import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
# NOTE: for handling for modules that use the unecessary,
|
||||
# `get_logger(__name__)`
|
||||
#
|
||||
# we make the following stylistic choice:
|
||||
# - always avoid duplicate project-package token
|
||||
# in msg output: i.e. tractor.tractor.ipc._chan.py in header
|
||||
# looks ridiculous XD
|
||||
# - never show the leaf module name in the {name} part
|
||||
# since in python the {filename} is always this same
|
||||
# module-file.
|
||||
if (
|
||||
name
|
||||
name != _proj_name
|
||||
and
|
||||
# ?TODO? more correct?
|
||||
# _proj_name not in name
|
||||
name != pkg_name
|
||||
name
|
||||
):
|
||||
# ex. modden.runtime.progman
|
||||
# -> rname='modden', _, pkg_path='runtime.progman'
|
||||
if (
|
||||
pkg_name
|
||||
and
|
||||
pkg_name in name
|
||||
):
|
||||
proper_name: str = name.removeprefix(
|
||||
f'{pkg_name}.'
|
||||
)
|
||||
msg: str = (
|
||||
f'@ {get_caller_mod()}\n'
|
||||
f'Duplicate pkg-name in sub-logger `name`-key?\n'
|
||||
f'pkg_name = {pkg_name!r}\n'
|
||||
f'name = {name!r}\n'
|
||||
f'\n'
|
||||
f'=> You should change your input params to,\n'
|
||||
f'get_logger(\n'
|
||||
f' pkg_name={pkg_name!r}\n'
|
||||
f' name={proper_name!r}\n'
|
||||
f')'
|
||||
)
|
||||
# assert _duplicate == rname
|
||||
if _curr_actor_no_exc():
|
||||
_root_log.warning(msg)
|
||||
else:
|
||||
print(
|
||||
f'=> tractor.log.get_logger() ERROR:\n'
|
||||
f'{msg}\n'
|
||||
)
|
||||
|
||||
name = proper_name
|
||||
# NOTE: for handling for modules that use `get_logger(__name__)`
|
||||
# we make the following stylistic choice:
|
||||
# - always avoid duplicate project-package token
|
||||
# in msg output: i.e. tractor.tractor.ipc._chan.py in header
|
||||
# looks ridiculous XD
|
||||
# - never show the leaf module name in the {name} part
|
||||
# since in python the {filename} is always this same
|
||||
# module-file.
|
||||
|
||||
rname: str = pkg_name
|
||||
pkg_path: str = name
|
||||
|
||||
# ex. modden.runtime.progman
|
||||
# -> rname='modden', _, pkg_path='runtime.progman'
|
||||
if pkg_name in name:
|
||||
rname, _, pkg_path = name.partition('.')
|
||||
|
||||
# (
|
||||
# rname,
|
||||
# _,
|
||||
# pkg_path,
|
||||
# ) = name.partition('.')
|
||||
|
||||
# For ex. 'modden.runtime.progman'
|
||||
# ex. modden.runtime.progman
|
||||
# -> pkgpath='runtime', _, leaf_mod='progman'
|
||||
(
|
||||
subpkg_path,
|
||||
_,
|
||||
leaf_mod,
|
||||
) = pkg_path.rpartition('.')
|
||||
subpkg_path, _, leaf_mod = pkg_path.rpartition('.')
|
||||
|
||||
# NOTE: special usage for passing `name=__name__`,
|
||||
#
|
||||
|
|
@ -543,84 +436,36 @@ def get_logger(
|
|||
# only includes the first 2 sub-pkg name-tokens in the
|
||||
# child-logger's name; the colored "pkg-namespace" header
|
||||
# will then correctly show the same value as `name`.
|
||||
if (
|
||||
# XXX, TRY to remove duplication cases
|
||||
# which get warn-logged on below!
|
||||
(
|
||||
# when, subpkg_path == pkg_path
|
||||
subpkg_path
|
||||
and
|
||||
rname == pkg_name
|
||||
)
|
||||
# ) or (
|
||||
# # when, pkg_path == leaf_mod
|
||||
# pkg_path
|
||||
# and
|
||||
# leaf_mod == pkg_path
|
||||
# )
|
||||
):
|
||||
if rname == pkg_name:
|
||||
pkg_path = subpkg_path
|
||||
|
||||
# XXX, do some double-checks for duplication of,
|
||||
# - root-pkg-name, already in root logger
|
||||
# - leaf-module-name already in `{filename}` header-field
|
||||
if (
|
||||
_strict_debug
|
||||
and
|
||||
pkg_name
|
||||
and
|
||||
pkg_name in pkg_path
|
||||
):
|
||||
if pkg_name in pkg_path:
|
||||
_duplicate, _, pkg_path = pkg_path.partition('.')
|
||||
if _duplicate:
|
||||
msg: str = (
|
||||
f'@ {get_caller_mod()}\n'
|
||||
# assert _duplicate == rname
|
||||
_root_log.warning(
|
||||
f'Duplicate pkg-name in sub-logger key?\n'
|
||||
f'pkg_name = {pkg_name!r}\n'
|
||||
f'pkg_path = {pkg_path!r}\n'
|
||||
)
|
||||
# assert _duplicate == rname
|
||||
if _curr_actor_no_exc():
|
||||
_root_log.warning(msg)
|
||||
else:
|
||||
print(
|
||||
f'=> tractor.log.get_logger() ERROR:\n'
|
||||
f'{msg}\n'
|
||||
)
|
||||
# XXX, should never get here?
|
||||
breakpoint()
|
||||
|
||||
if (
|
||||
_strict_debug
|
||||
and
|
||||
leaf_mod
|
||||
and
|
||||
leaf_mod in pkg_path
|
||||
):
|
||||
msg: str = (
|
||||
f'@ {get_caller_mod()}\n'
|
||||
_root_log.warning(
|
||||
f'Duplicate leaf-module-name in sub-logger key?\n'
|
||||
f'leaf_mod = {leaf_mod!r}\n'
|
||||
f'pkg_path = {pkg_path!r}\n'
|
||||
)
|
||||
if _curr_actor_no_exc():
|
||||
_root_log.warning(msg)
|
||||
else:
|
||||
print(
|
||||
f'=> tractor.log.get_logger() ERROR:\n'
|
||||
f'{msg}\n'
|
||||
)
|
||||
|
||||
# mk/get underlying (sub-)`Logger`
|
||||
if (
|
||||
not pkg_path
|
||||
and
|
||||
leaf_mod == pkg_name
|
||||
):
|
||||
# breakpoint()
|
||||
if not pkg_path:
|
||||
log = rlog
|
||||
|
||||
elif mk_sublog:
|
||||
# breakpoint()
|
||||
log = rlog.getChild(pkg_path)
|
||||
|
||||
log.level = rlog.level
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ from tractor.log import get_logger
|
|||
if TYPE_CHECKING:
|
||||
from tractor._context import Context
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: unify with `MsgCodec` by making `._dec` part this?
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ if TYPE_CHECKING:
|
|||
from tractor._streaming import MsgStream
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
_def_any_pldec: MsgDec[Any] = mk_dec(spec=Any)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ from tractor.log import get_logger
|
|||
# from tractor._addr import UnwrappedAddress
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger('tractor.msgspec')
|
||||
|
||||
# type variable for the boxed payload field `.pld`
|
||||
PayloadT = TypeVar('PayloadT')
|
||||
|
|
@ -202,10 +202,7 @@ class SpawnSpec(
|
|||
# TODO: similar to the `Start` kwargs spec needed below, we need
|
||||
# a hard `Struct` def for all of these fields!
|
||||
_parent_main_data: dict
|
||||
_runtime_vars: (
|
||||
dict[str, Any]
|
||||
#|RuntimeVars # !TODO
|
||||
)
|
||||
_runtime_vars: dict[str, Any]
|
||||
# ^NOTE see `._state._runtime_vars: dict`
|
||||
|
||||
# module import capability
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ from tractor._state import (
|
|||
_runtime_vars,
|
||||
)
|
||||
from tractor._context import Unresolved
|
||||
from tractor import devx
|
||||
from tractor.devx import debug
|
||||
from tractor.log import (
|
||||
get_logger,
|
||||
StackLevelAdapter,
|
||||
|
|
@ -71,7 +71,7 @@ from outcome import (
|
|||
Outcome,
|
||||
)
|
||||
|
||||
log: StackLevelAdapter = get_logger()
|
||||
log: StackLevelAdapter = get_logger(__name__)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -439,23 +439,7 @@ def _run_asyncio_task(
|
|||
|
||||
orig = result = id(coro)
|
||||
try:
|
||||
# XXX TODO UGH!
|
||||
# this seems to break a `test_sync_pause_from_aio_task`
|
||||
# in a REALLY weird way where a `dict` value for
|
||||
# `_runtime_vars['_root_addrs']` is delivered from the
|
||||
# parent actor??
|
||||
#
|
||||
# XXX => see masked `.set_trace()` block in
|
||||
# `Actor.from_parent()`..
|
||||
#
|
||||
# with devx.maybe_open_crash_handler(
|
||||
# # XXX, if trio-side exits (intentionally) we
|
||||
# # shouldn't care bc it should have its own crash
|
||||
# # handling logic.
|
||||
# ignore={TrioTaskExited,},
|
||||
# ) as _bxerr:
|
||||
result: Any = await coro
|
||||
|
||||
chan._aio_result = result
|
||||
except BaseException as aio_err:
|
||||
chan._aio_err = aio_err
|
||||
|
|
@ -562,7 +546,7 @@ def _run_asyncio_task(
|
|||
if (
|
||||
debug_mode()
|
||||
and
|
||||
(greenback := devx.debug.maybe_import_greenback(
|
||||
(greenback := debug.maybe_import_greenback(
|
||||
force_reload=True,
|
||||
raise_not_found=False,
|
||||
))
|
||||
|
|
@ -962,11 +946,7 @@ async def translate_aio_errors(
|
|||
except BaseException as _trio_err:
|
||||
trio_err = chan._trio_err = _trio_err
|
||||
# await tractor.pause(shield=True) # workx!
|
||||
|
||||
# !TODO! we need an inter-loop lock here to avoid aio-tasks
|
||||
# clobbering trio ones when both crash in debug-mode!
|
||||
#
|
||||
entered: bool = await devx.debug._maybe_enter_pm(
|
||||
entered: bool = await debug._maybe_enter_pm(
|
||||
trio_err,
|
||||
api_frame=inspect.currentframe(),
|
||||
)
|
||||
|
|
@ -1300,17 +1280,10 @@ async def open_channel_from(
|
|||
suppress_graceful_exits: bool = True,
|
||||
**target_kwargs,
|
||||
|
||||
) -> AsyncIterator[
|
||||
tuple[LinkedTaskChannel, Any]
|
||||
]:
|
||||
) -> AsyncIterator[Any]:
|
||||
'''
|
||||
Start an `asyncio.Task` as `target()` and open an inter-loop
|
||||
(linked) channel for streaming between it and the current
|
||||
`trio.Task`.
|
||||
|
||||
A pair `(chan: LinkedTaskChannel, Any)` is delivered to the caller
|
||||
where the 2nd element is the value provided by the
|
||||
`asyncio.Task`'s unblocking call to `chan.started_nowait()`.
|
||||
Open an inter-loop linked task channel for streaming between a target
|
||||
spawned ``asyncio`` task and ``trio``.
|
||||
|
||||
'''
|
||||
chan: LinkedTaskChannel = _run_asyncio_task(
|
||||
|
|
@ -1335,7 +1308,6 @@ async def open_channel_from(
|
|||
|
||||
# deliver stream handle upward
|
||||
yield first, chan
|
||||
# ^TODO! swap these!!
|
||||
except trio.Cancelled as taskc:
|
||||
if cs.cancel_called:
|
||||
if isinstance(chan._trio_to_raise, AsyncioCancelled):
|
||||
|
|
@ -1366,8 +1338,7 @@ async def open_channel_from(
|
|||
)
|
||||
else:
|
||||
# XXX SHOULD NEVER HAPPEN!
|
||||
log.error("SHOULD NEVER GET HERE !?!?")
|
||||
await tractor.pause(shield=True)
|
||||
await tractor.pause()
|
||||
else:
|
||||
chan._to_trio.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ from trio.lowlevel import current_task
|
|||
from msgspec import Struct
|
||||
from tractor.log import get_logger
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
# TODO: use new type-vars syntax from 3.12
|
||||
# https://realpython.com/python312-new-features/#dedicated-type-variable-syntax
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ if TYPE_CHECKING:
|
|||
from tractor import ActorNursery
|
||||
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
# A regular invariant generic type
|
||||
T = TypeVar("T")
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from typing import (
|
|||
import trio
|
||||
from tractor.log import get_logger
|
||||
|
||||
log = get_logger()
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -246,12 +246,23 @@ async def maybe_raise_from_masking_exc(
|
|||
type(exc_match) # masked type
|
||||
)
|
||||
|
||||
# Add to masked `exc_ctx`
|
||||
if do_warn:
|
||||
exc_ctx.add_note(note)
|
||||
|
||||
# don't unmask already known "special" cases..
|
||||
if (
|
||||
do_warn
|
||||
and
|
||||
type(exc_match) in always_warn_on
|
||||
):
|
||||
log.warning(note)
|
||||
|
||||
if (
|
||||
do_warn
|
||||
and
|
||||
raise_unmasked
|
||||
):
|
||||
if len(masked) < 2:
|
||||
# don't unmask already known "special" cases..
|
||||
if (
|
||||
_mask_cases
|
||||
and
|
||||
|
|
@ -272,26 +283,11 @@ async def maybe_raise_from_masking_exc(
|
|||
)
|
||||
raise exc_match
|
||||
|
||||
# ^?TODO, see above but, possibly unmasking sub-exc
|
||||
# entries if there are > 1
|
||||
# else:
|
||||
# await pause(shield=True)
|
||||
|
||||
if type(exc_match) in always_warn_on:
|
||||
import traceback
|
||||
trace: list[str] = traceback.format_exception(
|
||||
type(exc_ctx),
|
||||
exc_ctx,
|
||||
exc_ctx.__traceback__
|
||||
)
|
||||
tb_str: str = ''.join(trace)
|
||||
log.warning(tb_str)
|
||||
# XXX, for debug
|
||||
# from tractor import pause
|
||||
# await pause(shield=True)
|
||||
|
||||
if raise_unmasked:
|
||||
raise exc_ctx from exc_match
|
||||
raise exc_ctx from exc_match
|
||||
|
||||
# ??TODO, see above but, possibly unmasking sub-exc
|
||||
# entries if there are > 1
|
||||
# else:
|
||||
# await pause(shield=True)
|
||||
else:
|
||||
raise
|
||||
|
|
|
|||
23
uv.lock
23
uv.lock
|
|
@ -1,6 +1,6 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.11, <3.14"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
|
|
@ -88,16 +88,16 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "greenback"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "outcome" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597, upload-time = "2024-02-20T21:23:13.239Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/d2/3b70d0f03a1e0f48d4f2348de435fa282e5530ae60812fef672cabc40a28/greenback-1.3.0.tar.gz", hash = "sha256:d1441f542ec9c6efb32a9250dd954a5b1cc1eb789294c19b1eb747f49cab818c", size = 8070613, upload-time = "2025-12-23T01:49:33.582Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062, upload-time = "2024-02-20T21:23:12.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/41/a1b338d80775c47f79cd7310d57ad4b98730f0656b15464a57dab821c5bb/greenback-1.3.0-py3-none-any.whl", hash = "sha256:b0a333a35b40f422981ebdeefc7e0a00568f2ac634604d0108cc8c30da9b6252", size = 29079, upload-time = "2025-12-23T01:49:31.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -236,6 +236,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
|
|
@ -404,6 +413,7 @@ dependencies = [
|
|||
{ name = "colorlog" },
|
||||
{ name = "msgspec" },
|
||||
{ name = "pdbp" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "tricycle" },
|
||||
{ name = "trio" },
|
||||
{ name = "wrapt" },
|
||||
|
|
@ -447,6 +457,7 @@ requires-dist = [
|
|||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||
{ name = "pdbp", specifier = ">=1.6,<2" },
|
||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||
{ name = "trio", specifier = ">0.27" },
|
||||
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
||||
|
|
@ -454,7 +465,7 @@ requires-dist = [
|
|||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "greenback", specifier = ">=1.3,<2" },
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
|
|
@ -465,7 +476,7 @@ dev = [
|
|||
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||
]
|
||||
devx = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "greenback", specifier = ">=1.3,<2" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue