Compare commits
	
		
			9 Commits 
		
	
	
		
			81c33bf550
			...
			b761524a85
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						b761524a85 | |
| 
							
							
								 | 
						b4dbf5dd86 | |
| 
							
							
								 | 
						39952344cb | |
| 
							
							
								 | 
						15f58495d5 | |
| 
							
							
								 | 
						2be3f93a8f | |
| 
							
							
								 | 
						224e92b468 | |
| 
							
							
								 | 
						ccedee3b87 | |
| 
							
							
								 | 
						7d947d3776 | |
| 
							
							
								 | 
						6b3cc72e5c | 
| 
						 | 
				
			
			@ -454,7 +454,7 @@ async def send_back_values(
 | 
			
		|||
    with (
 | 
			
		||||
        maybe_apply_codec(nsp_codec) as codec,
 | 
			
		||||
        limit_plds(
 | 
			
		||||
            rent_pld_spec,
 | 
			
		||||
            spec=rent_pld_spec,
 | 
			
		||||
            dec_hook=dec_nsp if add_hooks else None,
 | 
			
		||||
            ext_types=[NamespacePath]  if add_hooks else None,
 | 
			
		||||
        ) as pld_dec,
 | 
			
		||||
| 
						 | 
				
			
			@ -665,7 +665,9 @@ def test_ext_types_over_ipc(
 | 
			
		|||
                    expect_codec=nsp_codec,
 | 
			
		||||
                    enter_value=codec,
 | 
			
		||||
                )
 | 
			
		||||
                rent_pld_spec_type_strs: list[str] = _exts.enc_type_union(pld_spec)
 | 
			
		||||
                rent_pld_spec_type_strs: list[str] = _exts.enc_type_union(
 | 
			
		||||
                    pld_spec
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # XXX should raise an mte (`MsgTypeError`)
 | 
			
		||||
                # when `add_hooks == False` bc the input
 | 
			
		||||
| 
						 | 
				
			
			@ -695,7 +697,7 @@ def test_ext_types_over_ipc(
 | 
			
		|||
                            limit_plds(
 | 
			
		||||
                                pld_spec,
 | 
			
		||||
                                dec_hook=dec_nsp if add_hooks else None,
 | 
			
		||||
                                ext_types=[NamespacePath]  if add_hooks else None,
 | 
			
		||||
                                ext_types=[NamespacePath] if add_hooks else None,
 | 
			
		||||
                            ) as pld_dec,
 | 
			
		||||
                        ):
 | 
			
		||||
                            ctx_pld_dec: MsgDec = ctx._pld_rx._pld_dec
 | 
			
		||||
| 
						 | 
				
			
			@ -704,7 +706,7 @@ def test_ext_types_over_ipc(
 | 
			
		|||
                            # if (
 | 
			
		||||
                            #     not add_hooks
 | 
			
		||||
                            #     and
 | 
			
		||||
                            #     NamespacePath in 
 | 
			
		||||
                            #     NamespacePath in
 | 
			
		||||
                            # ):
 | 
			
		||||
                            #     pytest.fail('ctx should fail to open without custom enc_hook!?')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -743,204 +745,10 @@ def test_ext_types_over_ipc(
 | 
			
		|||
        assert exc.boxed_type is TypeError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def chk_pld_type(
 | 
			
		||||
#     payload_spec: Type[Struct]|Any,
 | 
			
		||||
#     pld: Any,
 | 
			
		||||
 | 
			
		||||
#     expect_roundtrip: bool|None = None,
 | 
			
		||||
 | 
			
		||||
# ) -> bool:
 | 
			
		||||
 | 
			
		||||
#     pld_val_type: Type = type(pld)
 | 
			
		||||
 | 
			
		||||
#     # TODO: verify that the overridden subtypes
 | 
			
		||||
#     # DO NOT have modified type-annots from original!
 | 
			
		||||
#     # 'Start',  .pld: FuncSpec
 | 
			
		||||
#     # 'StartAck',  .pld: IpcCtxSpec
 | 
			
		||||
#     # 'Stop',  .pld: UNSEt
 | 
			
		||||
#     # 'Error',  .pld: ErrorData
 | 
			
		||||
 | 
			
		||||
#     codec: MsgCodec = mk_codec(
 | 
			
		||||
#         # NOTE: this ONLY accepts `PayloadMsg.pld` fields of a specified
 | 
			
		||||
#         # type union.
 | 
			
		||||
#         ipc_pld_spec=payload_spec,
 | 
			
		||||
#     )
 | 
			
		||||
 | 
			
		||||
#     # make a one-off dec to compare with our `MsgCodec` instance
 | 
			
		||||
#     # which does the below `mk_msg_spec()` call internally
 | 
			
		||||
#     ipc_msg_spec: Union[Type[Struct]]
 | 
			
		||||
#     msg_types: list[PayloadMsg[payload_spec]]
 | 
			
		||||
#     (
 | 
			
		||||
#         ipc_msg_spec,
 | 
			
		||||
#         msg_types,
 | 
			
		||||
#     ) = mk_msg_spec(
 | 
			
		||||
#         payload_type_union=payload_spec,
 | 
			
		||||
#     )
 | 
			
		||||
#     _enc = msgpack.Encoder()
 | 
			
		||||
#     _dec = msgpack.Decoder(
 | 
			
		||||
#         type=ipc_msg_spec or Any,  # like `PayloadMsg[Any]`
 | 
			
		||||
#     )
 | 
			
		||||
 | 
			
		||||
#     assert (
 | 
			
		||||
#         payload_spec
 | 
			
		||||
#         ==
 | 
			
		||||
#         codec.pld_spec
 | 
			
		||||
#     )
 | 
			
		||||
 | 
			
		||||
#     # assert codec.dec == dec
 | 
			
		||||
#     #
 | 
			
		||||
#     # ^-XXX-^ not sure why these aren't "equal" but when cast
 | 
			
		||||
#     # to `str` they seem to match ?? .. kk
 | 
			
		||||
 | 
			
		||||
#     assert (
 | 
			
		||||
#         str(ipc_msg_spec)
 | 
			
		||||
#         ==
 | 
			
		||||
#         str(codec.msg_spec)
 | 
			
		||||
#         ==
 | 
			
		||||
#         str(_dec.type)
 | 
			
		||||
#         ==
 | 
			
		||||
#         str(codec.dec.type)
 | 
			
		||||
#     )
 | 
			
		||||
 | 
			
		||||
#     # verify the boxed-type for all variable payload-type msgs.
 | 
			
		||||
#     if not msg_types:
 | 
			
		||||
#         breakpoint()
 | 
			
		||||
 | 
			
		||||
#     roundtrip: bool|None = None
 | 
			
		||||
#     pld_spec_msg_names: list[str] = [
 | 
			
		||||
#         td.__name__ for td in _payload_msgs
 | 
			
		||||
#     ]
 | 
			
		||||
#     for typedef in msg_types:
 | 
			
		||||
 | 
			
		||||
#         skip_runtime_msg: bool = typedef.__name__ not in pld_spec_msg_names
 | 
			
		||||
#         if skip_runtime_msg:
 | 
			
		||||
#             continue
 | 
			
		||||
 | 
			
		||||
#         pld_field = structs.fields(typedef)[1]
 | 
			
		||||
#         assert pld_field.type is payload_spec # TODO-^ does this need to work to get all subtypes to adhere?
 | 
			
		||||
 | 
			
		||||
#         kwargs: dict[str, Any] = {
 | 
			
		||||
#             'cid': '666',
 | 
			
		||||
#             'pld': pld,
 | 
			
		||||
#         }
 | 
			
		||||
#         enc_msg: PayloadMsg = typedef(**kwargs)
 | 
			
		||||
 | 
			
		||||
#         _wire_bytes: bytes = _enc.encode(enc_msg)
 | 
			
		||||
#         wire_bytes: bytes = codec.enc.encode(enc_msg)
 | 
			
		||||
#         assert _wire_bytes == wire_bytes
 | 
			
		||||
 | 
			
		||||
#         ve: ValidationError|None = None
 | 
			
		||||
#         try:
 | 
			
		||||
#             dec_msg = codec.dec.decode(wire_bytes)
 | 
			
		||||
#             _dec_msg = _dec.decode(wire_bytes)
 | 
			
		||||
 | 
			
		||||
#             # decoded msg and thus payload should be exactly same!
 | 
			
		||||
#             assert (roundtrip := (
 | 
			
		||||
#                 _dec_msg
 | 
			
		||||
#                 ==
 | 
			
		||||
#                 dec_msg
 | 
			
		||||
#                 ==
 | 
			
		||||
#                 enc_msg
 | 
			
		||||
#             ))
 | 
			
		||||
 | 
			
		||||
#             if (
 | 
			
		||||
#                 expect_roundtrip is not None
 | 
			
		||||
#                 and expect_roundtrip != roundtrip
 | 
			
		||||
#             ):
 | 
			
		||||
#                 breakpoint()
 | 
			
		||||
 | 
			
		||||
#             assert (
 | 
			
		||||
#                 pld
 | 
			
		||||
#                 ==
 | 
			
		||||
#                 dec_msg.pld
 | 
			
		||||
#                 ==
 | 
			
		||||
#                 enc_msg.pld
 | 
			
		||||
#             )
 | 
			
		||||
#             # assert (roundtrip := (_dec_msg == enc_msg))
 | 
			
		||||
 | 
			
		||||
#         except ValidationError as _ve:
 | 
			
		||||
#             ve = _ve
 | 
			
		||||
#             roundtrip: bool = False
 | 
			
		||||
#             if pld_val_type is payload_spec:
 | 
			
		||||
#                 raise ValueError(
 | 
			
		||||
#                    'Got `ValidationError` despite type-var match!?\n'
 | 
			
		||||
#                     f'pld_val_type: {pld_val_type}\n'
 | 
			
		||||
#                     f'payload_type: {payload_spec}\n'
 | 
			
		||||
#                 ) from ve
 | 
			
		||||
 | 
			
		||||
#             else:
 | 
			
		||||
#                 # ow we good cuz the pld spec mismatched.
 | 
			
		||||
#                 print(
 | 
			
		||||
#                     'Got expected `ValidationError` since,\n'
 | 
			
		||||
#                     f'{pld_val_type} is not {payload_spec}\n'
 | 
			
		||||
#                 )
 | 
			
		||||
#         else:
 | 
			
		||||
#             if (
 | 
			
		||||
#                 payload_spec is not Any
 | 
			
		||||
#                 and
 | 
			
		||||
#                 pld_val_type is not payload_spec
 | 
			
		||||
#             ):
 | 
			
		||||
#                 raise ValueError(
 | 
			
		||||
#                    'DID NOT `ValidationError` despite expected type match!?\n'
 | 
			
		||||
#                     f'pld_val_type: {pld_val_type}\n'
 | 
			
		||||
#                     f'payload_type: {payload_spec}\n'
 | 
			
		||||
#                 )
 | 
			
		||||
 | 
			
		||||
#     # full code decode should always be attempted!
 | 
			
		||||
#     if roundtrip is None:
 | 
			
		||||
#         breakpoint()
 | 
			
		||||
 | 
			
		||||
#     return roundtrip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ?TODO? maybe remove since covered in the newer `test_pldrx_limiting`
 | 
			
		||||
# via end-2-end testing of all this?
 | 
			
		||||
# -[ ] IOW do we really NEED this lowlevel unit testing?
 | 
			
		||||
#
 | 
			
		||||
# def test_limit_msgspec(
 | 
			
		||||
#     debug_mode: bool,
 | 
			
		||||
# ):
 | 
			
		||||
#     '''
 | 
			
		||||
#     Internals unit testing to verify that type-limiting an IPC ctx's
 | 
			
		||||
#     msg spec with `Pldrx.limit_plds()` results in various
 | 
			
		||||
#     encapsulated `msgspec` object settings and state.
 | 
			
		||||
 | 
			
		||||
#     '''
 | 
			
		||||
#     async def main():
 | 
			
		||||
#         async with tractor.open_root_actor(
 | 
			
		||||
#             debug_mode=debug_mode,
 | 
			
		||||
#         ):
 | 
			
		||||
#             # ensure we can round-trip a boxing `PayloadMsg`
 | 
			
		||||
#             assert chk_pld_type(
 | 
			
		||||
#                 payload_spec=Any,
 | 
			
		||||
#                 pld=None,
 | 
			
		||||
#                 expect_roundtrip=True,
 | 
			
		||||
#             )
 | 
			
		||||
 | 
			
		||||
#             # verify that a mis-typed payload value won't decode
 | 
			
		||||
#             assert not chk_pld_type(
 | 
			
		||||
#                 payload_spec=int,
 | 
			
		||||
#                 pld='doggy',
 | 
			
		||||
#             )
 | 
			
		||||
 | 
			
		||||
#             # parametrize the boxed `.pld` type as a custom-struct
 | 
			
		||||
#             # and ensure that parametrization propagates
 | 
			
		||||
#             # to all payload-msg-spec-able subtypes!
 | 
			
		||||
#             class CustomPayload(Struct):
 | 
			
		||||
#                 name: str
 | 
			
		||||
#                 value: Any
 | 
			
		||||
 | 
			
		||||
#             assert not chk_pld_type(
 | 
			
		||||
#                 payload_spec=CustomPayload,
 | 
			
		||||
#                 pld='doggy',
 | 
			
		||||
#             )
 | 
			
		||||
 | 
			
		||||
#             assert chk_pld_type(
 | 
			
		||||
#                 payload_spec=CustomPayload,
 | 
			
		||||
#                 pld=CustomPayload(name='doggy', value='urmom')
 | 
			
		||||
#             )
 | 
			
		||||
 | 
			
		||||
#             # yah, we can `.pause_from_sync()` now!
 | 
			
		||||
#             # breakpoint()
 | 
			
		||||
 | 
			
		||||
#     trio.run(main)
 | 
			
		||||
# TODO: further SC-msg-specific verification that the overridden
 | 
			
		||||
# subtypes DO NOT have modified type-annots from original!
 | 
			
		||||
# 'Start',  .pld: FuncSpec
 | 
			
		||||
# 'StartAck',  .pld: IpcCtxSpec
 | 
			
		||||
# 'Stop',  .pld: UNSEt
 | 
			
		||||
# 'Error',  .pld: ErrorData
 | 
			
		||||
# def test_per_msg_payload_spec_limits():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,14 +2,12 @@
 | 
			
		|||
`tractor.log`-wrapping unit tests.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
import importlib
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import shutil
 | 
			
		||||
import sys
 | 
			
		||||
from types import ModuleType
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import tractor
 | 
			
		||||
from tractor import _code_load
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_root_pkg_not_duplicated_in_logger_name():
 | 
			
		||||
| 
						 | 
				
			
			@ -37,31 +35,6 @@ def test_root_pkg_not_duplicated_in_logger_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(
 | 
			
		||||
    testdir: pytest.Pytester,
 | 
			
		||||
    loglevel: str,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +82,7 @@ def test_implicit_mod_name_applied_for_child(
 | 
			
		|||
    # 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.
 | 
			
		||||
    pkgmod = load_module_from_path(
 | 
			
		||||
    pkgmod = _code_load.load_module_from_path(
 | 
			
		||||
        Path(pkg / '__init__.py'),
 | 
			
		||||
        module_name=proj_name,
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,15 @@ related settings around IPC contexts.
 | 
			
		|||
from contextlib import (
 | 
			
		||||
    asynccontextmanager as acm,
 | 
			
		||||
)
 | 
			
		||||
import sys
 | 
			
		||||
import types
 | 
			
		||||
from typing import (
 | 
			
		||||
    Any,
 | 
			
		||||
    Union,
 | 
			
		||||
    Type,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import msgspec
 | 
			
		||||
from msgspec import (
 | 
			
		||||
    Struct,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -22,11 +30,10 @@ from tractor import (
 | 
			
		|||
    Portal,
 | 
			
		||||
)
 | 
			
		||||
from tractor.msg import (
 | 
			
		||||
    _codec,
 | 
			
		||||
    _ops as msgops,
 | 
			
		||||
    Return,
 | 
			
		||||
)
 | 
			
		||||
from tractor.msg import (
 | 
			
		||||
    _codec,
 | 
			
		||||
    _exts,
 | 
			
		||||
)
 | 
			
		||||
from tractor.msg.types import (
 | 
			
		||||
    log,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,13 +48,22 @@ class PldMsg(
 | 
			
		|||
    #      case of these details?
 | 
			
		||||
    #
 | 
			
		||||
    # https://jcristharif.com/msgspec/structs.html#tagged-unions
 | 
			
		||||
    # tag=True,
 | 
			
		||||
    # tag_field='msg_type',
 | 
			
		||||
    tag=True,
 | 
			
		||||
    tag_field='msg_type',
 | 
			
		||||
):
 | 
			
		||||
    field: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
maybe_msg_spec = PldMsg|None
 | 
			
		||||
class Msg1(PldMsg):
 | 
			
		||||
    field: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Msg2(PldMsg):
 | 
			
		||||
    field: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AnyFieldMsg(PldMsg):
 | 
			
		||||
    field: Any
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@acm
 | 
			
		||||
| 
						 | 
				
			
			@ -104,9 +120,15 @@ async def maybe_expect_raises(
 | 
			
		|||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context(
 | 
			
		||||
    pld_spec=maybe_msg_spec,
 | 
			
		||||
)
 | 
			
		||||
# NOTE, this decorator is applied dynamically by both the root and
 | 
			
		||||
# 'sub' actor such that we can dynamically apply various cases from
 | 
			
		||||
# a parametrized test.
 | 
			
		||||
#
 | 
			
		||||
# maybe_msg_spec = PldMsg|None
 | 
			
		||||
#
 | 
			
		||||
# @tractor.context(
 | 
			
		||||
#     pld_spec=maybe_msg_spec,
 | 
			
		||||
# )
 | 
			
		||||
async def child(
 | 
			
		||||
    ctx: Context,
 | 
			
		||||
    started_value: int|PldMsg|None,
 | 
			
		||||
| 
						 | 
				
			
			@ -114,13 +136,13 @@ async def child(
 | 
			
		|||
    validate_pld_spec: bool,
 | 
			
		||||
    raise_on_started_mte: bool = True,
 | 
			
		||||
 | 
			
		||||
    pack_any_field: bool = False,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    Call ``Context.started()`` more then once (an error).
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    expect_started_mte: bool = started_value == 10
 | 
			
		||||
 | 
			
		||||
    # sanaity check that child RPC context is the current one
 | 
			
		||||
    curr_ctx: Context = current_ipc_ctx()
 | 
			
		||||
    assert ctx is curr_ctx
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +150,7 @@ async def child(
 | 
			
		|||
    rx: msgops.PldRx = ctx._pld_rx
 | 
			
		||||
    curr_pldec: _codec.MsgDec = rx.pld_dec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ctx_meta: dict = getattr(
 | 
			
		||||
        child,
 | 
			
		||||
        '_tractor_context_meta',
 | 
			
		||||
| 
						 | 
				
			
			@ -136,10 +159,28 @@ async def child(
 | 
			
		|||
    if ctx_meta:
 | 
			
		||||
        assert (
 | 
			
		||||
            ctx_meta['pld_spec']
 | 
			
		||||
            is curr_pldec.spec
 | 
			
		||||
            is curr_pldec.pld_spec
 | 
			
		||||
            is
 | 
			
		||||
            curr_pldec.spec
 | 
			
		||||
            is
 | 
			
		||||
            curr_pldec.pld_spec
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    pld_types: set[Type] = _codec.unpack_spec_types(
 | 
			
		||||
        curr_pldec.pld_spec,
 | 
			
		||||
    )
 | 
			
		||||
    if (
 | 
			
		||||
        AnyFieldMsg in pld_types
 | 
			
		||||
        and
 | 
			
		||||
        pack_any_field
 | 
			
		||||
    ):
 | 
			
		||||
        started_value = AnyFieldMsg(field=started_value)
 | 
			
		||||
 | 
			
		||||
    expect_started_mte: bool = (
 | 
			
		||||
        started_value == 10
 | 
			
		||||
        and
 | 
			
		||||
        not pack_any_field
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # 2 cases: hdndle send-side and recv-only validation
 | 
			
		||||
    # - when `raise_on_started_mte == True`, send validate
 | 
			
		||||
    # - else, parent-recv-side only validation
 | 
			
		||||
| 
						 | 
				
			
			@ -219,16 +260,65 @@ async def child(
 | 
			
		|||
    # msg-type-error from this RPC task ;)
 | 
			
		||||
    return return_value
 | 
			
		||||
 | 
			
		||||
def decorate_child_ep(
 | 
			
		||||
    pld_spec: Union[Type],
 | 
			
		||||
) -> types.ModuleType:
 | 
			
		||||
    '''
 | 
			
		||||
    Apply parametrized pld_spec to ctx ep like,
 | 
			
		||||
 | 
			
		||||
        @tractor.context(
 | 
			
		||||
            pld_spec=maybe_msg_spec,
 | 
			
		||||
        )(child)
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    this_mod = sys.modules[__name__]
 | 
			
		||||
    global child  # a mod-fn defined above
 | 
			
		||||
    assert this_mod.child is child
 | 
			
		||||
    this_mod.child = tractor.context(
 | 
			
		||||
        pld_spec=pld_spec,
 | 
			
		||||
    )(child)
 | 
			
		||||
    return this_mod
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context
 | 
			
		||||
async def set_chld_pldspec(
 | 
			
		||||
    ctx: tractor.Context,
 | 
			
		||||
    pld_spec_strs: list[str],
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Dynamically apply the `@context(pld_spec=pld_spec)` deco to the
 | 
			
		||||
    current actor's in-mem instance of this test module.
 | 
			
		||||
 | 
			
		||||
    Allows dynamically applying the "payload-spec" in both a parent
 | 
			
		||||
    and child actor after spawn.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    this_mod = sys.modules[__name__]
 | 
			
		||||
    pld_spec: list[str] = _exts.dec_type_union(
 | 
			
		||||
        pld_spec_strs,
 | 
			
		||||
        mods=[
 | 
			
		||||
            this_mod,
 | 
			
		||||
            msgspec.inspect,
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    decorate_child_ep(pld_spec)
 | 
			
		||||
    await ctx.started()
 | 
			
		||||
    await trio.sleep_forever()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    'return_value',
 | 
			
		||||
    [
 | 
			
		||||
        'yo',
 | 
			
		||||
        None,
 | 
			
		||||
        Msg2(field=10),
 | 
			
		||||
        AnyFieldMsg(field='yo'),
 | 
			
		||||
    ],
 | 
			
		||||
    ids=[
 | 
			
		||||
        'return[invalid-"yo"]',
 | 
			
		||||
        'return[valid-None]',
 | 
			
		||||
        'return[maybe-valid-None]',
 | 
			
		||||
        'return[maybe-valid-Msg2]',
 | 
			
		||||
        'return[maybe-valid-any-packed-yo]',
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
| 
						 | 
				
			
			@ -236,10 +326,14 @@ async def child(
 | 
			
		|||
    [
 | 
			
		||||
        10,
 | 
			
		||||
        PldMsg(field='yo'),
 | 
			
		||||
        Msg1(field='yo'),
 | 
			
		||||
        AnyFieldMsg(field=10),
 | 
			
		||||
    ],
 | 
			
		||||
    ids=[
 | 
			
		||||
        'Started[invalid-10]',
 | 
			
		||||
        'Started[valid-PldMsg]',
 | 
			
		||||
        'Started[maybe-valid-PldMsg]',
 | 
			
		||||
        'Started[maybe-valid-Msg1]',
 | 
			
		||||
        'Started[maybe-valid-any-packed-10]',
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
| 
						 | 
				
			
			@ -253,12 +347,31 @@ async def child(
 | 
			
		|||
        'no-started-pld-validate',
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    'pld_spec',
 | 
			
		||||
    [
 | 
			
		||||
        PldMsg|None,
 | 
			
		||||
 | 
			
		||||
        # demo how to have strict msgs alongside all other supported
 | 
			
		||||
        # py-types by embedding the any-types inside a shuttle msg.
 | 
			
		||||
        Msg1|Msg2|AnyFieldMsg,
 | 
			
		||||
 | 
			
		||||
        # XXX, will never work since Struct overrides dict.
 | 
			
		||||
        # https://jcristharif.com/msgspec/usage.html#typed-decoding
 | 
			
		||||
        # Msg1|Msg2|msgspec.inspect.AnyType,
 | 
			
		||||
    ],
 | 
			
		||||
    ids=[
 | 
			
		||||
        'maybe_PldMsg_spec',
 | 
			
		||||
        'Msg1_or_Msg2_or_AnyFieldMsg_spec',
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
def test_basic_payload_spec(
 | 
			
		||||
    debug_mode: bool,
 | 
			
		||||
    loglevel: str,
 | 
			
		||||
    return_value: str|None,
 | 
			
		||||
    started_value: int|PldMsg,
 | 
			
		||||
    pld_check_started_value: bool,
 | 
			
		||||
    pld_spec: Union[Type],
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Validate the most basic `PldRx` msg-type-spec semantics around
 | 
			
		||||
| 
						 | 
				
			
			@ -267,16 +380,33 @@ def test_basic_payload_spec(
 | 
			
		|||
    pld-spec.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    invalid_return: bool = return_value == 'yo'
 | 
			
		||||
    invalid_started: bool = started_value == 10
 | 
			
		||||
    pld_types: set[Type] = _codec.unpack_spec_types(pld_spec)
 | 
			
		||||
    invalid_return: bool = (
 | 
			
		||||
        return_value == 'yo'
 | 
			
		||||
    )
 | 
			
		||||
    invalid_started: bool = (
 | 
			
		||||
        started_value == 10
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # dynamically apply ep's pld-spec in 'root'.
 | 
			
		||||
    decorate_child_ep(pld_spec)
 | 
			
		||||
    assert (
 | 
			
		||||
        child._tractor_context_meta['pld_spec'] == pld_spec
 | 
			
		||||
    )
 | 
			
		||||
    pld_spec_strs: list[str] = _exts.enc_type_union(
 | 
			
		||||
        pld_spec,
 | 
			
		||||
    )
 | 
			
		||||
    assert len(pld_types) > 1
 | 
			
		||||
 | 
			
		||||
    async def main():
 | 
			
		||||
        nonlocal pld_spec
 | 
			
		||||
 | 
			
		||||
        async with tractor.open_nursery(
 | 
			
		||||
            debug_mode=debug_mode,
 | 
			
		||||
            loglevel=loglevel,
 | 
			
		||||
        ) as an:
 | 
			
		||||
            p: Portal = await an.start_actor(
 | 
			
		||||
                'child',
 | 
			
		||||
                'sub',
 | 
			
		||||
                enable_modules=[__name__],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -286,9 +416,11 @@ def test_basic_payload_spec(
 | 
			
		|||
            if invalid_started:
 | 
			
		||||
                msg_type_str: str = 'Started'
 | 
			
		||||
                bad_value: int = 10
 | 
			
		||||
 | 
			
		||||
            elif invalid_return:
 | 
			
		||||
                msg_type_str: str = 'Return'
 | 
			
		||||
                bad_value: str = 'yo'
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                # XXX but should never be used below then..
 | 
			
		||||
                msg_type_str: str = ''
 | 
			
		||||
| 
						 | 
				
			
			@ -302,6 +434,7 @@ def test_basic_payload_spec(
 | 
			
		|||
                    invalid_started
 | 
			
		||||
                ) else None
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            async with (
 | 
			
		||||
                maybe_expect_raises(
 | 
			
		||||
                    raises=should_raise,
 | 
			
		||||
| 
						 | 
				
			
			@ -315,6 +448,11 @@ def test_basic_payload_spec(
 | 
			
		|||
                    # only for debug
 | 
			
		||||
                    # post_mortem=True,
 | 
			
		||||
                ),
 | 
			
		||||
                p.open_context(
 | 
			
		||||
                    set_chld_pldspec,
 | 
			
		||||
                    pld_spec_strs=pld_spec_strs,
 | 
			
		||||
                ) as (deco_ctx, _),
 | 
			
		||||
 | 
			
		||||
                p.open_context(
 | 
			
		||||
                    child,
 | 
			
		||||
                    return_value=return_value,
 | 
			
		||||
| 
						 | 
				
			
			@ -325,12 +463,18 @@ def test_basic_payload_spec(
 | 
			
		|||
                # now opened with 'child' sub
 | 
			
		||||
                assert current_ipc_ctx() is ctx
 | 
			
		||||
 | 
			
		||||
                assert type(first) is PldMsg
 | 
			
		||||
                # assert type(first) is PldMsg
 | 
			
		||||
                assert isinstance(first, PldMsg)
 | 
			
		||||
                assert first.field == 'yo'
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    res: None|PldMsg = await ctx.result(hide_tb=False)
 | 
			
		||||
                    assert res is None
 | 
			
		||||
                    assert res == return_value
 | 
			
		||||
                    if res is None:
 | 
			
		||||
                        await tractor.pause()
 | 
			
		||||
                    if isinstance(res, PldMsg):
 | 
			
		||||
                        assert res.field == 10
 | 
			
		||||
 | 
			
		||||
                except MsgTypeError as mte:
 | 
			
		||||
                    maybe_mte = mte
 | 
			
		||||
                    if not invalid_return:
 | 
			
		||||
| 
						 | 
				
			
			@ -356,6 +500,9 @@ def test_basic_payload_spec(
 | 
			
		|||
                        ctx.outcome
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                if should_raise is None:
 | 
			
		||||
                    await deco_ctx.cancel()
 | 
			
		||||
 | 
			
		||||
            if should_raise is None:
 | 
			
		||||
                assert maybe_mte is None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +181,11 @@ class MsgDec(Struct):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def mk_dec(
 | 
			
		||||
    spec: Union[Type[Struct]]|Type|None,
 | 
			
		||||
    spec: (
 | 
			
		||||
        Union[Type[Struct]]
 | 
			
		||||
        |Type  # lone type
 | 
			
		||||
        |None # implying `Union[*ext_types]|None`
 | 
			
		||||
    ),
 | 
			
		||||
 | 
			
		||||
    # NOTE, required for ad-hoc type extensions to the underlying
 | 
			
		||||
    # serialization proto (which is default `msgpack`),
 | 
			
		||||
| 
						 | 
				
			
			@ -194,16 +198,18 @@ def mk_dec(
 | 
			
		|||
    Create an IPC msg decoder, a slightly higher level wrapper around
 | 
			
		||||
    a `msgspec.msgpack.Decoder` which provides,
 | 
			
		||||
 | 
			
		||||
    - easier introspection of the underlying type spec via
 | 
			
		||||
      the `.spec` and `.spec_str` attrs,
 | 
			
		||||
    - easier introspection of the underlying type spec via the
 | 
			
		||||
      `.spec` and `.spec_str` attrs,
 | 
			
		||||
    - `.hook` access to the `Decoder.dec_hook()`,
 | 
			
		||||
    - automatic custom extension-types decode support when
 | 
			
		||||
      `dec_hook()` is provided such that any `PayloadMsg.pld` tagged
 | 
			
		||||
      as a type from from `ext_types` (presuming the `MsgCodec.encode()` also used
 | 
			
		||||
      a `.enc_hook()`) is processed and constructed by a `PldRx` implicitily.
 | 
			
		||||
      as a type from from `ext_types` (presuming the
 | 
			
		||||
      `MsgCodec.encode()` also used a `.enc_hook()`) is processed and
 | 
			
		||||
      constructed by a `PldRx` implicitily.
 | 
			
		||||
 | 
			
		||||
    NOTE, as mentioned a `MsgDec` is normally used for `PayloadMsg.pld: PayloadT` field
 | 
			
		||||
    decoding inside an IPC-ctx-oriented `PldRx`.
 | 
			
		||||
    NOTE, as mentioned a `MsgDec` is normally used for
 | 
			
		||||
    `PayloadMsg.pld: PayloadT` field decoding inside an
 | 
			
		||||
    IPC-ctx-oriented `PldRx`.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    if (
 | 
			
		||||
| 
						 | 
				
			
			@ -248,12 +254,16 @@ def mk_dec(
 | 
			
		|||
        # will work? kk B)
 | 
			
		||||
        #
 | 
			
		||||
        # maybe_box_struct = mk_boxed_ext_struct(ext_types)
 | 
			
		||||
        spec = Raw | Union[*ext_types]
 | 
			
		||||
 | 
			
		||||
        spec = spec | Union[*ext_types]
 | 
			
		||||
 | 
			
		||||
    return MsgDec(
 | 
			
		||||
        _dec=msgpack.Decoder(
 | 
			
		||||
            type=spec,  # like `MsgType[Any]`
 | 
			
		||||
            type=spec,
 | 
			
		||||
            dec_hook=dec_hook,
 | 
			
		||||
            # ?TODO, support it?
 | 
			
		||||
            # https://jcristharif.com/msgspec/usage.html#strict-vs-lax-mode
 | 
			
		||||
            # strict=False,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,9 +33,7 @@ converters,
 | 
			
		|||
  |_ https://jcristharif.com/msgspec/changelog.html
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from types import (
 | 
			
		||||
    ModuleType,
 | 
			
		||||
)
 | 
			
		||||
import types
 | 
			
		||||
import typing
 | 
			
		||||
from typing import (
 | 
			
		||||
    Type,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,35 +42,51 @@ from typing import (
 | 
			
		|||
 | 
			
		||||
def dec_type_union(
 | 
			
		||||
    type_names: list[str],
 | 
			
		||||
    mods: list[ModuleType] = []
 | 
			
		||||
    mods: list[types.ModuleType] = []
 | 
			
		||||
) -> Type|Union[Type]:
 | 
			
		||||
    '''
 | 
			
		||||
    Look up types by name, compile into a list and then create and
 | 
			
		||||
    return a `typing.Union` from the full set.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # import importlib
 | 
			
		||||
    types: list[Type] = []
 | 
			
		||||
    _types: list[Type] = []
 | 
			
		||||
    for type_name in type_names:
 | 
			
		||||
        for mod in [
 | 
			
		||||
            typing,
 | 
			
		||||
            # importlib.import_module(__name__),
 | 
			
		||||
            types,
 | 
			
		||||
        ] + mods:
 | 
			
		||||
            if type_ref := getattr(
 | 
			
		||||
                mod,
 | 
			
		||||
                type_name,
 | 
			
		||||
                False,
 | 
			
		||||
            ):
 | 
			
		||||
                types.append(type_ref)
 | 
			
		||||
                _types.append(type_ref)
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    # special case handling only..
 | 
			
		||||
    # ipc_pld_spec: Union[Type] = eval(
 | 
			
		||||
    #     pld_spec_str,
 | 
			
		||||
    #     {},  # globals
 | 
			
		||||
    #     {'typing': typing},  # locals
 | 
			
		||||
    # )
 | 
			
		||||
    report: str = ''
 | 
			
		||||
    if not _types:
 | 
			
		||||
        report: str = 'No type-instances could be resolved from `type_names` ??\n'
 | 
			
		||||
 | 
			
		||||
    return Union[*types]
 | 
			
		||||
    elif len(type_names) != len(_types):
 | 
			
		||||
        report: str = (
 | 
			
		||||
            f'Some type-instances could not be resolved from `type_names` ??\n'
 | 
			
		||||
            f'_types: {_types!r}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if report:
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            report
 | 
			
		||||
            +
 | 
			
		||||
            f'type_names: {type_names!r}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if not _types:
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            f'No type-instance could be resolved from `type_names` ??\n'
 | 
			
		||||
            f'type_names: {type_names!r}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Union[*_types]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def enc_type_union(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ class PldRx(Struct):
 | 
			
		|||
    def limit_plds(
 | 
			
		||||
        self,
 | 
			
		||||
        spec: Union[Type[Struct]],
 | 
			
		||||
        **dec_kwargs,
 | 
			
		||||
        **mk_dec_kwargs,
 | 
			
		||||
 | 
			
		||||
    ) -> MsgDec:
 | 
			
		||||
        '''
 | 
			
		||||
| 
						 | 
				
			
			@ -135,7 +135,7 @@ class PldRx(Struct):
 | 
			
		|||
        orig_dec: MsgDec = self._pld_dec
 | 
			
		||||
        limit_dec: MsgDec = mk_dec(
 | 
			
		||||
            spec=spec,
 | 
			
		||||
            **dec_kwargs,
 | 
			
		||||
            **mk_dec_kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        try:
 | 
			
		||||
            self._pld_dec = limit_dec
 | 
			
		||||
| 
						 | 
				
			
			@ -582,6 +582,7 @@ async def drain_to_final_msg(
 | 
			
		|||
    even after ctx closure and the `.open_context()` block exit.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
    raise_overrun: bool = not ctx._allow_overruns
 | 
			
		||||
    parent_never_opened_stream: bool = ctx._stream is None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -834,7 +835,8 @@ async def drain_to_final_msg(
 | 
			
		|||
            f'{ctx.outcome}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
    # ?TODO? why was this here and not above?
 | 
			
		||||
    # __tracebackhide__: bool = hide_tb
 | 
			
		||||
    return (
 | 
			
		||||
        result_msg,
 | 
			
		||||
        pre_result_drained,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1703,7 +1703,7 @@ def run_as_asyncio_guest(
 | 
			
		|||
                # asyncio.CancelledError,
 | 
			
		||||
                # ^^XXX `.shield()` call above prevents this??
 | 
			
		||||
 | 
			
		||||
            )as state_err:
 | 
			
		||||
            ) as state_err:
 | 
			
		||||
 | 
			
		||||
                # XXX be super dupere noisy about abandonment issues!
 | 
			
		||||
                aio_task: asyncio.Task = asyncio.current_task()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue