Compare commits

..

12 Commits

Author SHA1 Message Date
Gud Boi ff02939213 Toss in some `colorlog` alts to try 2026-02-11 21:05:16 -05:00
Gud Boi d61e8caab2 Improve `test_log_sys` for new auto-naming logic
Add assertions and comments to better test the reworked
implicit module-name detection in `get_logger()`.

Deats,
- add `assert not tractor.current_actor()` check to verify
  no runtime is active during test.
- import `.log` submod directly for use.
- add masked `breakpoint()` for debugging mod loading.
- add comment about using `ranger` to inspect `testdir` layout
  of auto-generated py pkg + module-files.
- improve comments explaining pkg-root-log creation.
- add TODO for testing `get_logger()` call from pkg
  `__init__.py`
- add comment about first-pkg-level module naming.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-11 21:05:07 -05:00
Gud Boi 0b0c83e9da Drop `name=__name__` from all `get_logger()` calls
Use new implicit module-name detection throughout codebase to simplify
logger creation and leverage auto-naming from caller mod .

Main changes,
- drop `name=__name__` arg from all `get_logger()` calls
  (across 29 modules).
- update `get_console_log()` calls to include `name='tractor'` for
  enabling root logger in test harness and entry points; this ensures
  logic in `get_logger()` triggers so that **all** `tractor`-internal
  logging emits to console.
- add info log msg in test `conftest.py` showing test-harness
  log level

Also,
- fix `.actor.uid` ref to `.actor.aid.uid` in `._trace`.
- adjust a `._context` log msg formatting for clarity.
- add TODO comments in `._addr`, `._uds` for when we mv to
  using `multiaddr`.
- add todo for `RuntimeVars` type hint TODO in `.msg.types` (once we
  eventually get that all going obvi!)

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-11 21:04:49 -05:00
Gud Boi 5e7c0f264d Rework `.get_logger()`, better autonaming, deduping
Overhaul of the automatic-calling-module-name detection and sub-log
creation logic to avoid (at least warn) on duplication(s) and still
handle the common usage of a call with `name=__name__` from a mod's top
level scope.

Main `get_logger()` changes,
- refactor auto-naming logic for implicit `name=None` case such that we
  handle at least `tractor` internal "bare" calls from internal submods.
- factor out the `get_caller_mod()` closure (still inside
  `get_logger()`)for introspecting caller's module with configurable
  frame depth.
- use `.removeprefix()` instead of `.lstrip()` for stripping pkg-name
  from mod paths
- mv root-logger creation before sub-logger name processing
- improve duplicate detection for `pkg_name` in `name`
- add `_strict_debug=True`-only-emitted warnings for duplicate
  pkg/leaf-mod names.
- use `print()` fallback for warnings when no actor runtime is up at
  call time.

Surrounding tweaks,
- add `.level` property to `StackLevelAdapter` for getting
  current emit level as lowercase `str`.
- mv `_proj_name` def to just above `get_logger()`
- use `_curr_actor_no_exc` partial in `_conc_name_getters`
  to avoid runtime errors
- improve comments/doc-strings throughout
- keep some masked `breakpoint()` calls for future debugging

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-11 21:04:29 -05:00
Gud Boi edf1189fe0 Multi-line styling in `test.devx.conftest` 2026-02-11 21:04:22 -05:00
Tyler Goodlet de24bfe052 Mv `load_module_from_path()` to a new `._code_load` submod 2026-02-11 21:03:29 -05:00
Tyler Goodlet e235b96894 Use new `pkg_name` in log-sys test suites 2026-02-11 21:03:07 -05:00
Tyler Goodlet dea4b9fd93 Implicitly name sub-logs by caller's mod
That is when no `name` is passed to `get_logger()`, try to introspect
the caller's `module.__name__` and use it to infer/get the "namespace
path" to that module the same as if using `name=__name__` as in the most
common usage.

Further, change the `_root_name` to be `pkg_name: str`, a public and
more obvious param name, and deprecate the former. This obviously adds
the necessary impl to make the new
`test_sys_log::test_implicit_mod_name_applied_for_child` test pass.

Impl detalles for `get_logger()`,
- add `pkg_name` and deprecate `_root_name`, include failover logic
  and a warning.
- implement calling module introspection using
  `inspect.stack()/getmodule()` to get both the `.__name__` and
  `.__package__` info alongside adjusted logic to set the `name`
  when not provided but only when a new `mk_sublog: bool` is set.
- tweak the `name` processing for implicitly set case,
  - rename `sub_name` -> `pkg_path: str` which is the path
    to the calling module minus that module's name component.
  - only partition `name` if `pkg_name` is `in` it.
  - use the `_root_log` for `pkg_name` duplication warnings.

