mirror of https://github.com/zeromq/pyzmq.git
423 lines
14 KiB
Python
423 lines
14 KiB
Python
"""Python bindings for 0MQ."""
|
|
|
|
# Copyright (C) PyZMQ Developers
|
|
# Distributed under the terms of the Modified BSD License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import atexit
|
|
import os
|
|
from threading import Lock
|
|
from typing import Any, Callable, Generic, TypeVar, overload
|
|
from warnings import warn
|
|
from weakref import WeakSet
|
|
|
|
import zmq
|
|
from zmq._typing import TypeAlias
|
|
from zmq.backend import Context as ContextBase
|
|
from zmq.constants import ContextOption, Errno, SocketOption
|
|
from zmq.error import ZMQError
|
|
from zmq.utils.interop import cast_int_addr
|
|
|
|
from .attrsettr import AttributeSetter, OptValT
|
|
from .socket import Socket, SyncSocket
|
|
|
|
# notice when exiting, to avoid triggering term on exit
|
|
_exiting = False
|
|
|
|
|
|
def _notice_atexit() -> None:
|
|
global _exiting
|
|
_exiting = True
|
|
|
|
|
|
atexit.register(_notice_atexit)
|
|
|
|
_ContextType = TypeVar('_ContextType', bound='Context')
|
|
_SocketType = TypeVar('_SocketType', bound='Socket', covariant=True)
|
|
|
|
|
|
class Context(ContextBase, AttributeSetter, Generic[_SocketType]):
|
|
"""Create a zmq Context
|
|
|
|
A zmq Context creates sockets via its ``ctx.socket`` method.
|
|
|
|
.. versionchanged:: 24
|
|
|
|
When using a Context as a context manager (``with zmq.Context()``),
|
|
or deleting a context without closing it first,
|
|
``ctx.destroy()`` is called,
|
|
closing any leftover sockets,
|
|
instead of `ctx.term()` which requires sockets to be closed first.
|
|
|
|
This prevents hangs caused by `ctx.term()` if sockets are left open,
|
|
but means that unclean destruction of contexts
|
|
(with sockets left open) is not safe
|
|
if sockets are managed in other threads.
|
|
|
|
.. versionadded:: 25
|
|
|
|
Contexts can now be shadowed by passing another Context.
|
|
This helps in creating an async copy of a sync context or vice versa::
|
|
|
|
ctx = zmq.Context(async_ctx)
|
|
|
|
Which previously had to be::
|
|
|
|
ctx = zmq.Context.shadow(async_ctx.underlying)
|
|
"""
|
|
|
|
sockopts: dict[int, Any]
|
|
_instance: Any = None
|
|
_instance_lock = Lock()
|
|
_instance_pid: int | None = None
|
|
_shadow = False
|
|
_shadow_obj = None
|
|
_warn_destroy_close = False
|
|
_sockets: WeakSet
|
|
# mypy doesn't like a default value here
|
|
_socket_class: type[_SocketType] = Socket # type: ignore
|
|
|
|
@overload
|
|
def __init__(self: SyncContext, io_threads: int = 1): ...
|
|
|
|
@overload
|
|
def __init__(self: SyncContext, io_threads: Context):
|
|
# this should be positional-only, but that requires 3.8
|
|
...
|
|
|
|
@overload
|
|
def __init__(self: SyncContext, *, shadow: Context | int): ...
|
|
|
|
def __init__(
|
|
self: SyncContext,
|
|
io_threads: int | Context = 1,
|
|
shadow: Context | int = 0,
|
|
) -> None:
|
|
if isinstance(io_threads, Context):
|
|
# allow positional shadow `zmq.Context(zmq.asyncio.Context())`
|
|
# this s
|
|
shadow = io_threads
|
|
io_threads = 1
|
|
|
|
shadow_address: int = 0
|
|
if shadow:
|
|
self._shadow = True
|
|
# hold a reference to the shadow object
|
|
self._shadow_obj = shadow
|
|
if not isinstance(shadow, int):
|
|
try:
|
|
shadow = shadow.underlying
|
|
except AttributeError:
|
|
pass
|
|
shadow_address = cast_int_addr(shadow)
|
|
else:
|
|
self._shadow = False
|
|
super().__init__(io_threads=io_threads, shadow=shadow_address)
|
|
self.sockopts = {}
|
|
self._sockets = WeakSet()
|
|
|
|
def __del__(self) -> None:
|
|
"""Deleting a Context without closing it destroys it and all sockets.
|
|
|
|
.. versionchanged:: 24
|
|
Switch from threadsafe `term()` which hangs in the event of open sockets
|
|
to less safe `destroy()` which
|
|
warns about any leftover sockets and closes them.
|
|
"""
|
|
|
|
# Calling locals() here conceals issue #1167 on Windows CPython 3.5.4.
|
|
locals()
|
|
|
|
if not self._shadow and not _exiting and not self.closed:
|
|
self._warn_destroy_close = True
|
|
if warn is not None and getattr(self, "_sockets", None) is not None:
|
|
# warn can be None during process teardown
|
|
warn(
|
|
f"Unclosed context {self}",
|
|
ResourceWarning,
|
|
stacklevel=2,
|
|
source=self,
|
|
)
|
|
self.destroy()
|
|
|
|
_repr_cls = "zmq.Context"
|
|
|
|
def __repr__(self) -> str:
|
|
cls = self.__class__
|
|
# look up _repr_cls on exact class, not inherited
|
|
_repr_cls = cls.__dict__.get("_repr_cls", None)
|
|
if _repr_cls is None:
|
|
_repr_cls = f"{cls.__module__}.{cls.__name__}"
|
|
|
|
closed = ' closed' if self.closed else ''
|
|
if getattr(self, "_sockets", None):
|
|
n_sockets = len(self._sockets)
|
|
s = 's' if n_sockets > 1 else ''
|
|
sockets = f"{n_sockets} socket{s}"
|
|
else:
|
|
sockets = ""
|
|
return f"<{_repr_cls}({sockets}) at {hex(id(self))}{closed}>"
|
|
|
|
def __enter__(self: _ContextType) -> _ContextType:
|
|
return self
|
|
|
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
# warn about any leftover sockets before closing them
|
|
self._warn_destroy_close = True
|
|
self.destroy()
|
|
|
|
def __copy__(self: _ContextType, memo: Any = None) -> _ContextType:
|
|
"""Copying a Context creates a shadow copy"""
|
|
return self.__class__.shadow(self.underlying)
|
|
|
|
__deepcopy__ = __copy__
|
|
|
|
@classmethod
|
|
def shadow(cls: type[_ContextType], address: int | zmq.Context) -> _ContextType:
|
|
"""Shadow an existing libzmq context
|
|
|
|
address is a zmq.Context or an integer (or FFI pointer)
|
|
representing the address of the libzmq context.
|
|
|
|
.. versionadded:: 14.1
|
|
|
|
.. versionadded:: 25
|
|
Support for shadowing `zmq.Context` objects,
|
|
instead of just integer addresses.
|
|
"""
|
|
return cls(shadow=address)
|
|
|
|
@classmethod
|
|
def shadow_pyczmq(cls: type[_ContextType], ctx: Any) -> _ContextType:
|
|
"""Shadow an existing pyczmq context
|
|
|
|
ctx is the FFI `zctx_t *` pointer
|
|
|
|
.. versionadded:: 14.1
|
|
"""
|
|
from pyczmq import zctx # type: ignore
|
|
|
|
from zmq.utils.interop import cast_int_addr
|
|
|
|
underlying = zctx.underlying(ctx)
|
|
address = cast_int_addr(underlying)
|
|
return cls(shadow=address)
|
|
|
|
# static method copied from tornado IOLoop.instance
|
|
@classmethod
|
|
def instance(cls: type[_ContextType], io_threads: int = 1) -> _ContextType:
|
|
"""Returns a global Context instance.
|
|
|
|
Most single-process applications have a single, global Context.
|
|
Use this method instead of passing around Context instances
|
|
throughout your code.
|
|
|
|
A common pattern for classes that depend on Contexts is to use
|
|
a default argument to enable programs with multiple Contexts
|
|
but not require the argument for simpler applications::
|
|
|
|
class MyClass(object):
|
|
def __init__(self, context=None):
|
|
self.context = context or Context.instance()
|
|
|
|
.. versionchanged:: 18.1
|
|
|
|
When called in a subprocess after forking,
|
|
a new global instance is created instead of inheriting
|
|
a Context that won't work from the parent process.
|
|
"""
|
|
if (
|
|
cls._instance is None
|
|
or cls._instance_pid != os.getpid()
|
|
or cls._instance.closed
|
|
):
|
|
with cls._instance_lock:
|
|
if (
|
|
cls._instance is None
|
|
or cls._instance_pid != os.getpid()
|
|
or cls._instance.closed
|
|
):
|
|
cls._instance = cls(io_threads=io_threads)
|
|
cls._instance_pid = os.getpid()
|
|
return cls._instance
|
|
|
|
def term(self) -> None:
|
|
"""Close or terminate the context.
|
|
|
|
Context termination is performed in the following steps:
|
|
|
|
- Any blocking operations currently in progress on sockets open within context shall
|
|
raise :class:`zmq.ContextTerminated`.
|
|
With the exception of socket.close(), any further operations on sockets open within this context
|
|
shall raise :class:`zmq.ContextTerminated`.
|
|
- After interrupting all blocking calls, term shall block until the following conditions are satisfied:
|
|
- All sockets open within context have been closed.
|
|
- For each socket within context, all messages sent on the socket have either been
|
|
physically transferred to a network peer,
|
|
or the socket's linger period set with the zmq.LINGER socket option has expired.
|
|
|
|
For further details regarding socket linger behaviour refer to libzmq documentation for ZMQ_LINGER.
|
|
|
|
This can be called to close the context by hand. If this is not called,
|
|
the context will automatically be closed when it is garbage collected,
|
|
in which case you may see a ResourceWarning about the unclosed context.
|
|
"""
|
|
super().term()
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Hooks for ctxopt completion
|
|
# -------------------------------------------------------------------------
|
|
|
|
def __dir__(self) -> list[str]:
|
|
keys = dir(self.__class__)
|
|
keys.extend(ContextOption.__members__)
|
|
return keys
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Creating Sockets
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _add_socket(self, socket: Any) -> None:
|
|
"""Add a weakref to a socket for Context.destroy / reference counting"""
|
|
self._sockets.add(socket)
|
|
|
|
def _rm_socket(self, socket: Any) -> None:
|
|
"""Remove a socket for Context.destroy / reference counting"""
|
|
# allow _sockets to be None in case of process teardown
|
|
if getattr(self, "_sockets", None) is not None:
|
|
self._sockets.discard(socket)
|
|
|
|
def destroy(self, linger: int | None = None) -> None:
|
|
"""Close all sockets associated with this context and then terminate
|
|
the context.
|
|
|
|
.. warning::
|
|
|
|
destroy involves calling :meth:`Socket.close`, which is **NOT** threadsafe.
|
|
If there are active sockets in other threads, this must not be called.
|
|
|
|
Parameters
|
|
----------
|
|
|
|
linger : int, optional
|
|
If specified, set LINGER on sockets prior to closing them.
|
|
"""
|
|
if self.closed:
|
|
return
|
|
|
|
sockets: list[_SocketType] = list(getattr(self, "_sockets", None) or [])
|
|
for s in sockets:
|
|
if s and not s.closed:
|
|
if self._warn_destroy_close and warn is not None:
|
|
# warn can be None during process teardown
|
|
warn(
|
|
f"Destroying context with unclosed socket {s}",
|
|
ResourceWarning,
|
|
stacklevel=3,
|
|
source=s,
|
|
)
|
|
if linger is not None:
|
|
s.setsockopt(SocketOption.LINGER, linger)
|
|
s.close()
|
|
|
|
self.term()
|
|
|
|
def socket(
|
|
self: _ContextType,
|
|
socket_type: int,
|
|
socket_class: Callable[[_ContextType, int], _SocketType] | None = None,
|
|
**kwargs: Any,
|
|
) -> _SocketType:
|
|
"""Create a Socket associated with this Context.
|
|
|
|
Parameters
|
|
----------
|
|
socket_type : int
|
|
The socket type, which can be any of the 0MQ socket types:
|
|
REQ, REP, PUB, SUB, PAIR, DEALER, ROUTER, PULL, PUSH, etc.
|
|
|
|
socket_class: zmq.Socket
|
|
The socket class to instantiate, if different from the default for this Context.
|
|
e.g. for creating an asyncio socket attached to a default Context or vice versa.
|
|
|
|
.. versionadded:: 25
|
|
|
|
kwargs:
|
|
will be passed to the __init__ method of the socket class.
|
|
"""
|
|
if self.closed:
|
|
raise ZMQError(Errno.ENOTSUP)
|
|
if socket_class is None:
|
|
socket_class = self._socket_class
|
|
s: _SocketType = (
|
|
socket_class( # set PYTHONTRACEMALLOC=2 to get the calling frame
|
|
self, socket_type, **kwargs
|
|
)
|
|
)
|
|
for opt, value in self.sockopts.items():
|
|
try:
|
|
s.setsockopt(opt, value)
|
|
except ZMQError:
|
|
# ignore ZMQErrors, which are likely for socket options
|
|
# that do not apply to a particular socket type, e.g.
|
|
# SUBSCRIBE for non-SUB sockets.
|
|
pass
|
|
self._add_socket(s)
|
|
return s
|
|
|
|
def setsockopt(self, opt: int, value: Any) -> None:
|
|
"""set default socket options for new sockets created by this Context
|
|
|
|
.. versionadded:: 13.0
|
|
"""
|
|
self.sockopts[opt] = value
|
|
|
|
def getsockopt(self, opt: int) -> OptValT:
|
|
"""get default socket options for new sockets created by this Context
|
|
|
|
.. versionadded:: 13.0
|
|
"""
|
|
return self.sockopts[opt]
|
|
|
|
def _set_attr_opt(self, name: str, opt: int, value: OptValT) -> None:
|
|
"""set default sockopts as attributes"""
|
|
if name in ContextOption.__members__:
|
|
return self.set(opt, value)
|
|
elif name in SocketOption.__members__:
|
|
self.sockopts[opt] = value
|
|
else:
|
|
raise AttributeError(f"No such context or socket option: {name}")
|
|
|
|
def _get_attr_opt(self, name: str, opt: int) -> OptValT:
|
|
"""get default sockopts as attributes"""
|
|
if name in ContextOption.__members__:
|
|
return self.get(opt)
|
|
else:
|
|
if opt not in self.sockopts:
|
|
raise AttributeError(name)
|
|
else:
|
|
return self.sockopts[opt]
|
|
|
|
def __delattr__(self, key: str) -> None:
|
|
"""delete default sockopts as attributes"""
|
|
if key in self.__dict__:
|
|
self.__dict__.pop(key)
|
|
return
|
|
key = key.upper()
|
|
try:
|
|
opt = getattr(SocketOption, key)
|
|
except AttributeError:
|
|
raise AttributeError(f"No such socket option: {key!r}")
|
|
else:
|
|
if opt not in self.sockopts:
|
|
raise AttributeError(key)
|
|
else:
|
|
del self.sockopts[opt]
|
|
|
|
|
|
SyncContext: TypeAlias = Context[SyncSocket]
|
|
|
|
|
|
__all__ = ['Context', 'SyncContext']
|