Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NamespacePath: a message compatible "object reference" type #295

Merged
merged 8 commits into from
Jan 30, 2022
23 changes: 10 additions & 13 deletions tractor/_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
# 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/>.

"""
'''
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 (
Expand All @@ -36,6 +37,7 @@
from ._state import current_actor
from ._ipc import Channel
from .log import get_logger
from .msg import NamespacePath
from ._exceptions import (
unpack_error,
NoResult,
Expand Down Expand Up @@ -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],
Expand All @@ -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'

Expand Down Expand Up @@ -316,7 +312,7 @@ async def run(
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,
Expand Down Expand Up @@ -346,7 +342,8 @@ async def open_stream_from(
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,
Expand Down Expand Up @@ -412,7 +409,7 @@ async def open_context(
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,
Expand All @@ -430,7 +427,7 @@ async def open_context(
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'):
Expand Down
51 changes: 50 additions & 1 deletion tractor/msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,55 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

'''
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/

from __future__ import annotations
from pkgutil import resolve_name


class NamespacePath(str):
'''
A serializeable description of a (function) Python object location
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm maybe update this?

described by the target's module path and its namespace key.

'''
_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__', ''))
))