Skip to content

Commit f13da12

Browse files
committed
Switch from Sentinel types to Enums
The latter are much easier to work with when type hinting and can be used successfully with mypyc, whereas the former are sadly very difficult in both aspects. This loses the nice property of `type(NEED_DATA) is NEED_DATA` (as expanded on in the deleted docs section). However, I don't think this is widely used in practice.
1 parent 95cd3fa commit f13da12

11 files changed

+104
-160
lines changed

docs/source/api.rst

+1-23
Original file line numberDiff line numberDiff line change
@@ -365,29 +365,7 @@ from :meth:`Connection.next_event`:
365365
.. data:: NEED_DATA
366366
PAUSED
367367

368-
All of these behave the same, and their behavior is modeled after
369-
:data:`None`: they're opaque singletons, their :meth:`__repr__` is
370-
their name, and you compare them with ``is``.
371-
372-
.. _sentinel-type-trickiness:
373-
374-
Finally, h11's constants have a quirky feature that can sometimes be
375-
useful: they are instances of themselves.
376-
377-
.. ipython:: python
378-
379-
type(h11.NEED_DATA) is h11.NEED_DATA
380-
type(h11.PAUSED) is h11.PAUSED
381-
382-
The main application of this is that when handling the return value
383-
from :meth:`Connection.next_event`, which is sometimes an instance of
384-
an event class and sometimes :data:`NEED_DATA` or :data:`PAUSED`, you
385-
can always call ``type(event)`` to get something useful to dispatch
386-
one, using e.g. a handler table, :func:`functools.singledispatch`, or
387-
calling ``getattr(some_object, "handle_" +
388-
type(event).__name__)``. Not that this kind of dispatch-based strategy
389-
is always the best approach -- but the option is there if you want it.
390-
368+
These special constants are part of a ``PseudoEvent`` enum.
391369

392370
The Connection object
393371
---------------------

docs/source/changes.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ v0.7.0 (2016-11-25)
181181

182182
New features (backwards compatible):
183183