Other/related,
- add types to various public mod vars missing them.
- rename `.log.log` -> `.log._root_log`.
2026-02-11 21:03:07 -05:00
Tyler Goodlet 557e2cec6a Add an implicit-pkg-path-as-logger-name test
A bit of test driven dev to anticipate support  of `.log.get_logger()`
usage such that it can be called from arbitrary sub-modules, themselves
embedded in arbitrary sub-pkgs, of some project; the when not provided,
the `sub_name` passed to the `Logger.getChild(<sub_name>)` will be set
as the sub-pkg path "down to" the calling module.

IOW if you call something like,

`log = tractor.log.get_logger(pkg_name='mypylib')`

from some `submod.py` in a project-dir that looks like,

mypylib/
  mod.py
  subpkg/
    submod.py  <- calling module

the `log: StackLevelAdapter` child-`Logger` instance will have a
`.name: str = 'mypylib.subpkg'`, discluding the `submod` part since this
already rendered as the `{filename}` header in `log.LOG_FORMAT`.

Previously similar behaviour would be obtained by passing
`get_logger(name=__name__)` in the calling module and so much so it
motivated me to make this the default, presuming we can introspect for
the info.

Impl deats,
- duplicated a `load_module_from_path()` from `modden` to load the
  `testdir` rendered py project dir from its path.
 |_should prolly factor it down to this lib anyway bc we're going to
   need it for hot code reload? (well that and `watchfiles` Bp)
- in each of `mod.py` and `submod.py` render the `get_logger()` code
  sin `name`, expecting the (coming shortly) implicit introspection
  feat to do this.
- do `.name` and `.parent` checks against expected sub-logger values
  from `StackLevelAdapter.logger.getChildren()`.
2026-02-11 21:03:07 -05:00
Tyler Goodlet 0e3229f16d Start a logging-sys unit-test module
To start ensuring that when `name=__name__` is passed we try to
de-duplicate the `_root_name` and any `leaf_mod: str` since it's already
included in the headers as `{filename}`.

Deats,
- heavily document the de-duplication `str.partition()`s in
  `.log.get_logger()` and provide the end fix by changing the predicate,
  `if rname == 'tractor':` -> `if rname == _root_name`.
  * also toss in some warnings for when we still detect duplicates.
- add todo comments around logging "filters" (vs. our "adapter").
- create the new `test_log_sys.test_root_pkg_not_duplicated()` which
  runs green with the fixes from ^.
- add a ton of test-suite todos both for existing and anticipated
  logging sys feats in the new mod.
2026-02-11 21:03:07 -05:00
Bd 448d25aef4
Merge pull request #409 from goodboy/nixos_flake
Nixos flake, for the *too-hip-for-arch-ers*
2026-02-11 21:02:37 -05:00
Gud Boi 343c9e0034 Tweaks per the `copilot` PR review 2026-02-11 20:55:08 -05:00
35 changed files with 387 additions and 150 deletions

View File

