first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
"""pure-Python sugar wrappers for core 0MQ objects."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
from zmq import error
from zmq.sugar import context, frame, poll, socket, tracker, version
__all__ = []
for submod in (context, error, frame, poll, socket, tracker, version):
__all__.extend(submod.__all__)
from zmq.error import * # noqa
from zmq.sugar.context import * # noqa
from zmq.sugar.frame import * # noqa
from zmq.sugar.poll import * # noqa
from zmq.sugar.socket import * # noqa
# deprecated:
from zmq.sugar.stopwatch import Stopwatch # noqa
from zmq.sugar.tracker import * # noqa
from zmq.sugar.version import * # noqa
__all__.append('Stopwatch')

View File

@@ -0,0 +1,10 @@
from zmq.error import *
from . import constants as constants
from .constants import *
from .context import *
from .frame import *
from .poll import *
from .socket import *
from .tracker import *
from .version import *

View File

@@ -0,0 +1,76 @@
"""Mixin for mapping set/getattr to self.set/get"""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
import errno
from typing import TypeVar, Union
from .. import constants
T = TypeVar("T")
OptValT = Union[str, bytes, int]
class AttributeSetter:
def __setattr__(self, key: str, value: OptValT) -> None:
"""set zmq options by attribute"""
if key in self.__dict__:
object.__setattr__(self, key, value)
return
# regular setattr only allowed for class-defined attributes
for cls in self.__class__.mro():
if key in cls.__dict__ or key in getattr(cls, "__annotations__", {}):
object.__setattr__(self, key, value)
return
upper_key = key.upper()
try:
opt = getattr(constants, upper_key)
except AttributeError:
raise AttributeError(
f"{self.__class__.__name__} has no such option: {upper_key}"
)
else:
self._set_attr_opt(upper_key, opt, value)
def _set_attr_opt(self, name: str, opt: int, value: OptValT) -> None:
"""override if setattr should do something other than call self.set"""
self.set(opt, value)
def __getattr__(self, key: str) -> OptValT:
"""get zmq options by attribute"""
upper_key = key.upper()
try:
opt = getattr(constants, upper_key)
except AttributeError:
raise AttributeError(
f"{self.__class__.__name__} has no such option: {upper_key}"
) from None
else:
from zmq import ZMQError
try:
return self._get_attr_opt(upper_key, opt)
except ZMQError as e:
# EINVAL will be raised on access for write-only attributes.
# Turn that into an AttributeError
# necessary for mocking
if e.errno in {errno.EINVAL, errno.EFAULT}:
raise AttributeError(f"{key} attribute is write-only")
else:
raise
def _get_attr_opt(self, name, opt) -> OptValT:
"""override if getattr should do something other than call self.get"""
return self.get(opt)
def get(self, opt: int) -> OptValT:
pass
def set(self, opt: int, val: OptValT) -> None:
pass
__all__ = ['AttributeSetter']

View File

@@ -0,0 +1,320 @@
"""Python bindings for 0MQ."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
import atexit
import os
import warnings
from threading import Lock
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
from weakref import WeakSet
from zmq.backend import Context as ContextBase
from zmq.constants import ContextOption, Errno, SocketOption
from zmq.error import ZMQError
from .attrsettr import AttributeSetter, OptValT
from .socket import Socket
# notice when exiting, to avoid triggering term on exit
_exiting = False
def _notice_atexit() -> None:
global _exiting
_exiting = True
atexit.register(_notice_atexit)
T = TypeVar('T', bound='Context')
ST = TypeVar('ST', bound='Socket', covariant=True)
class Context(ContextBase, AttributeSetter, Generic[ST]):
"""Create a zmq Context
A zmq Context creates sockets via its ``ctx.socket`` method.
"""
sockopts: Dict[int, Any]
_instance: Any = None
_instance_lock = Lock()
_instance_pid: Optional[int] = None
_shadow = False
_sockets: WeakSet
# mypy doesn't like a default value here
_socket_class: Type[ST] = Socket # type: ignore
def __init__(self: "Context[Socket]", io_threads: int = 1, **kwargs: Any) -> None:
super().__init__(io_threads=io_threads, **kwargs)
if kwargs.get('shadow', False):
self._shadow = True
else:
self._shadow = False
self.sockopts = {}
self._sockets = WeakSet()
def __del__(self) -> None:
"""deleting a Context should terminate it, without trying non-threadsafe destroy"""
# Calling locals() here conceals issue #1167 on Windows CPython 3.5.4.
locals()
if not self._shadow and not _exiting and not self.closed:
warnings.warn(
f"unclosed context {self}",
ResourceWarning,
stacklevel=2,
source=self,
)
self.term()
_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: T) -> T:
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
self.term()
def __copy__(self: T, memo: Any = None) -> T:
"""Copying a Context creates a shadow copy"""
return self.__class__.shadow(self.underlying)
__deepcopy__ = __copy__
@classmethod
def shadow(cls: Type[T], address: int) -> T:
"""Shadow an existing libzmq context
address is the integer address of the libzmq context
or an FFI pointer to it.
.. versionadded:: 14.1
"""
from zmq.utils.interop import cast_int_addr
address = cast_int_addr(address)
return cls(shadow=address)
@classmethod
def shadow_pyczmq(cls: Type[T], ctx: Any) -> T:
"""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[T], io_threads: int = 1) -> T:
"""Returns a global Context instance.
Most single-threaded 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.
"""
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: Optional[float] = None) -> None:
"""Close all sockets associated with this context and then terminate
the context.
.. warning::
destroy involves calling ``zmq_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 = self._sockets
self._sockets = WeakSet()
for s in sockets:
if s and not s.closed:
if linger is not None:
s.setsockopt(SocketOption.LINGER, linger)
s.close()
self.term()
def socket(self: T, socket_type: int, **kwargs: Any) -> ST:
"""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.
kwargs:
will be passed to the __init__ method of the socket class.
"""
if self.closed:
raise ZMQError(Errno.ENOTSUP)
s: ST = self._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"""
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]
__all__ = ['Context']

View File

@@ -0,0 +1,106 @@
"""0MQ Frame pure Python methods."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
import zmq
from zmq.backend import Frame as FrameBase
from .attrsettr import AttributeSetter
def _draft(v, feature):
zmq.error._check_version(v, feature)
if not zmq.DRAFT_API:
raise RuntimeError(
"libzmq and pyzmq must be built with draft support for %s" % feature
)
class Frame(FrameBase, AttributeSetter):
"""Frame(data=None, track=False, copy=None, copy_threshold=zmq.COPY_THRESHOLD)
A zmq message Frame class for non-copying send/recvs and access to message properties.
A ``zmq.Frame`` wraps an underlying ``zmq_msg_t``.
Message *properties* can be accessed by treating a Frame like a dictionary (``frame["User-Id"]``).
.. versionadded:: 14.4, libzmq 4
Frames created by ``recv(copy=False)`` can be used to access message properties and attributes,
such as the CURVE User-Id.
For example::
frames = socket.recv_multipart(copy=False)
user_id = frames[0]["User-Id"]
This class is used if you want to do non-copying send and recvs.
When you pass a chunk of bytes to this class, e.g. ``Frame(buf)``, the
ref-count of `buf` is increased by two: once because the Frame saves `buf` as
an instance attribute and another because a ZMQ message is created that
points to the buffer of `buf`. This second ref-count increase makes sure
that `buf` lives until all messages that use it have been sent.
Once 0MQ sends all the messages and it doesn't need the buffer of ``buf``,
0MQ will call ``Py_DECREF(s)``.
Parameters
----------
data : object, optional
any object that provides the buffer interface will be used to
construct the 0MQ message data.
track : bool [default: False]
whether a MessageTracker_ should be created to track this object.
Tracking a message has a cost at creation, because it creates a threadsafe
Event object.
copy : bool [default: use copy_threshold]
Whether to create a copy of the data to pass to libzmq
or share the memory with libzmq.
If unspecified, copy_threshold is used.
copy_threshold: int [default: zmq.COPY_THRESHOLD]
If copy is unspecified, messages smaller than this many bytes
will be copied and messages larger than this will be shared with libzmq.
"""
def __getitem__(self, key):
# map Frame['User-Id'] to Frame.get('User-Id')
return self.get(key)
@property
def group(self):
"""The RADIO-DISH group of the message.
Requires libzmq >= 4.2 and pyzmq built with draft APIs enabled.
.. versionadded:: 17
"""
_draft((4, 2), "RADIO-DISH")
return self.get('group')
@group.setter
def group(self, group):
_draft((4, 2), "RADIO-DISH")
self.set('group', group)
@property
def routing_id(self):
"""The CLIENT-SERVER routing id of the message.
Requires libzmq >= 4.2 and pyzmq built with draft APIs enabled.
.. versionadded:: 17
"""
_draft((4, 2), "CLIENT-SERVER")
return self.get('routing_id')
@routing_id.setter
def routing_id(self, routing_id):
_draft((4, 2), "CLIENT-SERVER")
self.set('routing_id', routing_id)
# keep deprecated alias
Message = Frame
__all__ = ['Frame', 'Message']

