diff --git a/Pyro5/api.py b/Pyro5/api.py index eaef1aa..cb5dd61 100644 --- a/Pyro5/api.py +++ b/Pyro5/api.py @@ -11,8 +11,8 @@ from . import __version__ from .configure import global_config as config from .core import URI, locate_ns, resolve, type_meta -from .client import Proxy, BatchProxy, SerializedBlob -from .server import Daemon, DaemonObject, callback, expose, behavior, oneway, serve +from .client import Proxy, BatchProxy, ConcurrentProxy, SerializedBlob +from .server import Daemon, DaemonObject, callback, expose, behavior, oneway, serve, Functor from .nameserver import start_ns, start_ns_loop from .serializers import SerializerBase from .callcontext import current_context @@ -24,7 +24,7 @@ __all__ = ["config", "URI", "locate_ns", "resolve", "type_meta", "current_context", - "Proxy", "BatchProxy", "SerializedBlob", "SerializerBase", - "Daemon", "DaemonObject", "callback", "expose", "behavior", "oneway", + "Proxy", "BatchProxy", "ConcurrentProxy", "SerializedBlob", "SerializerBase", + "Daemon", "DaemonObject", "callback", "expose", "behavior", "oneway", "Functor", "start_ns", "start_ns_loop", "serve", "register_dict_to_class", "register_class_to_dict", "unregister_dict_to_class", "unregister_class_to_dict"] diff --git a/Pyro5/client.py b/Pyro5/client.py index ee0e599..1333031 100644 --- a/Pyro5/client.py +++ b/Pyro5/client.py @@ -9,6 +9,8 @@ import logging import serpent import contextlib +from threading import local +from collections import defaultdict from . import config, core, serializers, protocol, errors, socketutil from .callcontext import current_context try: @@ -180,6 +182,7 @@ def __dir__(self): # obj.__getitem__(index)), the special methods are not looked up via __getattr__ # for efficiency reasons; instead, their presence is checked directly. # Thus we need to define them here to force (remote) lookup through __getitem__. + def __call__(self, *args, **kwargs): return self.__getattr__('__call__')(*args, **kwargs) def __bool__(self): return True def __len__(self): return self.__getattr__('__len__')() def __getitem__(self, index): return self.__getattr__('__getitem__')(index) @@ -626,7 +629,24 @@ def _pyroInvoke(self, name, args, kwargs): results = self.__proxy._pyroInvokeBatch(self.__calls) self.__calls = [] # clear for re-use return self.__resultsgenerator(results) + +class ConcurrentProxy(Proxy): + """ + Proxy for remote python objects. The `Proxy` must be explicitly passed across threads. This class handles automatically + creating new proxies for the current thread when necessary. + """ + THREAD_PROXY_MAP = defaultdict(local) + def __init__(self, uri: str, **kwargs): + super().__init__(uri, **kwargs) + ConcurrentProxy.THREAD_PROXY_MAP[self._pyroUri].proxy = self + def _pyroInvoke(self, methodname, vargs, kwargs, flags=0, objectId=None): + local_data = ConcurrentProxy.THREAD_PROXY_MAP[self._pyroUri] + if not hasattr(local_data, "proxy"): + local_data.proxy = self.__copy__() + return Proxy._pyroInvoke( + local_data.proxy, methodname, vargs, kwargs, flags, objectId + ) class SerializedBlob(object): """ diff --git a/Pyro5/server.py b/Pyro5/server.py index 6c91fda..934ea90 100644 --- a/Pyro5/server.py +++ b/Pyro5/server.py @@ -17,7 +17,7 @@ import weakref import serpent import ipaddress -from typing import TypeVar, Tuple, Union, Optional, Dict, Any, Sequence, Set +from typing import Generic, ParamSpec, TypeVar, Tuple, Union, Optional, Dict, Any, Sequence, Set from . import config, core, errors, serializers, socketutil, protocol, client from .callcontext import current_context from collections.abc import Callable @@ -29,7 +29,7 @@ _private_dunder_methods = frozenset([ "__init__", "__init_subclass__", "__class__", "__module__", "__weakref__", - "__call__", "__new__", "__del__", "__repr__", + "__new__", "__del__", "__repr__", "__str__", "__format__", "__nonzero__", "__bool__", "__coerce__", "__cmp__", "__eq__", "__ne__", "__hash__", "__ge__", "__gt__", "__le__", "__lt__", "__dir__", "__enter__", "__exit__", "__copy__", "__deepcopy__", "__sizeof__", @@ -138,6 +138,26 @@ def _behavior(clazz): return _behavior +ReturnType = TypeVar("ReturnType") +ParamsTypes = ParamSpec("ParamsTypes") + +@expose +class Functor(Generic[ParamsTypes, ReturnType]): + """ + A functor is a callable object that can be used as a function. + This is used to wrap functions that are not methods of a class. + """ + + def __init__(self, func: Callable[ParamsTypes, ReturnType]): + self.func = func + + @expose + def __call__( + self, *args: ParamsTypes.args, **kwargs: ParamsTypes.kwargs + ) -> ReturnType: + return self.func(*args, **kwargs) + + @expose class DaemonObject(object): """The part of the daemon that is exposed as a Pyro object.""" diff --git a/tests/test_server.py b/tests/test_server.py index da79784..ff81a6c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1032,7 +1032,6 @@ def testIsPrivateName(self): assert Pyro5.server.is_private_attribute("___p") assert not Pyro5.server.is_private_attribute("__dunder__") # dunder methods should not be private except a list of exceptions as tested below assert Pyro5.server.is_private_attribute("__init__") - assert Pyro5.server.is_private_attribute("__call__") assert Pyro5.server.is_private_attribute("__new__") assert Pyro5.server.is_private_attribute("__del__") assert Pyro5.server.is_private_attribute("__repr__")