diff --git a/nooz/295.misc.rst b/nooz/295.misc.rst new file mode 100644 index 0000000..1724e06 --- /dev/null +++ b/nooz/295.misc.rst @@ -0,0 +1,3 @@ +Add an experimental `tractor.msg.NamespacePath` type for passing Python +objects by "reference" through a ``str``-subtype message and using the +new ``pkgutil.resolve_name()`` for reference loading. diff --git a/tractor/_portal.py b/tractor/_portal.py index 1dc6ee4..672c9af 100644 --- a/tractor/_portal.py +++ b/tractor/_portal.py @@ -14,11 +14,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -""" +''' Memory boundary "Portals": an API for structured concurrency linked tasks running in disparate memory domains. -""" +''' +from __future__ import annotations import importlib import inspect from typing import ( @@ -36,6 +37,7 @@ from async_generator import asynccontextmanager from ._state import current_actor from ._ipc import Channel from .log import get_logger +from .msg import NamespacePath from ._exceptions import ( unpack_error, NoResult, @@ -66,13 +68,6 @@ async def maybe_open_nursery( yield nursery -def func_deats(func: Callable) -> tuple[str, str]: - return ( - func.__module__, - func.__name__, - ) - - def _unwrap_msg( msg: dict[str, Any], @@ -86,6 +81,7 @@ def _unwrap_msg( assert msg.get('cid'), "Received internal error at portal?" raise unpack_error(msg, channel) + class MessagingError(Exception): 'Some kind of unexpected SC messaging dialog issue' @@ -316,7 +312,7 @@ class Portal: raise TypeError( f'{func} must be a non-streaming async function!') - fn_mod_path, fn_name = func_deats(func) + fn_mod_path, fn_name = NamespacePath.from_ref(func).to_tuple() ctx = await self.actor.start_remote_task( self.channel, @@ -346,7 +342,8 @@ class Portal: raise TypeError( f'{async_gen_func} must be an async generator function!') - fn_mod_path, fn_name = func_deats(async_gen_func) + fn_mod_path, fn_name = NamespacePath.from_ref( + async_gen_func).to_tuple() ctx = await self.actor.start_remote_task( self.channel, fn_mod_path, @@ -412,7 +409,7 @@ class Portal: raise TypeError( f'{func} must be an async generator function!') - fn_mod_path, fn_name = func_deats(func) + fn_mod_path, fn_name = NamespacePath.from_ref(func).to_tuple() ctx = await self.actor.start_remote_task( self.channel, @@ -430,7 +427,7 @@ class Portal: first = msg['started'] ctx._started_called = True - except KeyError as kerr: + except KeyError: assert msg.get('cid'), ("Received internal error at context?") if msg.get('error'): diff --git a/tractor/msg.py b/tractor/msg.py index 16f4e5c..138718b 100644 --- a/tractor/msg.py +++ b/tractor/msg.py @@ -15,6 +15,66 @@ # along with this program. If not, see . ''' -Coming soon! +Built-in messaging patterns, types, APIs and helpers. ''' + +# TODO: integration with our ``enable_modules: list[str]`` caps sys. + +# ``pkgutil.resolve_name()`` internally uses +# ``importlib.import_module()`` which can be filtered by inserting +# a ``MetaPathFinder`` into ``sys.meta_path`` (which we could do before +# entering the ``Actor._process_messages()`` loop). +# - https://github.com/python/cpython/blob/main/Lib/pkgutil.py#L645 +# - https://stackoverflow.com/questions/1350466/preventing-python-code-from-importing-certain-modules +# - https://stackoverflow.com/a/63320902 +# - https://docs.python.org/3/library/sys.html#sys.meta_path + +# the new "Implicit Namespace Packages" might be relevant? +# - https://www.python.org/dev/peps/pep-0420/ + +# add implicit serialized message type support so that paths can be +# handed directly to IPC primitives such as streams and `Portal.run()` +# calls: +# - via ``msgspec``: +# - https://jcristharif.com/msgspec/api.html#struct +# - https://jcristharif.com/msgspec/extending.html +# via ``msgpack-python``: +# - https://github.com/msgpack/msgpack-python#packingunpacking-of-custom-data-type + +from __future__ import annotations +from pkgutil import resolve_name + + +class NamespacePath(str): + ''' + A serializeable description of a (function) Python object location + described by the target's module path and namespace key meant as + a message-native "packet" to allows actors to point-and-load objects + by absolute reference. + + ''' + _ref: object = None + + def load_ref(self) -> object: + if self._ref is None: + self._ref = resolve_name(self) + return self._ref + + def to_tuple( + self, + + ) -> tuple[str, str]: + ref = self.load_ref() + return ref.__module__, getattr(ref, '__name__', '') + + @classmethod + def from_ref( + cls, + ref, + + ) -> NamespacePath: + return cls(':'.join( + (ref.__module__, + getattr(ref, '__name__', '')) + ))