View File

@@ -0,0 +1,164 @@
"""0MQ polling related functions and classes."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
from typing import Any, Dict, List, Optional, Tuple
from zmq.backend import zmq_poll
from zmq.constants import POLLERR, POLLIN, POLLOUT
# -----------------------------------------------------------------------------
# Polling related methods
# -----------------------------------------------------------------------------
class Poller:
"""A stateful poll interface that mirrors Python's built-in poll."""
sockets: List[Tuple[Any, int]]
_map: Dict
def __init__(self) -> None:
self.sockets = []
self._map = {}
def __contains__(self, socket: Any) -> bool:
return socket in self._map
def register(self, socket: Any, flags: int = POLLIN | POLLOUT):
"""p.register(socket, flags=POLLIN|POLLOUT)
Register a 0MQ socket or native fd for I/O monitoring.
register(s,0) is equivalent to unregister(s).
Parameters
----------
socket : zmq.Socket or native socket
A zmq.Socket or any Python object having a ``fileno()``
method that returns a valid file descriptor.
flags : int
The events to watch for. Can be POLLIN, POLLOUT or POLLIN|POLLOUT.
If `flags=0`, socket will be unregistered.
"""
if flags:
if socket in self._map:
idx = self._map[socket]
self.sockets[idx] = (socket, flags)
else:
idx = len(self.sockets)
self.sockets.append((socket, flags))
self._map[socket] = idx
elif socket in self._map:
# uregister sockets registered with no events
self.unregister(socket)
else:
# ignore new sockets with no events
pass
def modify(self, socket, flags=POLLIN | POLLOUT):
"""Modify the flags for an already registered 0MQ socket or native fd."""
self.register(socket, flags)
def unregister(self, socket: Any):
"""Remove a 0MQ socket or native fd for I/O monitoring.
Parameters
----------
socket : Socket
The socket instance to stop polling.
"""
idx = self._map.pop(socket)
self.sockets.pop(idx)
# shift indices after deletion
for socket, flags in self.sockets[idx:]:
self._map[socket] -= 1
def poll(self, timeout: Optional[int] = None) -> List[Tuple[Any, int]]:
"""Poll the registered 0MQ or native fds for I/O.
If there are currently events ready to be processed, this function will return immediately.
Otherwise, this function will return as soon the first event is available or after timeout
milliseconds have elapsed.
Parameters
----------
timeout : int
The timeout in milliseconds. If None, no `timeout` (infinite). This
is in milliseconds to be compatible with ``select.poll()``.
Returns
-------
events : list of tuples
The list of events that are ready to be processed.
This is a list of tuples of the form ``(socket, event_mask)``, where the 0MQ Socket
or integer fd is the first element, and the poll event mask (POLLIN, POLLOUT) is the second.
It is common to call ``events = dict(poller.poll())``,
which turns the list of tuples into a mapping of ``socket : event_mask``.
"""
if timeout is None or timeout < 0:
timeout = -1
elif isinstance(timeout, float):
timeout = int(timeout)
return zmq_poll(self.sockets, timeout=timeout)
def select(rlist: List, wlist: List, xlist: List, timeout: Optional[float] = None):
"""select(rlist, wlist, xlist, timeout=None) -> (rlist, wlist, xlist)
Return the result of poll as a lists of sockets ready for r/w/exception.
This has the same interface as Python's built-in ``select.select()`` function.
Parameters
----------
timeout : float, int, optional
The timeout in seconds. If None, no timeout (infinite). This is in seconds to be
compatible with ``select.select()``.
rlist : list of sockets/FDs
sockets/FDs to be polled for read events
wlist : list of sockets/FDs
sockets/FDs to be polled for write events
xlist : list of sockets/FDs
sockets/FDs to be polled for error events
Returns
-------
(rlist, wlist, xlist) : tuple of lists of sockets (length 3)
Lists correspond to sockets available for read/write/error events respectively.
"""
if timeout is None:
timeout = -1
# Convert from sec -> ms for zmq_poll.
# zmq_poll accepts 3.x style timeout in ms
timeout = int(timeout * 1000.0)
if timeout < 0:
timeout = -1
sockets = []
for s in set(rlist + wlist + xlist):
flags = 0
if s in rlist:
flags |= POLLIN
if s in wlist:
flags |= POLLOUT
if s in xlist:
flags |= POLLERR
sockets.append((s, flags))
return_sockets = zmq_poll(sockets, timeout)
rlist, wlist, xlist = [], [], []
for s, flags in return_sockets:
if flags & POLLIN:
rlist.append(s)
if flags & POLLOUT:
wlist.append(s)
if flags & POLLERR:
xlist.append(s)
return rlist, wlist, xlist
# -----------------------------------------------------------------------------
# Symbols to export
# -----------------------------------------------------------------------------
__all__ = ['Poller', 'select']

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
"""Deprecated Stopwatch implementation"""
# Copyright (c) PyZMQ Development Team.
# Distributed under the terms of the Modified BSD License.
class Stopwatch:
"""Deprecated zmq.Stopwatch implementation
You can use Python's builtin timers (time.monotonic, etc.).
"""
def __init__(self):
import warnings
warnings.warn(
"zmq.Stopwatch is deprecated. Use stdlib time.monotonic and friends instead",
DeprecationWarning,
stacklevel=2,
)
self._start = 0
import time
try:
self._monotonic = time.monotonic
except AttributeError:
self._monotonic = time.time
def start(self):
"""Start the counter"""
self._start = self._monotonic()
def stop(self):
"""Return time since start in microseconds"""
stop = self._monotonic()
return int(1e6 * (stop - self._start))

