diff --git a/python/ci_requirements.txt b/python/ci_requirements.txt index 9b0ec6a7f..6b3101103 100644 --- a/python/ci_requirements.txt +++ b/python/ci_requirements.txt @@ -5,3 +5,4 @@ wheel flake8 tox>=1.7.2 cffi>=1.0.0 +typing-cffi diff --git a/python/cproton.py b/python/cproton.py index 61a60589e..528e675b9 100644 --- a/python/cproton.py +++ b/python/cproton.py @@ -340,24 +340,14 @@ def pn_collector_put_pyref(collector, obj, etype): lib.pn_collector_put_py(collector, d, etype.number) -def pn_record_def_py(record): - lib.pn_record_def_py(record) - - def pn_record_get_py(record): d = lib.pn_record_get_py(record) if d == ffi.NULL: - return None - return ffi.from_handle(d) - - -def pn_record_set_py(record, value): - if value is None: - d = ffi.NULL - else: - d = ffi.new_handle(value) + d = ffi.new_handle({}) retained_objects.add(d) - lib.pn_record_set_py(record, d) + lib.pn_record_def_py(record) + lib.pn_record_set_py(record, d) + return ffi.from_handle(d) def pn_event_class_name(event): diff --git a/python/proton/_delivery.py b/python/proton/_delivery.py index 5d62d86b6..6332fd8a7 100644 --- a/python/proton/_delivery.py +++ b/python/proton/_delivery.py @@ -24,11 +24,11 @@ pn_delivery_writable, pn_disposition_annotations, pn_disposition_condition, pn_disposition_data, \ pn_disposition_get_section_number, pn_disposition_get_section_offset, pn_disposition_is_failed, \ pn_disposition_is_undeliverable, pn_disposition_set_failed, pn_disposition_set_section_number, \ - pn_disposition_set_section_offset, pn_disposition_set_undeliverable, pn_disposition_type, \ - isnull + pn_disposition_set_section_offset, pn_disposition_set_undeliverable, pn_disposition_type from ._condition import cond2obj, obj2cond from ._data import dat2obj, obj2dat +from ._transport import Transport from ._wrapper import Wrapper from enum import IntEnum @@ -39,7 +39,7 @@ from ._condition import Condition from ._data import PythonAMQPData, symbol from ._endpoints import Receiver, Sender # circular import - from ._reactor import Connection, Session, Transport + from ._reactor import Connection, Session class DispositionType(IntEnum): @@ -277,19 +277,12 @@ class Delivery(Wrapper): delivery being settled. """ - @staticmethod - def wrap(impl): - if isnull(impl): - return None - else: - return Delivery(impl) + get_context = pn_delivery_attachments def __init__(self, impl): - Wrapper.__init__(self, impl, pn_delivery_attachments) - - def _init(self) -> None: - self.local = Disposition(pn_delivery_local(self._impl), True) - self.remote = Disposition(pn_delivery_remote(self._impl), False) + if self.Uninitialized(): + self.local = Disposition(pn_delivery_local(self._impl), True) + self.remote = Disposition(pn_delivery_remote(self._impl), False) @property def tag(self) -> str: @@ -414,7 +407,7 @@ def connection(self) -> 'Connection': return self.session.connection @property - def transport(self) -> 'Transport': + def transport(self) -> Optional[Transport]: """ The :class:`Transport` bound to the :class:`Connection` over which the delivery was sent or received. diff --git a/python/proton/_endpoints.py b/python/proton/_endpoints.py index ae7ab81d6..38dde5e6d 100644 --- a/python/proton/_endpoints.py +++ b/python/proton/_endpoints.py @@ -54,8 +54,7 @@ pn_terminus_is_dynamic, pn_terminus_outcomes, pn_terminus_properties, pn_terminus_set_address, \ pn_terminus_set_distribution_mode, pn_terminus_set_durability, pn_terminus_set_dynamic, \ pn_terminus_set_expiry_policy, pn_terminus_set_timeout, pn_terminus_set_type, \ - pn_link_properties, pn_link_remote_properties, \ - isnull + pn_link_properties, pn_link_remote_properties from ._condition import cond2obj, obj2cond from ._data import Data, dat2obj, obj2dat, PropertyDict, SymbolList @@ -64,7 +63,7 @@ from ._handler import Handler from ._transport import Transport from ._wrapper import Wrapper -from typing import Dict, List, Optional, Union, TYPE_CHECKING, Any +from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING if TYPE_CHECKING: from ._condition import Condition @@ -112,7 +111,7 @@ class Endpoint(object): REMOTE_CLOSED = PN_REMOTE_CLOSED """ The remote endpoint state is closed. """ - def _init(self) -> None: + def __init__(self) -> None: self.condition: Optional['Condition'] = None self._handler: Optional[Handler] = None @@ -159,26 +158,17 @@ class Connection(Wrapper, Endpoint): A representation of an AMQP connection. """ - @staticmethod - def wrap(impl): - if isnull(impl): - return None - else: - return Connection(impl) + constructor = pn_connection + get_context = pn_connection_attachments def __init__(self, impl: Any = None) -> None: - if impl is None: - Wrapper.__init__(self, constructor=pn_connection, get_context=pn_connection_attachments) - else: - Wrapper.__init__(self, impl, pn_connection_attachments) - - def _init(self) -> None: - Endpoint._init(self) - self.offered_capabilities_list = None - self.desired_capabilities_list = None - self.properties = None - self.url = None - self._acceptor = None + if self.Uninitialized(): + Endpoint.__init__(self) + self.offered_capabilities_list = None + self.desired_capabilities_list = None + self.properties = None + self.url = None + self._acceptor = None def _get_attachments(self): return pn_connection_attachments(self._impl) @@ -550,15 +540,12 @@ def properties(self, properties_dict: Optional[Union[PropertyDict, Dict[str, 'Py class Session(Wrapper, Endpoint): """A container of links""" - @staticmethod - def wrap(impl): - if isnull(impl): - return None - else: - return Session(impl) + + get_context = pn_session_attachments def __init__(self, impl): - Wrapper.__init__(self, impl, pn_session_attachments) + if self.Uninitialized(): + Endpoint.__init__(self) def _get_attachments(self): return pn_session_attachments(self._impl) @@ -714,21 +701,18 @@ class Link(Wrapper, Endpoint): RCV_SECOND = PN_RCV_SECOND """The receiver will only settle deliveries after the sender settles.""" - @staticmethod - def wrap(impl): - if isnull(impl): - return None + get_context = pn_link_attachments + + def __new__(cls, impl) -> 'Link': if pn_link_is_sender(impl): - return Sender(impl) + return super().__new__(Sender, impl) else: - return Receiver(impl) + return super().__new__(Receiver, impl) def __init__(self, impl): - Wrapper.__init__(self, impl, pn_link_attachments) - - def _init(self) -> None: - Endpoint._init(self) - self.properties = None + if self.Uninitialized(): + Endpoint.__init__(self) + self.properties = None def _get_attachments(self): return pn_link_attachments(self._impl) diff --git a/python/proton/_transport.py b/python/proton/_transport.py index 489654b14..b1cbfa16e 100644 --- a/python/proton/_transport.py +++ b/python/proton/_transport.py @@ -92,27 +92,32 @@ def wrap(impl: Optional[Callable]) -> Optional['Transport']: else: return Transport(impl=impl) + constructor = pn_transport + get_context = pn_transport_attachments + + def __new__( + cls, + mode: Optional[int] = None, + impl=None, + ) -> 'Transport': + return super().__new__(cls, impl) + def __init__( self, - mode: 'Optional[int]' = None, - impl: 'Callable' = None, + mode: Optional[int] = None, + impl=None, ) -> None: - if impl is None: - Wrapper.__init__(self, constructor=pn_transport, get_context=pn_transport_attachments) - else: - Wrapper.__init__(self, impl, pn_transport_attachments) if mode == Transport.SERVER: pn_transport_set_server(self._impl) elif mode is None or mode == Transport.CLIENT: pass else: raise TransportException("Cannot initialise Transport from mode: %s" % str(mode)) - - def _init(self) -> None: - self._sasl = None - self._ssl = None - self._reactor = None - self._connect_selectable = None + if self.Uninitialized(): + self._sasl = None + self._ssl = None + self._reactor = None + self._connect_selectable = None def _check(self, err: int) -> int: if err < 0: @@ -468,7 +473,9 @@ def sasl(self) -> 'SASL': :return: SASL object associated with this transport. """ - return SASL(self) + if not self._sasl: + self._sasl = SASL(self) + return self._sasl def ssl(self, domain: Optional['SSLDomain'] = None, session_details: Optional['SSLSessionDetails'] = None) -> 'SSL': """ @@ -512,7 +519,7 @@ class SASLException(TransportException): pass -class SASL(Wrapper): +class SASL: """ The SASL layer is responsible for establishing an authenticated and/or encrypted tunnel over which AMQP frames are passed between @@ -542,9 +549,14 @@ def extended() -> bool: """ return pn_sasl_extended() - def __init__(self, transport: Transport) -> None: - Wrapper.__init__(self, transport._impl, pn_transport_attachments) - self._sasl = pn_sasl(transport._impl) + def __new__(cls, transport: Transport) -> 'SASL': + if not transport._sasl: + sasl = super().__new__(cls) + sasl._sasl = pn_sasl(transport._impl) + transport._sasl = sasl + return sasl + else: + return transport._sasl def _check(self, err): if err < 0: diff --git a/python/proton/_wrapper.py b/python/proton/_wrapper.py index 88ab25990..673c2cf54 100644 --- a/python/proton/_wrapper.py +++ b/python/proton/_wrapper.py @@ -17,83 +17,72 @@ # under the License. # -from typing import Any, Callable, Optional +from typing import Any, Callable, ClassVar, Optional -from cproton import addressof, pn_incref, pn_decref, \ - pn_record_get_py, pn_record_def_py, pn_record_set_py +from cproton import addressof, isnull, pn_incref, pn_decref, \ + pn_record_get_py from ._exceptions import ProtonException -class EmptyAttrs: - - def __contains__(self, name): - return False - - def __getitem__(self, name): - raise KeyError(name) - - def __setitem__(self, name, value): - raise TypeError("does not support item assignment") - - -EMPTY_ATTRS = EmptyAttrs() - - -class Wrapper(object): +class Wrapper: """ Wrapper for python objects that need to be stored in event contexts and be retrieved again from them Quick note on how this works: - The actual *python* object has only 3 attributes which redirect into the wrapped C objects: + The actual *python* object has only 2 attributes which redirect into the wrapped C objects: _impl The wrapped C object itself _attrs This is a special pn_record_t holding a PYCTX which is a python dict every attribute in the python object is actually looked up here - Because the objects actual attributes are stored away they must be initialised *after* the wrapping - is set up. This is the purpose of the _init method in the wrapped object. Wrapper.__init__ will call - eht subclass _init to initialise attributes. So they *must not* be initialised in the subclass __init__ - before calling the superclass (Wrapper) __init__ or they will not be accessible from the wrapper at all. + Because the objects actual attributes are stored away they must be initialised *after* the wrapping. This is + achieved by using the __new__ method of Wrapper to create the object with the actiual attributes before the + __init__ method is called. + In the case where an existing object is being wrapped, the __init__ method is called with the existing object + but should not initialise the object. This is because the object is already initialised and the attributes + are already set. Use the Uninitialised method to check if the object is already initialised. """ - def __init__( - self, - impl: Any = None, - get_context: Optional[Callable[[Any], Any]] = None, - constructor: Optional[Callable[[], Any]] = None - ) -> None: - init = False - if impl is None and constructor is not None: - # we are constructing a new object - impl = constructor() - if impl is None: - self.__dict__["_impl"] = impl - self.__dict__["_attrs"] = EMPTY_ATTRS - raise ProtonException( - "Wrapper failed to create wrapped object. Check for file descriptor or memory exhaustion.") - init = True + constructor: ClassVar[Optional[Callable[[], Any]]] = None + get_context: ClassVar[Optional[Callable[[Any], dict[str, Any]]]] = None + + __slots__ = ["_impl", "_attrs"] + + @classmethod + def wrap(cls, impl: Any) -> Optional['Wrapper']: + if isnull(impl): + return None else: - # we are wrapping an existing object - pn_incref(impl) + return cls(impl) - if get_context: - record = get_context(impl) + def __new__(cls, impl: Any = None) -> 'Wrapper': + attrs = None + try: + if impl is None: + # we are constructing a new object + assert cls.constructor + impl = cls.constructor() + if impl is None: + raise ProtonException( + "Wrapper failed to create wrapped object. Check for file descriptor or memory exhaustion.") + else: + # we are wrapping an existing object + pn_incref(impl) + + assert cls.get_context + record = cls.get_context(impl) attrs = pn_record_get_py(record) - if attrs is None: - attrs = {} - pn_record_def_py(record) - pn_record_set_py(record, attrs) - init = True - else: - attrs = EMPTY_ATTRS - init = False - self.__dict__["_impl"] = impl - self.__dict__["_attrs"] = attrs - if init: - self._init() + finally: + self = super().__new__(cls) + self._impl = impl + self._attrs = attrs + return self + + def Uninitialized(self) -> bool: + return self._attrs == {} def __getattr__(self, name: str) -> Any: - attrs = self.__dict__["_attrs"] - if name in attrs: + attrs = self._attrs + if attrs and name in attrs: return attrs[name] else: raise AttributeError(name + " not in _attrs") @@ -102,11 +91,12 @@ def __setattr__(self, name: str, value: Any) -> None: if hasattr(self.__class__, name): object.__setattr__(self, name, value) else: - attrs = self.__dict__["_attrs"] - attrs[name] = value + attrs = self._attrs + if attrs is not None: + attrs[name] = value def __delattr__(self, name: str) -> None: - attrs = self.__dict__["_attrs"] + attrs = self._attrs if attrs: del attrs[name]