@ -2,7 +2,7 @@
# https://pyproject-nix.github.io/pyproject.nix/templates.html#impure # https://pyproject-nix.github.io/pyproject.nix/templates.html#impure
# https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix # https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix
{ {
description = "An impure overlay using `uv` with Nix(OS)"; description = "An impure overlay (w dev-shell) using `uv`";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
@ -29,17 +29,17 @@
{ {
default = pkgs.mkShell { default = pkgs.mkShell {
packages = with pkgs; [ packages = [
# XXX, ensure sh completions activate! # XXX, ensure sh completions activate!
bashInteractive pkgs.bashInteractive
bash-completion pkgs.bash-completion
# on nixos, use pkg(s) # XXX, on nix(os), use pkgs version to avoid
ruff # build/sys-sh-integration issues
pypkgs.ruff pkgs.ruff
uv pkgs.uv
python313 # ?TODO^ how to set from `cpython` above? pkgs.${cpython}# ?TODO^ how to set from `cpython` above?
]; ];
shellHook = '' shellHook = ''
@ -56,7 +56,7 @@
# - always use the ./py313/ venv-subdir # - always use the ./py313/ venv-subdir
# - sync env with all extras # - sync env with all extras
export UV_PROJECT_ENVIRONMENT=${venv_dir} export UV_PROJECT_ENVIRONMENT=${venv_dir}
uv sync --dev --all-extras --no-group lint uv sync --dev --all-extras
# ------ TIPS ------ # ------ TIPS ------
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy) # NOTE, to launch the py-venv installed `xonsh` (like @goodboy)

View File

@ -65,7 +65,11 @@ def loglevel(request):
import tractor import tractor
orig = tractor.log._default_loglevel orig = tractor.log._default_loglevel
level = tractor.log._default_loglevel = request.config.option.loglevel level = tractor.log._default_loglevel = request.config.option.loglevel
tractor.log.get_console_log(level) log = tractor.log.get_console_log(
level=level,
name='tractor', # <- enable root logger
)
log.info(f'Test-harness logging level: {level}\n')
yield level yield level
tractor.log._default_loglevel = orig tractor.log._default_loglevel = orig

View File

@ -34,7 +34,10 @@ if TYPE_CHECKING:
# a fn that sub-instantiates a `pexpect.spawn()` # a fn that sub-instantiates a `pexpect.spawn()`
# and returns it. # and returns it.
type PexpectSpawner = Callable[[str], pty_spawn.spawn] type PexpectSpawner = Callable[
[str],
pty_spawn.spawn,
]
@pytest.fixture @pytest.fixture
@ -109,7 +112,11 @@ def ctlc(
'https://github.com/goodboy/tractor/issues/320' 'https://github.com/goodboy/tractor/issues/320'
) )
if mark.name == 'ctlcs_bish': if (
mark.name == 'ctlcs_bish'
and
use_ctlc
):
pytest.skip( pytest.skip(
f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n' f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n'
f'The test and/or underlying example script can *sometimes* run fine ' f'The test and/or underlying example script can *sometimes* run fine '

View File

@ -2,14 +2,15 @@
`tractor.log`-wrapping unit tests. `tractor.log`-wrapping unit tests.
''' '''
import importlib
from pathlib import Path from pathlib import Path
import shutil import shutil
import sys
from types import ModuleType
import pytest import pytest
import tractor import tractor
from tractor import (
_code_load,
log,
)
def test_root_pkg_not_duplicated_in_logger_name(): def test_root_pkg_not_duplicated_in_logger_name():
@ -22,12 +23,15 @@ def test_root_pkg_not_duplicated_in_logger_name():
project_name: str = 'pylib' project_name: str = 'pylib'
pkg_path: str = 'pylib.subpkg.mod' pkg_path: str = 'pylib.subpkg.mod'
proj_log = tractor.log.get_logger( assert not tractor.current_actor(
err_on_no_runtime=False,
)
proj_log = log.get_logger(
pkg_name=project_name, pkg_name=project_name,
mk_sublog=False, mk_sublog=False,
) )
sublog = tractor.log.get_logger( sublog = log.get_logger(
pkg_name=project_name, pkg_name=project_name,
name=pkg_path, name=pkg_path,
) )
@ -37,31 +41,6 @@ def test_root_pkg_not_duplicated_in_logger_name():
assert 'mod' not in sublog.name assert 'mod' not in sublog.name
# ?TODO, move this into internal libs?
# -[ ] we already use it in `modden.config._pymod` as well
def load_module_from_path(
path: Path,
module_name: str|None = None,
) -> ModuleType:
'''
Taken from SO,
https://stackoverflow.com/a/67208147
which is based on stdlib docs,
https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
'''
module_name = module_name or path.stem
spec = importlib.util.spec_from_file_location(
module_name,
str(path),
)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
def test_implicit_mod_name_applied_for_child( def test_implicit_mod_name_applied_for_child(
testdir: pytest.Pytester, testdir: pytest.Pytester,
loglevel: str, loglevel: str,
@ -78,6 +57,7 @@ def test_implicit_mod_name_applied_for_child(
mod_code: str = ( mod_code: str = (
f'import tractor\n' f'import tractor\n'
f'\n' f'\n'
# f'breakpoint()\n' # if you want to trace it all
f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n' f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n'
) )
@ -105,21 +85,30 @@ def test_implicit_mod_name_applied_for_child(
pkg_subpkg_submod, pkg_subpkg_submod,
) )
testdir.chdir() 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 # XXX NOTE, once the "top level" pkg mod has been
# imported, we can then use `import` syntax to # imported, we can then use `import` syntax to
# import it's sub-pkgs and modules. # import it's sub-pkgs and modules.
pkgmod = load_module_from_path( pkgmod = _code_load.load_module_from_path(
Path(pkg / '__init__.py'), Path(pkg / '__init__.py'),
module_name=proj_name, module_name=proj_name,
) )
pkg_root_log = tractor.log.get_logger( pkg_root_log = log.get_logger(
pkg_name=proj_name, pkg_name=proj_name,
mk_sublog=False, mk_sublog=False,
) )
# the top level pkg-mod, created just now,
# by above API call.
assert pkg_root_log.name == proj_name assert pkg_root_log.name == proj_name
assert not pkg_root_log.logger.getChildren() assert not pkg_root_log.logger.getChildren()
#
# ^TODO! test this same output but created via a `get_logger()`
# call in the `snakelib.__init__py`!!
# a first-pkg-level module should only
# use
from snakelib import mod from snakelib import mod
assert mod.log.name == proj_name assert mod.log.name == proj_name

View File

@ -17,9 +17,8 @@ from tractor.log import (
get_console_log, get_console_log,
get_logger, get_logger,
) )
log = get_logger(__name__)
log = get_logger()
_resource: int = 0 _resource: int = 0

View File

@ -37,7 +37,7 @@ from .ipc._uds import UDSAddress
if TYPE_CHECKING: if TYPE_CHECKING:
from ._runtime import Actor from ._runtime import Actor
log = get_logger(__name__) log = get_logger()
# TODO, maybe breakout the netns key to a struct? # TODO, maybe breakout the netns key to a struct?
@ -259,6 +259,8 @@ def wrap_address(
case _: case _:
# import pdbp; pdbp.set_trace() # import pdbp; pdbp.set_trace()
# from tractor.devx import mk_pdb
# mk_pdb().set_trace()
raise TypeError( raise TypeError(
f'Can not wrap unwrapped-address ??\n' f'Can not wrap unwrapped-address ??\n'
f'type(addr): {type(addr)!r}\n' f'type(addr): {type(addr)!r}\n'

View File

@ -0,0 +1,48 @@
# 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/>.
'''
(Hot) coad (re-)load utils for python.
'''
import importlib
from pathlib import Path
import sys
from types import ModuleType
# ?TODO, move this into internal libs?
# -[ ] we already use it in `modden.config._pymod` as well
def load_module_from_path(
path: Path,
module_name: str|None = None,
) -> ModuleType:
'''
Taken from SO,
https://stackoverflow.com/a/67208147
which is based on stdlib docs,
https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
'''
module_name = module_name or path.stem
spec = importlib.util.spec_from_file_location(
module_name,
str(path),
)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module

View File

@ -113,7 +113,7 @@ if TYPE_CHECKING:
CallerInfo, CallerInfo,
) )
log = get_logger(__name__) log = get_logger()
class Unresolved: class Unresolved:
@ -2391,16 +2391,18 @@ async def open_context_from_portal(
case trio.Cancelled(): case trio.Cancelled():
logmeth = log.cancel logmeth = log.cancel
cause: str = 'cancelled' 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 # XXX explicitly report on any non-graceful-taskc cases
case _: case _:
cause: str = 'errored' cause: str = 'errored'
logmeth = log.exception logmeth = log.exception
msg: str = f'ctx {ctx.side!r}-side {cause!r} with,\n'
logmeth( logmeth(msg)
f'ctx {ctx.side!r}-side {cause!r} with,\n'
f'{ctx.repr_outcome()!r}\n'
)
if debug_mode(): if debug_mode():
# async with debug.acquire_debug_lock(portal.actor.uid): # async with debug.acquire_debug_lock(portal.actor.uid):

View File

@ -53,7 +53,7 @@ if TYPE_CHECKING:
from ._runtime import Actor from ._runtime import Actor
log = get_logger(__name__) log = get_logger()
@acm @acm

View File

@ -50,7 +50,7 @@ if TYPE_CHECKING:
from ._spawn import SpawnMethodKey from ._spawn import SpawnMethodKey
log = get_logger(__name__) log = get_logger()
def _mp_main( def _mp_main(
@ -72,11 +72,15 @@ def _mp_main(
spawn_ctx: mp.context.BaseContext = try_set_start_method(start_method) spawn_ctx: mp.context.BaseContext = try_set_start_method(start_method)
assert spawn_ctx assert spawn_ctx
# XXX, enable root log at level
if actor.loglevel is not None: if actor.loglevel is not None:
log.info( log.info(
f'Setting loglevel for {actor.uid} to {actor.loglevel}' f'Setting loglevel for {actor.uid} to {actor.loglevel!r}'
)
get_console_log(
level=actor.loglevel,
name='tractor',
) )
get_console_log(actor.loglevel)
# TODO: use scops headers like for `trio` below! # TODO: use scops headers like for `trio` below!
# (well after we libify it maybe..) # (well after we libify it maybe..)
@ -126,8 +130,12 @@ def _trio_main(
parent_addr=parent_addr parent_addr=parent_addr
) )
# XXX, enable root log at level
if actor.loglevel is not None: if actor.loglevel is not None:
get_console_log(actor.loglevel) get_console_log(
level=actor.loglevel,
name='tractor',
)
log.info( log.info(
f'Starting `trio` subactor from parent @ ' f'Starting `trio` subactor from parent @ '
f'{parent_addr}\n' f'{parent_addr}\n'

View File

@ -69,7 +69,7 @@ from ._streaming import (
if TYPE_CHECKING: if TYPE_CHECKING:
from ._runtime import Actor from ._runtime import Actor
log = get_logger(__name__) log = get_logger()
class Portal: class Portal:

View File

@ -289,10 +289,12 @@ async def open_root_actor(
for uw_addr in uw_reg_addrs for uw_addr in uw_reg_addrs
] ]
loglevel = ( loglevel: str = (
loglevel loglevel
or log._default_loglevel or
).upper() log._default_loglevel
)
loglevel: str = loglevel.upper()
if ( if (
debug_mode debug_mode
@ -323,7 +325,10 @@ async def open_root_actor(
) )
assert loglevel assert loglevel
_log = log.get_console_log(loglevel) _log = log.get_console_log(
level=loglevel,
name='tractor',
)
assert _log assert _log
# TODO: factor this into `.devx._stackscope`!! # TODO: factor this into `.devx._stackscope`!!

View File

@ -59,7 +59,7 @@ if TYPE_CHECKING:
from .ipc import Channel from .ipc import Channel
log = get_logger(__name__) log = get_logger()
# TODO: the list # TODO: the list

View File

@ -62,7 +62,7 @@ if TYPE_CHECKING:
from .ipc import IPCServer from .ipc import IPCServer
log = get_logger(__name__) log = get_logger()
class ActorNursery: class ActorNursery:

View File

@ -49,7 +49,7 @@ from tractor.msg import (
import wrapt import wrapt
log = get_logger(__name__) log = get_logger()
# TODO: yeah, i don't love this and we should prolly just # TODO: yeah, i don't love this and we should prolly just
# write a decorator that actually keeps a stupid ref to the func # write a decorator that actually keeps a stupid ref to the func

View File

@ -51,7 +51,7 @@ from tractor import (
) )
from tractor.devx import debug from tractor.devx import debug
log = logmod.get_logger(__name__) log = logmod.get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -59,7 +59,7 @@ from ._sigint import (
_ctlc_ignore_header as _ctlc_ignore_header _ctlc_ignore_header as _ctlc_ignore_header
) )
log = get_logger(__name__) log = get_logger()
# ---------------- # ----------------
# XXX PKG TODO XXX # XXX PKG TODO XXX

View File

@ -84,7 +84,7 @@ _crash_msg: str = (
'Opening a pdb REPL in crashed actor' 'Opening a pdb REPL in crashed actor'
) )
log = get_logger(__package__) log = get_logger()
class BoxedMaybeException(Struct): class BoxedMaybeException(Struct):

View File

@ -47,7 +47,7 @@ if TYPE_CHECKING:
Actor, Actor,
) )
log = get_logger(__name__) log = get_logger()
_ctlc_ignore_header: str = ( _ctlc_ignore_header: str = (
'Ignoring SIGINT while debug REPL in use' 'Ignoring SIGINT while debug REPL in use'

View File

@ -58,7 +58,7 @@ from ._sigint import (
_ctlc_ignore_header as _ctlc_ignore_header _ctlc_ignore_header as _ctlc_ignore_header
) )
log = get_logger(__package__) log = get_logger()
async def maybe_wait_for_debugger( async def maybe_wait_for_debugger(

View File

@ -93,7 +93,7 @@ if TYPE_CHECKING:
# from ._post_mortem import BoxedMaybeException # from ._post_mortem import BoxedMaybeException
from ._repl import PdbREPL from ._repl import PdbREPL
log = get_logger(__package__) log = get_logger()
_pause_msg: str = 'Opening a pdb REPL in paused actor' _pause_msg: str = 'Opening a pdb REPL in paused actor'
_repl_fail_msg: str|None = ( _repl_fail_msg: str|None = (
@ -628,7 +628,7 @@ def _set_trace(
log.pdb( log.pdb(
f'{_pause_msg}\n' f'{_pause_msg}\n'
f'>(\n' f'>(\n'
f'|_{actor.uid}\n' f'|_{actor.aid.uid}\n'
f' |_{task}\n' # @ {actor.uid}\n' f' |_{task}\n' # @ {actor.uid}\n'
# f'|_{task}\n' # f'|_{task}\n'
# ^-TODO-^ more compact pformating? # ^-TODO-^ more compact pformating?

View File

@ -81,7 +81,7 @@ if TYPE_CHECKING:
BoxedMaybeException, BoxedMaybeException,
) )
log = get_logger(__name__) log = get_logger()
class LockStatus( class LockStatus(

View File

@ -60,7 +60,7 @@ if TYPE_CHECKING:
from ._transport import MsgTransport from ._transport import MsgTransport
log = get_logger(__name__) log = get_logger()
_is_windows = platform.system() == 'Windows' _is_windows = platform.system() == 'Windows'

View File

@ -72,7 +72,7 @@ if TYPE_CHECKING:
from .._supervise import ActorNursery from .._supervise import ActorNursery
log = log.get_logger(__name__) log = log.get_logger()
async def maybe_wait_on_canced_subs( async def maybe_wait_on_canced_subs(

View File

@ -59,7 +59,7 @@ except ImportError:
pass pass
log = get_logger(__name__) log = get_logger()
SharedMemory = disable_mantracker() SharedMemory = disable_mantracker()

View File

@ -41,7 +41,7 @@ from tractor.ipc._transport import (
) )
log = get_logger(__name__) log = get_logger()
class TCPAddress( class TCPAddress(

View File

@ -56,7 +56,7 @@ from tractor.msg import (
if TYPE_CHECKING: if TYPE_CHECKING:
from tractor._addr import Address from tractor._addr import Address
log = get_logger(__name__) log = get_logger()
# (codec, transport) # (codec, transport)

View File

@ -63,7 +63,7 @@ if TYPE_CHECKING:
from ._runtime import Actor from ._runtime import Actor
log = get_logger(__name__) log = get_logger()
def unwrap_sockpath( def unwrap_sockpath(
@ -166,6 +166,10 @@ class UDSAddress(
) )
if actor: if actor:
sockname: str = '::'.join(actor.uid) + f'@{pid}' 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}'
# -[ ] CURRETLY using `.` BREAKS TEST SUITE tho..
else: else:
prefix: str = '<unknown-actor>' prefix: str = '<unknown-actor>'
if is_root_process(): if is_root_process():

View File

@ -25,6 +25,7 @@ built on `tractor`.
''' '''
from collections.abc import Mapping from collections.abc import Mapping
from functools import partial
from inspect import ( from inspect import (
FrameInfo, FrameInfo,
getmodule, getmodule,
@ -41,12 +42,14 @@ from types import ModuleType
import warnings import warnings
import colorlog # type: ignore import colorlog # type: ignore
# ?TODO, some other (modern) alt libs?
# import coloredlogs
# import colored_traceback.auto # ?TODO, need better config?
import trio import trio
from ._state import current_actor from ._state import current_actor
_proj_name: str = 'tractor'
_default_loglevel: str = 'ERROR' _default_loglevel: str = 'ERROR'
# Super sexy formatting thanks to ``colorlog``. # Super sexy formatting thanks to ``colorlog``.
@ -124,6 +127,16 @@ class StackLevelAdapter(LoggerAdapter):
A (software) stack oriented logger "adapter". 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( def at_least_level(
self, self,
level: str, level: str,
@ -271,9 +284,14 @@ def pformat_task_uid(
return f'{task.name}[{tid_part}]' return f'{task.name}[{tid_part}]'
_curr_actor_no_exc = partial(
current_actor,
err_on_no_runtime=False,
)
_conc_name_getters = { _conc_name_getters = {
'task': pformat_task_uid, 'task': pformat_task_uid,
'actor': lambda: current_actor(), 'actor': lambda: _curr_actor_no_exc(),
'actor_name': lambda: current_actor().name, 'actor_name': lambda: current_actor().name,
'actor_uid': lambda: current_actor().uid[1][:6], 'actor_uid': lambda: current_actor().uid[1][:6],
} }
@ -305,8 +323,13 @@ class ActorContextInfo(Mapping):
return f'no {key} context' return f'no {key} context'
_proj_name: str = 'tractor'
def get_logger( def get_logger(
name: str|None = None, name: str|None = None,
# ^NOTE, setting `name=_proj_name=='tractor'` enables the "root
# logger" for `tractor` itself.
pkg_name: str = _proj_name, pkg_name: str = _proj_name,
# XXX, deprecated, use ^ # XXX, deprecated, use ^
_root_name: str|None = None, _root_name: str|None = None,
@ -319,6 +342,7 @@ def get_logger(
# |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema # |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
subsys_spec: str|None = None, subsys_spec: str|None = None,
mk_sublog: bool = True, mk_sublog: bool = True,
_strict_debug: bool = False,
) -> StackLevelAdapter: ) -> StackLevelAdapter:
''' '''
@ -348,60 +372,101 @@ def get_logger(
DeprecationWarning, DeprecationWarning,
stacklevel=2, 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` # Implicitly introspect the caller's module-name whenever `name`
# if left as the null default. # if left as the null default.
# #
# When the `pkg_name` is `in` in the `mod.__name__` we presume # When the `pkg_name` is `in` in the `mod.__name__` we presume
# this instance can be created as a sub-`StackLevelAdapter` and # this instance can be created as a sub-`StackLevelAdapter` and
# that the intention is get free module-path tracing and # that the intention is to get free module-path tracing and
# filtering (well once we implement that) oriented around the # filtering (well once we implement that) oriented around the
# py-module code hierarchy of the consuming project. # py-module code hierarchy of the consuming project.
#
if ( if (
pkg_name != _proj_name mk_sublog
and and
name is None name is None
and and
mk_sublog pkg_name
): ):
callstack: list[FrameInfo] = stack() if (caller_mod := get_caller_mod()):
caller_fi: FrameInfo = callstack[1] # ?XXX how is this `caller_mod.__name__` defined?
caller_mod: ModuleType = getmodule(caller_fi.frame) # => well by how the mod is imported.. XD
if caller_mod:
# ?how is this `mod.__name__` defined?
# -> well by how the mod is imported..
# |_https://stackoverflow.com/a/15883682 # |_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__: # if pkg_name in caller_mod.__package__:
# from tractor.devx.debug import mk_pdb # from tractor.devx.debug import mk_pdb
# mk_pdb().set_trace() # mk_pdb().set_trace()
mod_ns_path: str = caller_mod.__name__
mod_pkg_ns_path: str = caller_mod.__package__
# if 'snakelib' in mod_pkg_ns_path:
# import pdbp
# breakpoint()
if ( 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 pkg_name
# and # and
# pkg_name in mod_name # pkg_name in mod_ns_path
): ):
name = 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)
)
# XXX, lowlevel debuggin.. # XXX, lowlevel debuggin..
# if pkg_name != _proj_name: # if pkg_name != _proj_name:
# from tractor.devx.debug import mk_pdb # from tractor.devx.debug import mk_pdb
# mk_pdb().set_trace() # mk_pdb().set_trace()
if ( # NOTE: for handling for modules that use the unecessary,
name != _proj_name # `get_logger(__name__)`
and #
name
):
# NOTE: for handling for modules that use `get_logger(__name__)`
# we make the following stylistic choice: # we make the following stylistic choice:
# - always avoid duplicate project-package token # - always avoid duplicate project-package token
# in msg output: i.e. tractor.tractor.ipc._chan.py in header # in msg output: i.e. tractor.tractor.ipc._chan.py in header
@ -409,18 +474,67 @@ def get_logger(
# - never show the leaf module name in the {name} part # - never show the leaf module name in the {name} part
# since in python the {filename} is always this same # since in python the {filename} is always this same
# module-file. # module-file.
if (
name
and
# ?TODO? more correct?
# _proj_name not in name
name != pkg_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}.'
)
# if 'pylib' in name:
# import pdbp
# breakpoint()
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
rname: str = pkg_name rname: str = pkg_name
pkg_path: str = 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('.')
# ex. modden.runtime.progman # (
# rname,
# _,
# pkg_path,
# ) = name.partition('.')
# For ex. 'modden.runtime.progman'
# -> pkgpath='runtime', _, leaf_mod='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__`, # NOTE: special usage for passing `name=__name__`,
# #
@ -436,36 +550,84 @@ def get_logger(
# only includes the first 2 sub-pkg name-tokens in the # only includes the first 2 sub-pkg name-tokens in the
# child-logger's name; the colored "pkg-namespace" header # child-logger's name; the colored "pkg-namespace" header
# will then correctly show the same value as `name`. # will then correctly show the same value as `name`.
if rname == pkg_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
# )
):
pkg_path = subpkg_path pkg_path = subpkg_path
# XXX, do some double-checks for duplication of, # XXX, do some double-checks for duplication of,
# - root-pkg-name, already in root logger # - root-pkg-name, already in root logger
# - leaf-module-name already in `{filename}` header-field # - leaf-module-name already in `{filename}` header-field
if pkg_name in pkg_path: if (
_strict_debug
and
pkg_name
and
pkg_name in pkg_path
):
_duplicate, _, pkg_path = pkg_path.partition('.') _duplicate, _, pkg_path = pkg_path.partition('.')
if _duplicate: if _duplicate:
# assert _duplicate == rname msg: str = (
_root_log.warning( f'@ {get_caller_mod()}\n'
f'Duplicate pkg-name in sub-logger key?\n' f'Duplicate pkg-name in sub-logger key?\n'
f'pkg_name = {pkg_name!r}\n' f'pkg_name = {pkg_name!r}\n'
f'pkg_path = {pkg_path!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 ( if (
_strict_debug
and
leaf_mod leaf_mod
and and
leaf_mod in pkg_path leaf_mod in pkg_path
): ):
_root_log.warning( msg: str = (
f'@ {get_caller_mod()}\n'
f'Duplicate leaf-module-name in sub-logger key?\n' f'Duplicate leaf-module-name in sub-logger key?\n'
f'leaf_mod = {leaf_mod!r}\n' f'leaf_mod = {leaf_mod!r}\n'
f'pkg_path = {pkg_path!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'
)
if not pkg_path: # mk/get underlying (sub-)`Logger`
if (
not pkg_path
and
leaf_mod == pkg_name
):
# breakpoint()
log = rlog log = rlog
elif mk_sublog: elif mk_sublog:
# breakpoint()
log = rlog.getChild(pkg_path) log = rlog.getChild(pkg_path)
log.level = rlog.level log.level = rlog.level

View File

@ -68,7 +68,7 @@ from tractor.log import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
from tractor._context import Context from tractor._context import Context
log = get_logger(__name__) log = get_logger()
# TODO: unify with `MsgCodec` by making `._dec` part this? # TODO: unify with `MsgCodec` by making `._dec` part this?

View File

@ -77,7 +77,7 @@ if TYPE_CHECKING:
from tractor._streaming import MsgStream from tractor._streaming import MsgStream
log = get_logger(__name__) log = get_logger()
_def_any_pldec: MsgDec[Any] = mk_dec(spec=Any) _def_any_pldec: MsgDec[Any] = mk_dec(spec=Any)

View File

@ -51,7 +51,7 @@ from tractor.log import get_logger
# from tractor._addr import UnwrappedAddress # from tractor._addr import UnwrappedAddress
log = get_logger('tractor.msgspec') log = get_logger()
# type variable for the boxed payload field `.pld` # type variable for the boxed payload field `.pld`
PayloadT = TypeVar('PayloadT') PayloadT = TypeVar('PayloadT')
@ -202,7 +202,10 @@ class SpawnSpec(
# TODO: similar to the `Start` kwargs spec needed below, we need # TODO: similar to the `Start` kwargs spec needed below, we need
# a hard `Struct` def for all of these fields! # a hard `Struct` def for all of these fields!
_parent_main_data: dict _parent_main_data: dict
_runtime_vars: dict[str, Any] _runtime_vars: (
dict[str, Any]
#|RuntimeVars # !TODO
)
# ^NOTE see `._state._runtime_vars: dict` # ^NOTE see `._state._runtime_vars: dict`
# module import capability # module import capability

View File

@ -42,7 +42,7 @@ from trio.lowlevel import current_task
from msgspec import Struct from msgspec import Struct
from tractor.log import get_logger from tractor.log import get_logger
log = get_logger(__name__) log = get_logger()
# TODO: use new type-vars syntax from 3.12 # TODO: use new type-vars syntax from 3.12
# https://realpython.com/python312-new-features/#dedicated-type-variable-syntax # https://realpython.com/python312-new-features/#dedicated-type-variable-syntax

View File

@ -49,7 +49,7 @@ if TYPE_CHECKING:
from tractor import ActorNursery from tractor import ActorNursery
log = get_logger(__name__) log = get_logger()
# A regular invariant generic type # A regular invariant generic type
T = TypeVar("T") T = TypeVar("T")

View File

@ -34,7 +34,7 @@ from typing import (
import trio import trio
from tractor.log import get_logger from tractor.log import get_logger
log = get_logger(__name__) log = get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:
@ -246,23 +246,12 @@ async def maybe_raise_from_masking_exc(
type(exc_match) # masked type type(exc_match) # masked type
) )
# Add to masked `exc_ctx`
if do_warn: if do_warn:
exc_ctx.add_note(note) exc_ctx.add_note(note)
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.. # don't unmask already known "special" cases..
if len(masked) < 2:
if ( if (
_mask_cases _mask_cases
and and
@ -283,11 +272,26 @@ async def maybe_raise_from_masking_exc(
) )
raise exc_match raise exc_match
raise exc_ctx from exc_match # ^?TODO, see above but, possibly unmasking sub-exc
# ??TODO, see above but, possibly unmasking sub-exc
# entries if there are > 1 # entries if there are > 1
# else: # else:
# await pause(shield=True) # 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
else: else:
raise raise