From f6c685d6ea390be9916fe90528f2126ea2e905f1 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 25 Oct 2016 00:49:29 -0700 Subject: [PATCH] Make sentinel objects instances of themselves This is a reworked version of @RazerM's work in gh-11. --- docs/source/api.rst | 17 +++++++++++++++++ docs/source/changes.rst | 9 +++++++++ h11/_connection.py | 8 ++++---- h11/_state.py | 4 ++-- h11/_util.py | 22 +++++++++++++++------- h11/tests/test_util.py | 11 +++++++++-- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 14ec9f0..9a50fd7 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -352,6 +352,23 @@ All of these behave the same, and their behavior is modeled after :data:`None`: they're opaque singletons, their :meth:`__repr__` is their name, and you compare them with ``is``. +Finally, h11's constants have a quirky feature that can sometimes be +useful: they are instances of themselves. + +.. ipython:: python + + type(NEED_DATA) is NEED_DATA + type(PAUSED) is PAUSED + +The main application of this is that when handling the return value +from :meth:`Connection.next_event`, which is sometimes an instance of +an event class and sometimes :data:`NEED_DATA` or :data:`PAUSED`, you +can always call ``type(event)`` to get something useful to dispatch +one, using e.g. a handler table, :func:`functools.singledispatch`, or +calling ``getattr(some_object, "handle_" + +type(event).__name__)``. Not that this kind of dispatch-based strategy +is always the best approach -- but the option is there if you want it. + The Connection object --------------------- diff --git a/docs/source/changes.rst b/docs/source/changes.rst index ef9a7cf..f50db9c 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -3,6 +3,15 @@ History of changes .. currentmodule:: h11 +vNEXT (????-??-??) +------------------- + +* Made it so that sentinels are instances of themselves, to enable + certain dispatch tricks on the return value of + :func:`Connection.next_event` (see `issue #8 + `). + + v0.6.0 (2016-10-24) ------------------- diff --git a/h11/_connection.py b/h11/_connection.py index 3b29a8c..e99a1fc 100644 --- a/h11/_connection.py +++ b/h11/_connection.py @@ -6,7 +6,7 @@ # Import all state sentinels from ._state import * # Import the internal things we need -from ._util import LocalProtocolError, RemoteProtocolError, Sentinel +from ._util import LocalProtocolError, RemoteProtocolError, make_sentinel from ._state import ConnectionState, _SWITCH_UPGRADE, _SWITCH_CONNECT from ._headers import ( get_comma_header, set_comma_header, has_expect_100_continue, @@ -18,8 +18,8 @@ # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = ["Connection", "NEED_DATA", "PAUSED"] -NEED_DATA = Sentinel("NEED_DATA") -PAUSED = Sentinel("PAUSED") +NEED_DATA = make_sentinel("NEED_DATA") +PAUSED = make_sentinel("PAUSED") # If we ever have this much buffered without it making a complete parseable # event, we error out. The only time we really buffer is when reading the @@ -418,7 +418,7 @@ def next_event(self): "Can't receive data when peer state is ERROR") try: event = self._extract_next_receive_event() - if type(event) is not Sentinel: + if event not in [NEED_DATA, PAUSED]: self._process_event(self.their_role, event) self._receive_buffer.compress() if event is NEED_DATA: diff --git a/h11/_state.py b/h11/_state.py index 2c02ee4..c1d2e5c 100644 --- a/h11/_state.py +++ b/h11/_state.py @@ -112,7 +112,7 @@ # script to keep it in sync! from ._events import * -from ._util import LocalProtocolError, Sentinel +from ._util import LocalProtocolError, make_sentinel # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = [] @@ -125,7 +125,7 @@ # Switch types "_SWITCH_UPGRADE _SWITCH_CONNECT").split() for token in sentinels: - globals()[token] = Sentinel(token) + globals()[token] = make_sentinel(token) __all__ += [s for s in sentinels if not s.startswith("_")] diff --git a/h11/_util.py b/h11/_util.py index b4d318f..dff406c 100644 --- a/h11/_util.py +++ b/h11/_util.py @@ -1,7 +1,7 @@ import sys __all__ = ["ProtocolError", "LocalProtocolError", "RemoteProtocolError", - "validate", "Sentinel", "bytesify"] + "validate", "make_sentinel", "bytesify"] class ProtocolError(Exception): """Exception indicating a violation of the HTTP/1.1 protocol. @@ -92,13 +92,21 @@ def validate(regex, data, msg="malformed data"): return match.groupdict() # Sentinel values -# Inherits identity-based comparison and hashing from object -class Sentinel(object): - def __init__(self, name): - self._name = name - +# +# - Inherit identity-based comparison and hashing from object +# - Have a nice repr +# - Have a *bonus property*: type(sentinel) is sentinel +# +# The bonus property is useful if you want to take the return value from +# next_event() and do some sort of dispatch based on type(event). +class _SentinelBase(type): def __repr__(self): - return self._name + return self.__name__ + +def make_sentinel(name): + cls = _SentinelBase(name, (_SentinelBase,), {}) + cls.__class__ = cls + return cls # Used for methods, request targets, HTTP versions, header names, and header # values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always diff --git a/h11/tests/test_util.py b/h11/tests/test_util.py index 30cf589..806b0a7 100644 --- a/h11/tests/test_util.py +++ b/h11/tests/test_util.py @@ -53,11 +53,18 @@ def test_validate(): with pytest.raises(LocalProtocolError): validate(my_re, b"0.1\n") -def test_Sentinel(): - S = Sentinel("S") +def test_make_sentinel(): + S = make_sentinel("S") assert repr(S) == "S" assert S == S + assert type(S).__name__ == "S" assert S in {S} + assert type(S) is S + S2 = make_sentinel("S2") + assert repr(S2) == "S2" + assert S != S2 + assert S not in {S2} + assert type(S) is not type(S2) def test_bytesify(): assert bytesify(b"123") == b"123"