Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
9 changes: 9 additions & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://github.com/njsmith/h11/issues/8>`).


v0.6.0 (2016-10-24)
-------------------

Expand Down
8 changes: 4 additions & 4 deletions h11/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions h11/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = []
Expand 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("_")]

Expand Down
22 changes: 15 additions & 7 deletions h11/_util.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions h11/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down