1# coding: utf-8
2"""Python bindings for 0MQ."""
3
4# Copyright (C) PyZMQ Developers
5# Distributed under the terms of the Modified BSD License.
6
7import atexit
8
9from zmq.backend import Context as ContextBase
10from . import constants
11from .attrsettr import AttributeSetter
12from .constants import ENOTSUP, ctx_opt_names
13from .socket import Socket
14from zmq.error import ZMQError
15
16# notice when exiting, to avoid triggering term on exit
17_exiting = False
18def _notice_atexit():
19    global _exiting
20    _exiting = True
21atexit.register(_notice_atexit)
22
23
24from zmq.utils.interop import cast_int_addr
25
26
27class Context(ContextBase, AttributeSetter):
28    """Create a zmq Context
29
30    A zmq Context creates sockets via its ``ctx.socket`` method.
31    """
32    sockopts = None
33    _instance = None
34    _shadow = False
35
36    def __init__(self, io_threads=1, **kwargs):
37        super(Context, self).__init__(io_threads=io_threads, **kwargs)
38        if kwargs.get('shadow', False):
39            self._shadow = True
40        else:
41            self._shadow = False
42        self.sockopts = {}
43
44
45    def __del__(self):
46        """deleting a Context should terminate it, without trying non-threadsafe destroy"""
47        if not self._shadow and not _exiting:
48            self.term()
49
50    def __enter__(self):
51        return self
52
53    def __exit__(self, *args, **kwargs):
54        self.term()
55
56    @classmethod
57    def shadow(cls, address):
58        """Shadow an existing libzmq context
59
60        address is the integer address of the libzmq context
61        or an FFI pointer to it.
62
63        .. versionadded:: 14.1
64        """
65        address = cast_int_addr(address)
66        return cls(shadow=address)
67
68    @classmethod
69    def shadow_pyczmq(cls, ctx):
70        """Shadow an existing pyczmq context
71
72        ctx is the FFI `zctx_t *` pointer
73
74        .. versionadded:: 14.1
75        """
76        from pyczmq import zctx
77
78        underlying = zctx.underlying(ctx)
79        address = cast_int_addr(underlying)
80        return cls(shadow=address)
81
82    # static method copied from tornado IOLoop.instance
83    @classmethod
84    def instance(cls, io_threads=1):
85        """Returns a global Context instance.
86
87        Most single-threaded applications have a single, global Context.
88        Use this method instead of passing around Context instances
89        throughout your code.
90
91        A common pattern for classes that depend on Contexts is to use
92        a default argument to enable programs with multiple Contexts
93        but not require the argument for simpler applications:
94
95            class MyClass(object):
96                def __init__(self, context=None):
97                    self.context = context or Context.instance()
98        """
99        if cls._instance is None or cls._instance.closed:
100            cls._instance = cls(io_threads=io_threads)
101        return cls._instance
102
103    #-------------------------------------------------------------------------
104    # Hooks for ctxopt completion
105    #-------------------------------------------------------------------------
106
107    def __dir__(self):
108        keys = dir(self.__class__)
109
110        for collection in (
111            ctx_opt_names,
112        ):
113            keys.extend(collection)
114        return keys
115
116    #-------------------------------------------------------------------------
117    # Creating Sockets
118    #-------------------------------------------------------------------------
119
120    @property
121    def _socket_class(self):
122        return Socket
123
124    def socket(self, socket_type):
125        """Create a Socket associated with this Context.
126
127        Parameters
128        ----------
129        socket_type : int
130            The socket type, which can be any of the 0MQ socket types:
131            REQ, REP, PUB, SUB, PAIR, DEALER, ROUTER, PULL, PUSH, etc.
132        """
133        if self.closed:
134            raise ZMQError(ENOTSUP)
135        s = self._socket_class(self, socket_type)
136        for opt, value in self.sockopts.items():
137            try:
138                s.setsockopt(opt, value)
139            except ZMQError:
140                # ignore ZMQErrors, which are likely for socket options
141                # that do not apply to a particular socket type, e.g.
142                # SUBSCRIBE for non-SUB sockets.
143                pass
144        return s
145
146    def setsockopt(self, opt, value):
147        """set default socket options for new sockets created by this Context
148
149        .. versionadded:: 13.0
150        """
151        self.sockopts[opt] = value
152
153    def getsockopt(self, opt):
154        """get default socket options for new sockets created by this Context
155
156        .. versionadded:: 13.0
157        """
158        return self.sockopts[opt]
159
160    def _set_attr_opt(self, name, opt, value):
161        """set default sockopts as attributes"""
162        if name in constants.ctx_opt_names:
163            return self.set(opt, value)
164        else:
165            self.sockopts[opt] = value
166
167    def _get_attr_opt(self, name, opt):
168        """get default sockopts as attributes"""
169        if name in constants.ctx_opt_names:
170            return self.get(opt)
171        else:
172            if opt not in self.sockopts:
173                raise AttributeError(name)
174            else:
175                return self.sockopts[opt]
176
177    def __delattr__(self, key):
178        """delete default sockopts as attributes"""
179        key = key.upper()
180        try:
181            opt = getattr(constants, key)
182        except AttributeError:
183            raise AttributeError("no such socket option: %s" % key)
184        else:
185            if opt not in self.sockopts:
186                raise AttributeError(key)
187            else:
188                del self.sockopts[opt]
189
190__all__ = ['Context']
191