Skip to content

Commit c42389e

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 c42389e

10 files changed

+98
-158
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

h11/_connection.py

+24-21
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
@@ -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

h11/_state.py

+58-52
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@
110110
# tables. But it can't automatically read the transitions that are written
111111
# directly in Python code. So if you touch those, you need to also update the
112112
# script to keep it in sync!
113+
from enum import auto, Enum
113114
from typing import cast, Dict, Optional, Set, Tuple, Type, Union
114115

115116
from ._events import *
116-
from ._util import LocalProtocolError, Sentinel
117+
from ._util import LocalProtocolError
117118

118119
# Everything in __all__ gets re-exported as part of the h11 public API.
119120
__all__ = [
@@ -131,65 +132,64 @@
131132
]
132133

133134

134-
class CLIENT(Sentinel, metaclass=Sentinel):
135-
pass
135+
class Role(Enum):
136+
CLIENT = auto()
137+
SERVER = auto()
136138

137139

138-
class SERVER(Sentinel, metaclass=Sentinel):
139-
pass
140+
CLIENT = Role.CLIENT
141+
SERVER = Role.SERVER
140142

141143

142-
# States
143-
class IDLE(Sentinel, metaclass=Sentinel):
144-
pass
145-
146-
147-
class SEND_RESPONSE(Sentinel, metaclass=Sentinel):
148-
pass
149-
150-
151-
class SEND_BODY(Sentinel, metaclass=Sentinel):
152-
pass
153-
144+
class State(Enum):
145+
IDLE = auto()
146+
SEND_RESPONSE = auto()
147+
SEND_BODY = auto()
148+
DONE = auto()
149+
MUST_CLOSE = auto()
150+
CLOSED = auto()
151+
ERROR = auto()
154152

155-
class DONE(Sentinel, metaclass=Sentinel):
156-
pass
157153

158-
159-
class MUST_CLOSE(Sentinel, metaclass=Sentinel):
160-
pass
161-
162-
163-
class CLOSED(Sentinel, metaclass=Sentinel):
164-
pass
165-
166-
167-
class ERROR(Sentinel, metaclass=Sentinel):
168-
pass
154+
# States
155+
IDLE = State.IDLE
156+
SEND_RESPONSE = State.SEND_RESPONSE
157+
SEND_BODY = State.SEND_BODY
158+
DONE = State.DONE
159+
MUST_CLOSE = State.MUST_CLOSE
160+
CLOSED = State.CLOSED
161+
ERROR = State.ERROR
169162

170163

171-
# Switch types
172-
class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):
173-
pass
164+
class SwitchState(Enum):
165+
MIGHT_SWITCH_PROTOCOL = auto()
166+
SWITCHED_PROTOCOL = auto()
174167

175168

176-
class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):
177-
pass
169+
# Switch states
170+
MIGHT_SWITCH_PROTOCOL = SwitchState.MIGHT_SWITCH_PROTOCOL
171+
SWITCHED_PROTOCOL = SwitchState.SWITCHED_PROTOCOL
178172

179173

180-
class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):
181-
pass
174+
class SwitchType(Enum):
175+
UPGRADE = auto()
176+
CONNECT = auto()
182177

183178

184-
class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
185-
pass
179+
_SWITCH_UPGRADE = SwitchType.UPGRADE
180+
_SWITCH_CONNECT = SwitchType.CONNECT
186181

187182

188183
EventTransitionType = Dict[
189-
Type[Sentinel],
184+
Role,
190185
Dict[
191-
Type[Sentinel],
192-
Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],
186+
Union[State, SwitchState],
187+
Dict[
188+
Union[
189+
Type[Event], Tuple[Type[Event], Role], Tuple[Type[Event], SwitchType]
190+
],
191+
Union[State, SwitchState],
192+
],
193193
],
194194
]
195195

@@ -227,7 +227,8 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
227227
}
228228

229229
StateTransitionType = Dict[
230-
Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]]
230+
Tuple[Union[State, SwitchState], Union[State, SwitchState]],
231+
Dict[Role, Union[State, SwitchState]],
231232
]
232233

233234
# NB: there are also some special-case state-triggered transitions hard-coded
@@ -256,29 +257,32 @@ def __init__(self) -> None:
256257

257258
# This is a subset of {UPGRADE, CONNECT}, containing the proposals
258259
# made by the client for switching protocols.
259-
self.pending_switch_proposals: Set[Type[Sentinel]] = set()
260+
self.pending_switch_proposals: Set[SwitchType] = set()
260261

261-
self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}
262+
self.states: Dict[Role, Union[State, SwitchState]] = {
263+
CLIENT: IDLE,
264+
SERVER: IDLE,
265+
}
262266

263-
def process_error(self, role: Type[Sentinel]) -> None:
267+
def process_error(self, role: Role) -> None:
264268
self.states[role] = ERROR
265269
self._fire_state_triggered_transitions()
266270

267271
def process_keep_alive_disabled(self) -> None:
268272
self.keep_alive = False
269273
self._fire_state_triggered_transitions()
270274

271-
def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:
275+
def process_client_switch_proposal(self, switch_event: SwitchType) -> None:
272276
self.pending_switch_proposals.add(switch_event)
273277
self._fire_state_triggered_transitions()
274278

275279
def process_event(
276280
self,
277-
role: Type[Sentinel],
281+
role: Role,
278282
event_type: Type[Event],
279-
server_switch_event: Optional[Type[Sentinel]] = None,
283+
server_switch_event: Optional[SwitchType] = None,
280284
) -> None:
281-
_event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type
285+
_event_type: Union[Type[Event], Tuple[Type[Event], SwitchType]] = event_type
282286
if server_switch_event is not None:
283287
assert role is SERVER
284288
if server_switch_event not in self.pending_switch_proposals:
@@ -300,8 +304,10 @@ def process_event(
300304

301305
def _fire_event_triggered_transitions(
302306
self,
303-
role: Type[Sentinel],
304-
event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],
307+
role: Role,
308+
event_type: Union[
309+
Type[Event], Tuple[Type[Event], SwitchType], Tuple[Type[Event], Role]
310+
],
305311
) -> None:
306312
state = self.states[role]
307313
try:

0 commit comments

Comments
 (0)