184-
* Made it so that sentinels are :ref:`instances of themselves
185-
<sentinel-type-trickiness>`, to enable certain dispatch tricks on
186-
the return value of :func:`Connection.next_event` (see `issue #8
184+
* Made it so that sentinels are instances of themselves, to enable
185+
certain dispatch tricks on the return value of
186+
:func:`Connection.next_event` (see `issue #8
187187
<https://github.com/python-hyper/h11/issues/8>`__ for discussion).
188188

189189
* Added :data:`Data.chunk_start` and :data:`Data.chunk_end` properties

docs/source/make-state-diagrams.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import os.path
77
import subprocess
8+
from enum import Enum
89

910
from h11._events import *
1011
from h11._state import *
@@ -41,8 +42,11 @@ def e(self, source, target, label, color, italicize=False, weight=1):
4142
quoted_label = "<<i>{}</i>>".format(label)
4243
else:
4344
quoted_label = '<{}>'.format(label)
45+
46+
source_name = source.name if isinstance(source, Enum) else str(source)
47+
target_name = target.name if isinstance(target, Enum) else str(target)
4448
self.edges.append(
45-
'{source} -> {target} [\n'
49+
'{source_name} -> {target_name} [\n'
4650
' label={quoted_label},\n'
4751
' color="{color}", fontcolor="{color}",\n'
4852
' weight={weight},\n'

h11/_connection.py

+25-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# This contains the main Connection class. Everything in h11 revolves around
22
# this.
3+
from enum import auto, Enum
34
from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Type, Union
45

56
from ._events import (
@@ -22,27 +23,29 @@
2223
DONE,
2324
ERROR,
2425
MIGHT_SWITCH_PROTOCOL,
26+
Role,
2527
SEND_BODY,
2628
SERVER,
29+
State,
2730
SWITCHED_PROTOCOL,
31+
SwitchState,
32+
SwitchType,
2833
)
29-
from ._util import ( # Import the internal things we need
30-
LocalProtocolError,
31-
RemoteProtocolError,
32-
Sentinel,
33-
)
34+
from ._util import LocalProtocolError # Import the internal things we need
35+
from ._util import RemoteProtocolError
3436
from ._writers import WRITERS, WritersType
3537

3638
# Everything in __all__ gets re-exported as part of the h11 public API.
3739
__all__ = ["Connection", "NEED_DATA", "PAUSED"]
3840

3941

40-
class NEED_DATA(Sentinel, metaclass=Sentinel):
41-
pass
42+
class PseudoEvent(Enum):
43+
NEED_DATA = auto()
44+
PAUSED = auto()
4245

4346

44-
class PAUSED(Sentinel, metaclass=Sentinel):
45-
pass
47+
NEED_DATA = PseudoEvent.NEED_DATA
48+
PAUSED = PseudoEvent.PAUSED
4649

4750

4851
# If we ever have this much buffered without it making a complete parseable
@@ -154,15 +157,15 @@ class Connection:
154157

155158
def __init__(
156159
self,
157-
our_role: Type[Sentinel],
160+
our_role: Role,
158161
max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE,
159162
) -> None:
160163
self._max_incomplete_event_size = max_incomplete_event_size
161164
# State and role tracking
162165
if our_role not in (CLIENT, SERVER):
163166
raise ValueError("expected CLIENT or SERVER, not {!r}".format(our_role))
164167
self.our_role = our_role
165-
self.their_role: Type[Sentinel]
168+
self.their_role: Role
166169
if our_role is CLIENT:
167170
self.their_role = SERVER
168171
else:
@@ -192,7 +195,7 @@ def __init__(
192195
self.client_is_waiting_for_100_continue = False
193196

194197
@property
195-
def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]:
198+
def states(self) -> Dict[Role, Union[State, SwitchState]]:
196199
"""A dictionary like::
197200
198201
{CLIENT: <client state>, SERVER: <server state>}
@@ -203,14 +206,14 @@ def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]:
203206
return dict(self._cstate.states)
204207

205208
@property
206-
def our_state(self) -> Type[Sentinel]:
209+
def our_state(self) -> Union[State, SwitchState]:
207210
"""The current state of whichever role we are playing. See
208211
:ref:`state-machine` for details.
209212
"""
210213
return self._cstate.states[self.our_role]
211214

212215
@property
213-
def their_state(self) -> Type[Sentinel]:
216+
def their_state(self) -> Union[State, SwitchState]:
214217
"""The current state of whichever role we are NOT playing. See
215218
:ref:`state-machine` for details.
216219
"""
@@ -240,12 +243,12 @@ def start_next_cycle(self) -> None:
240243
assert not self.client_is_waiting_for_100_continue
241244
self._respond_to_state_changes(old_states)
242245

243-
def _process_error(self, role: Type[Sentinel]) -> None:
246+
def _process_error(self, role: Role) -> None:
244247
old_states = dict(self._cstate.states)
245248
self._cstate.process_error(role)
246249
self._respond_to_state_changes(old_states)
247250

248-
def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
251+
def _server_switch_event(self, event: Event) -> Optional[SwitchType]:
249252
if type(event) is InformationalResponse and event.status_code == 101:
250253
return _SWITCH_UPGRADE
251254
if type(event) is Response:
@@ -257,7 +260,7 @@ def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
257260
return None
258261

259262
# All events go through here
260-
def _process_event(self, role: Type[Sentinel], event: Event) -> None:
263+
def _process_event(self, role: Role, event: Event) -> None:
261264
# First, pass the event through the state machine to make sure it
262265
# succeeds.
263266
old_states = dict(self._cstate.states)
@@ -307,7 +310,7 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None:
307310

308311
def _get_io_object(
309312
self,
310-
role: Type[Sentinel],
313+
role: Role,
311314
event: Optional[Event],
312315
io_dict: Union[ReadersType, WritersType],
313316
) -> Optional[Callable[..., Any]]:
@@ -323,13 +326,13 @@ def _get_io_object(
323326
else:
324327
# General case: the io_dict just has the appropriate reader/writer
325328
# for this state
326-
return io_dict.get((role, state)) # type: ignore[return-value]
329+
return io_dict.get((role, state)) # type: ignore[arg-type, return-value]
327330

328331
# This must be called after any action that might have caused
329332
# self._cstate.states to change.
330333
def _respond_to_state_changes(
331334
self,
332-
old_states: Dict[Type[Sentinel], Type[Sentinel]],
335+
old_states: Dict[Role, Union[State, SwitchState]],
333336
event: Optional[Event] = None,
334337
) -> None:
335338
# Update reader/writer
@@ -397,7 +400,7 @@ def receive_data(self, data: bytes) -> None:
397400

398401
def _extract_next_receive_event(
399402
self,
400-
) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
403+
) -> Union[Event, PseudoEvent]:
401404
state = self.their_state
402405
# We don't pause immediately when they enter DONE, because even in
403406
# DONE state we can still process a ConnectionClosed() event. But
@@ -423,7 +426,7 @@ def _extract_next_receive_event(
423426
event = NEED_DATA
424427
return event # type: ignore[no-any-return]
425428

426-
def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
429+
def next_event(self) -> Union[Event, PseudoEvent]:
427430
"""Parse the next event out of our receive buffer, update our internal
428431
state, and return it.
429432

h11/_readers.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
DONE,
2929
IDLE,
3030
MUST_CLOSE,
31+
Role,
3132
SEND_BODY,
3233
SEND_RESPONSE,
3334
SERVER,
35+
State,
3436
)
35-
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
37+
from ._util import LocalProtocolError, RemoteProtocolError, validate
3638

3739
__all__ = ["READERS"]
3840

@@ -225,7 +227,7 @@ def expect_nothing(buf: ReceiveBuffer) -> None:
225227

226228

227229
ReadersType = Dict[
228-
Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
230+
Union[State, Tuple[Role, State]],
229231
Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
230232
]
231233

0 commit comments

Comments
 (0)