pyzmq/zmq/decorators.py

191 lines
5.0 KiB
Python

"""Decorators for running functions with context/sockets.
.. versionadded:: 15.3
Like using Contexts and Sockets as context managers, but with decorator syntax.
Context and sockets are closed at the end of the function.
For example::
from zmq.decorators import context, socket
@context()
@socket(zmq.PUSH)
def work(ctx, push):
...
"""
from __future__ import annotations
# Copyright (c) PyZMQ Developers.
# Distributed under the terms of the Modified BSD License.
__all__ = (
'context',
'socket',
)
from functools import wraps
import zmq
class _Decorator:
'''The mini decorator factory'''
def __init__(self, target=None):
self._target = target
def __call__(self, *dec_args, **dec_kwargs):
"""
The main logic of decorator
Here is how those arguments works::
@out_decorator(*dec_args, *dec_kwargs)
def func(*wrap_args, **wrap_kwargs):
...
And in the ``wrapper``, we simply create ``self.target`` instance via
``with``::
target = self.get_target(*args, **kwargs)
with target(*dec_args, **dec_kwargs) as obj:
...
"""
kw_name, dec_args, dec_kwargs = self.process_decorator_args(
*dec_args, **dec_kwargs
)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
target = self.get_target(*args, **kwargs)
with target(*dec_args, **dec_kwargs) as obj:
# insert our object into args
if kw_name and kw_name not in kwargs:
kwargs[kw_name] = obj
elif kw_name and kw_name in kwargs:
raise TypeError(
f"{func.__name__}() got multiple values for"
f" argument '{kw_name}'"
)
else:
args = args + (obj,)
return func(*args, **kwargs)
return wrapper
return decorator
def get_target(self, *args, **kwargs):
"""Return the target function
Allows modifying args/kwargs to be passed.
"""
return self._target
def process_decorator_args(self, *args, **kwargs):
"""Process args passed to the decorator.
args not consumed by the decorator will be passed to the target factory
(Context/Socket constructor).
"""
kw_name = None
if isinstance(kwargs.get('name'), str):
kw_name = kwargs.pop('name')
elif len(args) >= 1 and isinstance(args[0], str):
kw_name = args[0]
args = args[1:]
return kw_name, args, kwargs
class _ContextDecorator(_Decorator):
"""Decorator subclass for Contexts"""
def __init__(self):
super().__init__(zmq.Context)
class _SocketDecorator(_Decorator):
"""Decorator subclass for sockets
Gets the context from other args.
"""
def process_decorator_args(self, *args, **kwargs):
"""Also grab context_name out of kwargs"""
kw_name, args, kwargs = super().process_decorator_args(*args, **kwargs)
self.context_name = kwargs.pop('context_name', 'context')
return kw_name, args, kwargs
def get_target(self, *args, **kwargs):
"""Get context, based on call-time args"""
context = self._get_context(*args, **kwargs)
return context.socket
def _get_context(self, *args, **kwargs):
"""
Find the ``zmq.Context`` from ``args`` and ``kwargs`` at call time.
First, if there is an keyword argument named ``context`` and it is a
``zmq.Context`` instance , we will take it.
Second, we check all the ``args``, take the first ``zmq.Context``
instance.
Finally, we will provide default Context -- ``zmq.Context.instance``
:return: a ``zmq.Context`` instance
"""
if self.context_name in kwargs:
ctx = kwargs[self.context_name]
if isinstance(ctx, zmq.Context):
return ctx
for arg in args:
if isinstance(arg, zmq.Context):
return arg
# not specified by any decorator
return zmq.Context.instance()
def context(*args, **kwargs):
"""Decorator for adding a Context to a function.
Usage::
@context()
def foo(ctx):
...
.. versionadded:: 15.3
:param str name: the keyword argument passed to decorated function
"""
return _ContextDecorator()(*args, **kwargs)
def socket(*args, **kwargs):
"""Decorator for adding a socket to a function.
Usage::
@socket(zmq.PUSH)
def foo(push):
...
.. versionadded:: 15.3
:param str name: the keyword argument passed to decorated function
:param str context_name: the keyword only argument to identify context
object
"""
return _SocketDecorator()(*args, **kwargs)