| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- """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, /): ...
- @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']
|