View File

@@ -0,0 +1,120 @@
"""Tracker for zero-copy messages with 0MQ."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
import time
from threading import Event
from typing import Set, Tuple, Union
from zmq.backend import Frame
from zmq.error import NotDone
class MessageTracker:
"""MessageTracker(*towatch)
A class for tracking if 0MQ is done using one or more messages.
When you send a 0MQ message, it is not sent immediately. The 0MQ IO thread
sends the message at some later time. Often you want to know when 0MQ has
actually sent the message though. This is complicated by the fact that
a single 0MQ message can be sent multiple times using different sockets.
This class allows you to track all of the 0MQ usages of a message.
Parameters
----------
towatch : Event, MessageTracker, Message instances.
This objects to track. This class can track the low-level
Events used by the Message class, other MessageTrackers or
actual Messages.
"""
events: Set[Event]
peers: Set["MessageTracker"]
def __init__(self, *towatch: Tuple[Union["MessageTracker", Event, Frame]]):
"""MessageTracker(*towatch)
Create a message tracker to track a set of mesages.
Parameters
----------
*towatch : tuple of Event, MessageTracker, Message instances.
This list of objects to track. This class can track the low-level
Events used by the Message class, other MessageTrackers or
actual Messages.
"""
self.events = set()
self.peers = set()
for obj in towatch:
if isinstance(obj, Event):
self.events.add(obj)
elif isinstance(obj, MessageTracker):
self.peers.add(obj)
elif isinstance(obj, Frame):
if not obj.tracker:
raise ValueError("Not a tracked message")
self.peers.add(obj.tracker)
else:
raise TypeError("Require Events or Message Frames, not %s" % type(obj))
@property
def done(self):
"""Is 0MQ completely done with the message(s) being tracked?"""
for evt in self.events:
if not evt.is_set():
return False
for pm in self.peers:
if not pm.done:
return False
return True
def wait(self, timeout: Union[float, int] = -1):
"""mt.wait(timeout=-1)
Wait for 0MQ to be done with the message or until `timeout`.
Parameters
----------
timeout : float [default: -1, wait forever]
Maximum time in (s) to wait before raising NotDone.
Returns
-------
None
if done before `timeout`
Raises
------
NotDone
if `timeout` reached before I am done.
"""
tic = time.time()
remaining: float
if timeout is False or timeout < 0:
remaining = 3600 * 24 * 7 # a week
else:
remaining = timeout
for evt in self.events:
if remaining < 0:
raise NotDone
evt.wait(timeout=remaining)
if not evt.is_set():
raise NotDone
toc = time.time()
remaining -= toc - tic
tic = toc
for peer in self.peers:
if remaining < 0:
raise NotDone
peer.wait(timeout=remaining)
toc = time.time()
remaining -= toc - tic
tic = toc
_FINISHED_TRACKER = MessageTracker()
__all__ = ['MessageTracker', '_FINISHED_TRACKER']

View File

@@ -0,0 +1,66 @@
"""PyZMQ and 0MQ version functions."""
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
import re
from typing import Match, Tuple, Union, cast
from zmq.backend import zmq_version_info
__version__: str = "23.0.0"
_version_pat = re.compile(r"(\d+)\.(\d+)\.(\d+)(.*)")
_match = cast(Match, _version_pat.match(__version__))
_version_groups = _match.groups()
VERSION_MAJOR = int(_version_groups[0])
VERSION_MINOR = int(_version_groups[1])
VERSION_PATCH = int(_version_groups[2])
VERSION_EXTRA = _version_groups[3].lstrip(".")
version_info: Union[Tuple[int, int, int], Tuple[int, int, int, float]] = (
VERSION_MAJOR,
VERSION_MINOR,
VERSION_PATCH,
)
if VERSION_EXTRA:
version_info = (
VERSION_MAJOR,
VERSION_MINOR,
VERSION_PATCH,
float('inf'),
)
__revision__: str = ''
def pyzmq_version() -> str:
"""return the version of pyzmq as a string"""
if __revision__:
return '+'.join([__version__, __revision__[:6]])
else:
return __version__
def pyzmq_version_info() -> Union[Tuple[int, int, int], Tuple[int, int, int, float]]:
"""return the pyzmq version as a tuple of at least three numbers
If pyzmq is a development version, `inf` will be appended after the third integer.
"""
return version_info
def zmq_version() -> str:
"""return the version of libzmq as a string"""
return "%i.%i.%i" % zmq_version_info()
__all__ = [
'zmq_version',
'zmq_version_info',
'pyzmq_version',
'pyzmq_version_info',
'__version__',
'__revision__',
]