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 f13da12
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 160 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
6 changes: 5 additions & 1 deletion docs/source/make-state-diagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import os.path
import subprocess
from enum import Enum

from h11._events import *
from h11._state import *
Expand Down Expand Up @@ -41,8 +42,11 @@ def e(self, source, target, label, color, italicize=False, weight=1):
quoted_label = "<<i>{}</i>>".format(label)
else:
quoted_label = '<{}>'.format(label)

source_name = source.name if isinstance(source, Enum) else str(source)
target_name = target.name if isinstance(target, Enum) else str(target)
self.edges.append(
'{source} -> {target} [\n'
'{source_name} -> {target_name} [\n'
' label={quoted_label},\n'
' color="{color}", fontcolor="{color}",\n'
' weight={weight},\n'
Expand Down
47 changes: 25 additions & 22 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 @@ -397,7 +400,7 @@ def receive_data(self, data: bytes) -> None:

def _extract_next_receive_event(
self,
) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
) -> Union[Event, PseudoEvent]:
state = self.their_state
# We don't pause immediately when they enter DONE, because even in
# DONE state we can still process a ConnectionClosed() event. But
Expand All @@ -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
Loading

0 comments on commit f13da12

Please sign in to comment.