Compare commits
3 Commits
70c7e334a7
...
66ac7863b5
| Author | SHA1 | Date |
|---|---|---|
|
|
66ac7863b5 | |
|
|
089e158da9 | |
|
|
7987f6b8d7 |
|
|
@ -0,0 +1,92 @@
|
||||||
|
'''
|
||||||
|
Regression tests for the cold package import surface.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
get_type_hints,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tractor.discovery import (
|
||||||
|
_addr,
|
||||||
|
_multiaddr,
|
||||||
|
)
|
||||||
|
from tractor.ipc import (
|
||||||
|
_tcp,
|
||||||
|
_uds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cold_import(code: str) -> dict[str, object]:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
'-c',
|
||||||
|
code,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lazy_to_asyncio_package_api():
|
||||||
|
'''
|
||||||
|
Keep the public lazy submodule discoverable without eagerly
|
||||||
|
importing it.
|
||||||
|
|
||||||
|
Before the lazy conversion, package import side effects exposed
|
||||||
|
`to_asyncio` to `dir()` and wildcard imports. Exercise those APIs
|
||||||
|
in cold interpreters so this test proves normal `import tractor`
|
||||||
|
leaves `asyncio` unloaded, while discovery and wildcard access
|
||||||
|
still advertise and resolve the public submodule.
|
||||||
|
|
||||||
|
'''
|
||||||
|
cold = run_cold_import(
|
||||||
|
'import json, sys, tractor; '
|
||||||
|
'print(json.dumps({'
|
||||||
|
'"advertised": "to_asyncio" in dir(tractor), '
|
||||||
|
'"asyncio_loaded": "asyncio" in sys.modules}))'
|
||||||
|
)
|
||||||
|
assert cold == {
|
||||||
|
'advertised': True,
|
||||||
|
'asyncio_loaded': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
wildcard = run_cold_import(
|
||||||
|
'import json; '
|
||||||
|
'from tractor import *; '
|
||||||
|
'print(json.dumps({'
|
||||||
|
'"module": to_asyncio.__name__}))'
|
||||||
|
)
|
||||||
|
assert wildcard == {
|
||||||
|
'module': 'tractor.to_asyncio',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_lazy_annotation_names_resolve():
|
||||||
|
'''
|
||||||
|
Resolve annotations without importing optional dependencies.
|
||||||
|
|
||||||
|
Moving annotation-only third-party names under `TYPE_CHECKING`
|
||||||
|
left their runtime globals undefined, causing
|
||||||
|
`typing.get_type_hints()` to raise `NameError`. Resolve every
|
||||||
|
affected API and prove the lazy aliases retain import-free runtime
|
||||||
|
introspection.
|
||||||
|
|
||||||
|
'''
|
||||||
|
assert get_type_hints(_multiaddr.mk_maddr)['return'] is Any
|
||||||
|
assert get_type_hints(_tcp.MsgpackTCPStream.maddr.fget)[
|
||||||
|
'return'
|
||||||
|
] is Any
|
||||||
|
assert get_type_hints(_uds.MsgpackUDSStream.maddr.fget)[
|
||||||
|
'return'
|
||||||
|
] == Any|str
|
||||||
|
assert get_type_hints(_addr.Address.get_random)[
|
||||||
|
'current_actor'
|
||||||
|
] is Any
|
||||||
|
assert _addr.__annotations__['_address_types'].startswith('dict')
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
`tractor.log`-wrapping unit tests.
|
`tractor.log`-wrapping unit tests.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -165,6 +167,53 @@ def test_implicit_mod_name_applied_for_child(
|
||||||
assert submod.log.logger in sub_logs
|
assert submod.log.logger in sub_logs
|
||||||
|
|
||||||
|
|
||||||
|
def test_implicit_mod_name_from_unregistered_namespace(
|
||||||
|
tmp_path: Path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Preserve implicit logger naming for dynamic module namespaces.
|
||||||
|
|
||||||
|
The fast `sys.modules` caller lookup cannot resolve `runpy`,
|
||||||
|
plugin-loader, or `exec()` namespaces that are not registered.
|
||||||
|
Compile a real package file under an unregistered module name so
|
||||||
|
the rare filename fallback must recover its imported package and
|
||||||
|
retain the same package-level logger name.
|
||||||
|
|
||||||
|
'''
|
||||||
|
pkg_name = 'dynamic_logger_pkg'
|
||||||
|
pkg_dir = tmp_path / pkg_name
|
||||||
|
pkg_dir.mkdir()
|
||||||
|
init_path = pkg_dir / '__init__.py'
|
||||||
|
init_path.write_text('')
|
||||||
|
mod_path = pkg_dir / 'plugin.py'
|
||||||
|
mod_path.write_text('')
|
||||||
|
|
||||||
|
sys.path.insert(0, str(tmp_path))
|
||||||
|
try:
|
||||||
|
importlib.import_module(pkg_name)
|
||||||
|
namespace = {
|
||||||
|
'__name__': f'{pkg_name}.unregistered',
|
||||||
|
'__package__': pkg_name,
|
||||||
|
'tractor': tractor,
|
||||||
|
}
|
||||||
|
exec(
|
||||||
|
compile(
|
||||||
|
'log = tractor.log.get_logger('
|
||||||
|
f'pkg_name={pkg_name!r})',
|
||||||
|
str(mod_path),
|
||||||
|
'exec',
|
||||||
|
),
|
||||||
|
namespace,
|
||||||
|
)
|
||||||
|
dynamic_log = namespace.get('log')
|
||||||
|
finally:
|
||||||
|
sys.path.remove(str(tmp_path))
|
||||||
|
sys.modules.pop(pkg_name, None)
|
||||||
|
|
||||||
|
assert dynamic_log is not None
|
||||||
|
assert dynamic_log.name == pkg_name
|
||||||
|
|
||||||
|
|
||||||
def test_io_custom_level_registered():
|
def test_io_custom_level_registered():
|
||||||
'''
|
'''
|
||||||
The `IO`(21) level (registered via `add_log_level()` at
|
The `IO`(21) level (registered via `add_log_level()` at
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,19 @@ from .discovery._registry import (
|
||||||
# from . import hilevel as hilevel
|
# from . import hilevel as hilevel
|
||||||
|
|
||||||
|
|
||||||
|
__all__: tuple[str, ...] = tuple(
|
||||||
|
name
|
||||||
|
for name in globals()
|
||||||
|
if not name.startswith('_')
|
||||||
|
) + (
|
||||||
|
'to_asyncio',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return sorted(set(globals()) | set(__all__))
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
def __getattr__(name: str):
|
||||||
'''
|
'''
|
||||||
PEP 562 lazy sub-module loading, presently only for
|
PEP 562 lazy sub-module loading, presently only for
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
Protocol,
|
Protocol,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Type,
|
Type,
|
||||||
|
|
@ -36,8 +37,9 @@ from ..ipc._uds import UDSAddress
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# ONLY type-annots, the eager import costs ~4.5ms
|
# ONLY type-annots, the eager import costs ~4.5ms
|
||||||
# of `import tractor` wall-time (gh #470).
|
# of `import tractor` wall-time (gh #470).
|
||||||
from bidict import bidict
|
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
|
else:
|
||||||
|
Actor = Any
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
||||||
|
|
@ -172,7 +174,7 @@ class Address(Protocol):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
_address_types: bidict[str, Type[Address]] = {
|
_address_types: dict[str, Type[Address]] = {
|
||||||
'tcp': TCPAddress,
|
'tcp': TCPAddress,
|
||||||
'uds': UDSAddress
|
'uds': UDSAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ Multiaddress support using the upstream `py-multiaddr` lib
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import (
|
||||||
|
Any,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# NOTE, `multiaddr` is lazy-imported at first use
|
# NOTE, `multiaddr` is lazy-imported at first use
|
||||||
|
|
@ -35,6 +38,9 @@ if TYPE_CHECKING:
|
||||||
# `import tractor` path (gh #470).
|
# `import tractor` path (gh #470).
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
from tractor.discovery._addr import Address
|
from tractor.discovery._addr import Address
|
||||||
|
else:
|
||||||
|
Multiaddr = Any
|
||||||
|
Address = Any
|
||||||
|
|
||||||
# map from tractor-internal `proto_key` identifiers
|
# map from tractor-internal `proto_key` identifiers
|
||||||
# to the standard multiaddr protocol name strings.
|
# to the standard multiaddr protocol name strings.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ TCP implementation of tractor.ipc._transport.MsgTransport protocol
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +47,8 @@ if TYPE_CHECKING:
|
||||||
# ONLY type-annots, the eager import costs
|
# ONLY type-annots, the eager import costs
|
||||||
# `import tractor` wall-time (gh #470).
|
# `import tractor` wall-time (gh #470).
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
|
else:
|
||||||
|
Multiaddr = Any
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from socket import (
|
||||||
)
|
)
|
||||||
import struct
|
import struct
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
Type,
|
Type,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
|
|
@ -66,6 +67,9 @@ if TYPE_CHECKING:
|
||||||
# `import tractor` wall-time (gh #470).
|
# `import tractor` wall-time (gh #470).
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
from tractor.runtime._runtime import Actor
|
from tractor.runtime._runtime import Actor
|
||||||
|
else:
|
||||||
|
Multiaddr = Any
|
||||||
|
Actor = Any
|
||||||
|
|
||||||
|
|
||||||
# Platform-specific credential passing constants
|
# Platform-specific credential passing constants
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,14 @@ def get_logger(
|
||||||
if mod_name is None:
|
if mod_name is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return sys.modules.get(mod_name)
|
if caller_mod := sys.modules.get(mod_name):
|
||||||
|
return caller_mod
|
||||||
|
|
||||||
|
# Preserve caller discovery for `runpy`, plugin loaders,
|
||||||
|
# and `exec()` namespaces not registered in `sys.modules`.
|
||||||
|
# Import `inspect` only on this rare fallback path.
|
||||||
|
from inspect import getmodule
|
||||||
|
return getmodule(caller_frame)
|
||||||
|
|
||||||
# --- Auto--naming-CASE ---
|
# --- Auto--naming-CASE ---
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue