Skip to content

Commit

Permalink
Switch from Sentinel types to Enums
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pgjones committed Aug 25, 2022
1 parent 95cd3fa commit c42389e
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 158 deletions.
24 changes: 1 addition & 23 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -365,29 +365,7 @@ from :meth:`Connection.next_event`:
.. data:: NEED_DATA
PAUSED

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``.

.. _sentinel-type-trickiness:

Finally, h11's constants have a quirky feature that can sometimes be
useful: they are instances of themselves.

.. ipython:: python
type(h11.NEED_DATA) is h11.NEED_DATA
type(h11.PAUSED) is h11.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.

These special constants are part of a ``PseudoEvent`` enum.

The Connection object
---------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ v0.7.0 (2016-11-25)

New features (backwards compatible):

* Made it so that sentinels are :ref:`instances of themselves
<sentinel-type-trickiness>`, to enable certain dispatch tricks on
the return value of :func:`Connection.next_event` (see `issue #8
* 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/python-hyper/h11/issues/8>`__ for discussion).

* Added :data:`Data.chunk_start` and :data:`Data.chunk_end` properties
Expand Down
45 changes: 24 additions & 21 deletions h11/_connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# This contains the main Connection class. Everything in h11 revolves around
# this.
from enum import auto, Enum
from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Type, Union

from ._events import (
Expand All @@ -22,27 +23,29 @@
DONE,
ERROR,
MIGHT_SWITCH_PROTOCOL,
Role,
SEND_BODY,
SERVER,
State,
SWITCHED_PROTOCOL,
SwitchState,
SwitchType,
)
from ._util import ( # Import the internal things we need
LocalProtocolError,
RemoteProtocolError,
Sentinel,
)
from ._util import LocalProtocolError # Import the internal things we need
from ._util import RemoteProtocolError
from ._writers import WRITERS, WritersType

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


class NEED_DATA(Sentinel, metaclass=Sentinel):
pass
class PseudoEvent(Enum):
NEED_DATA = auto()
PAUSED = auto()


class PAUSED(Sentinel, metaclass=Sentinel):
pass
NEED_DATA = PseudoEvent.NEED_DATA
PAUSED = PseudoEvent.PAUSED


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

def __init__(
self,
our_role: Type[Sentinel],
our_role: Role,
max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE,
) -> None:
self._max_incomplete_event_size = max_incomplete_event_size
# State and role tracking
if our_role not in (CLIENT, SERVER):
raise ValueError("expected CLIENT or SERVER, not {!r}".format(our_role))
self.our_role = our_role
self.their_role: Type[Sentinel]
self.their_role: Role
if our_role is CLIENT:
self.their_role = SERVER
else:
Expand Down Expand Up @@ -192,7 +195,7 @@ def __init__(
self.client_is_waiting_for_100_continue = False

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

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

@property
def their_state(self) -> Type[Sentinel]:
def their_state(self) -> Union[State, SwitchState]:
"""The current state of whichever role we are NOT playing. See
:ref:`state-machine` for details.
"""
Expand Down Expand Up @@ -240,12 +243,12 @@ def start_next_cycle(self) -> None:
assert not self.client_is_waiting_for_100_continue
self._respond_to_state_changes(old_states)

def _process_error(self, role: Type[Sentinel]) -> None:
def _process_error(self, role: Role) -> None:
old_states = dict(self._cstate.states)
self._cstate.process_error(role)
self._respond_to_state_changes(old_states)

def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
def _server_switch_event(self, event: Event) -> Optional[SwitchType]:
if type(event) is InformationalResponse and event.status_code == 101:
return _SWITCH_UPGRADE
if type(event) is Response:
Expand All @@ -257,7 +260,7 @@ def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
return None

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

def _get_io_object(
self,
role: Type[Sentinel],
role: Role,
event: Optional[Event],
io_dict: Union[ReadersType, WritersType],
) -> Optional[Callable[..., Any]]:
Expand All @@ -323,13 +326,13 @@ def _get_io_object(
else:
# General case: the io_dict just has the appropriate reader/writer
# for this state
return io_dict.get((role, state)) # type: ignore[return-value]
return io_dict.get((role, state)) # type: ignore[arg-type, return-value]

# This must be called after any action that might have caused
# self._cstate.states to change.
def _respond_to_state_changes(
self,
old_states: Dict[Type[Sentinel], Type[Sentinel]],
old_states: Dict[Role, Union[State, SwitchState]],
event: Optional[Event] = None,
) -> None:
# Update reader/writer
Expand Down Expand Up @@ -423,7 +426,7 @@ def _extract_next_receive_event(
event = NEED_DATA
return event # type: ignore[no-any-return]

def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
def next_event(self) -> Union[Event, PseudoEvent]:
"""Parse the next event out of our receive buffer, update our internal
state, and return it.
Expand Down
6 changes: 4 additions & 2 deletions h11/_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
DONE,
IDLE,
MUST_CLOSE,
Role,
SEND_BODY,
SEND_RESPONSE,
SERVER,
State,
)
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
from ._util import LocalProtocolError, RemoteProtocolError, validate

__all__ = ["READERS"]

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


ReadersType = Dict[
Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
Union[State, Tuple[Role, State]],
Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
]

Expand Down
110 changes: 58 additions & 52 deletions h11/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,11 @@
# tables. But it can't automatically read the transitions that are written
# directly in Python code. So if you touch those, you need to also update the
# script to keep it in sync!
from enum import auto, Enum
from typing import cast, Dict, Optional, Set, Tuple, Type, Union

from ._events import *
from ._util import LocalProtocolError, Sentinel
from ._util import LocalProtocolError

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


class CLIENT(Sentinel, metaclass=Sentinel):
pass
class Role(Enum):
CLIENT = auto()
SERVER = auto()


class SERVER(Sentinel, metaclass=Sentinel):
pass
CLIENT = Role.CLIENT
SERVER = Role.SERVER


# States
class IDLE(Sentinel, metaclass=Sentinel):
pass


class SEND_RESPONSE(Sentinel, metaclass=Sentinel):
pass


class SEND_BODY(Sentinel, metaclass=Sentinel):
pass

class State(Enum):
IDLE = auto()
SEND_RESPONSE = auto()
SEND_BODY = auto()
DONE = auto()
MUST_CLOSE = auto()
CLOSED = auto()
ERROR = auto()

class DONE(Sentinel, metaclass=Sentinel):
pass


class MUST_CLOSE(Sentinel, metaclass=Sentinel):
pass


class CLOSED(Sentinel, metaclass=Sentinel):
pass


class ERROR(Sentinel, metaclass=Sentinel):
pass
# States
IDLE = State.IDLE
SEND_RESPONSE = State.SEND_RESPONSE
SEND_BODY = State.SEND_BODY
DONE = State.DONE
MUST_CLOSE = State.MUST_CLOSE
CLOSED = State.CLOSED
ERROR = State.ERROR


# Switch types
class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):
pass
class SwitchState(Enum):
MIGHT_SWITCH_PROTOCOL = auto()
SWITCHED_PROTOCOL = auto()


class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):
pass
# Switch states
MIGHT_SWITCH_PROTOCOL = SwitchState.MIGHT_SWITCH_PROTOCOL
SWITCHED_PROTOCOL = SwitchState.SWITCHED_PROTOCOL


class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):
pass
class SwitchType(Enum):
UPGRADE = auto()
CONNECT = auto()


class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
pass
_SWITCH_UPGRADE = SwitchType.UPGRADE
_SWITCH_CONNECT = SwitchType.CONNECT


EventTransitionType = Dict[
Type[Sentinel],
Role,
Dict[
Type[Sentinel],
Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],
Union[State, SwitchState],
Dict[
Union[
Type[Event], Tuple[Type[Event], Role], Tuple[Type[Event], SwitchType]
],
Union[State, SwitchState],
],
],
]

Expand Down Expand Up @@ -227,7 +227,8 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
}

StateTransitionType = Dict[
Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]]
Tuple[Union[State, SwitchState], Union[State, SwitchState]],
Dict[Role, Union[State, SwitchState]],
]

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

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

self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}
self.states: Dict[Role, Union[State, SwitchState]] = {
CLIENT: IDLE,
SERVER: IDLE,
}

def process_error(self, role: Type[Sentinel]) -> None:
def process_error(self, role: Role) -> None:
self.states[role] = ERROR
self._fire_state_triggered_transitions()

def process_keep_alive_disabled(self) -> None:
self.keep_alive = False
self._fire_state_triggered_transitions()

def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:
def process_client_switch_proposal(self, switch_event: SwitchType) -> None:
self.pending_switch_proposals.add(switch_event)
self._fire_state_triggered_transitions()

def process_event(
self,
role: Type[Sentinel],
role: Role,
event_type: Type[Event],
server_switch_event: Optional[Type[Sentinel]] = None,
server_switch_event: Optional[SwitchType] = None,
) -> None:
_event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type
_event_type: Union[Type[Event], Tuple[Type[Event], SwitchType]] = event_type
if server_switch_event is not None:
assert role is SERVER
if server_switch_event not in self.pending_switch_proposals:
Expand All @@ -300,8 +304,10 @@ def process_event(

def _fire_event_triggered_transitions(
self,
role: Type[Sentinel],
event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],
role: Role,
event_type: Union[
Type[Event], Tuple[Type[Event], SwitchType], Tuple[Type[Event], Role]
],
) -> None:
state = self.states[role]
try:
Expand Down
Loading

0 comments on commit c42389e

Please sign in to comment.