-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update types for h11 v0.13 #526
Conversation
Allow for h11 V. 013. Facilitates installation alongside latest version of Uvicorn.
This also reverted commit f9a9847 which was dropped during rebase with the `master` branch. We probably do not want to suggest that passing `None` to the `write` function is proper. The short circuit there (return on empty buffer) should be targeted to an empty byte array.
I think it may not be a good idea to pin h11 under 0.14, it can cause unnecessary conflicts. See: https://hynek.me/articles/semver-will-not-save-you/ |
That sentiment is the point of issue #509. This pull request is to add compatibility which is necessary whether the pin is removed or not. There are other dependencies with upper versions in this project and that decision should be made separately from this fix. |
httpcore/_async/http11.py
Outdated
@@ -191,8 +195,11 @@ async def _receive_event(self, timeout: Optional[float] = None) -> H11Event: | |||
raise RemoteProtocolError(msg) | |||
|
|||
self._h11_state.receive_data(data) | |||
elif event is h11.PAUSED: | |||
# TODO: Implement handling for paused |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
httpcore/_sync/http11.py
Outdated
@@ -191,8 +180,11 @@ def _receive_event(self, timeout: Optional[float] = None) -> H11Event: | |||
raise RemoteProtocolError(msg) | |||
|
|||
self._h11_state.receive_data(data) | |||
elif event is h11.PAUSED: | |||
# TODO: Implement handling for paused |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
event = cast( | ||
Union[h11.Event, h11.NEED_DATA, h11.PAUSED], | ||
self._h11_state.next_event(), | ||
) | ||
|
||
if event is h11.NEED_DATA: | ||
if isinstance(event, h11.NEED_DATA): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry how much this cast and isinstance will slow stuff down - as this is a hot loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can hack around this by moving the cast outside the loop, but I'd want other maintainers to have a think on what's best here
httpcore/httpcore/_async/http11.py
Lines 30 to 49 in 627db6c
if TYPE_CHECKING: | |
from typing_extensions import Literal, Protocol, TypeAlias | |
class _Sentinel(enum.Enum): | |
PAUSED = enum.auto() | |
NEED_DATA = enum.auto() | |
_PausedType: TypeAlias = Literal[_Sentinel.PAUSED] | |
_NeedDataType: TypeAlias = Literal[_Sentinel.NEED_DATA] | |
_PAUSED: _PausedType = _Sentinel.PAUSED | |
_NEED_DATA: _NeedDataType = _Sentinel.NEED_DATA | |
class _NextEventType(Protocol): | |
async def __call__(self) -> Union[h11.Event, _PausedType, _NeedDataType]: | |
... | |
else: | |
_PausedType = _PAUSED = h11.PAUSED | |
_NeedDataType = _NEED_DATA = h11.NEED_DATA | |
_Sentinel = _NextEventType = object |
httpcore/httpcore/_async/http11.py
Lines 195 to 207 in 627db6c
async def _receive_event( | |
self, timeout: Optional[float] = None | |
) -> Union[h11.Event, _PausedType]: | |
# The h11 type signature uses a private return type | |
next_event = cast(_NextEventType, self._h11_state.next_event) | |
while True: | |
with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): | |
event = next_event() | |
if event is _NEED_DATA: | |
data = await self._network_stream.read( | |
self.READ_NUM_BYTES, timeout=timeout | |
) |
|
||
def _send_event( | ||
self, event: H11Event, timeout: Optional[float] = None | ||
self, event: h11.Event, timeout: Optional[float] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we still want our send_event to have a specific allowlist of events we send:
H11SendEvent = Union[
h11.Request,
h11.Data,
h11.EndOfMessage,
]
self, event: h11.Event, timeout: Optional[float] = None | |
self, event: H11SendEvent, timeout: Optional[float] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 makes sense to me. Seems like we would need this with the overload for correct typing.
) -> None: | ||
bytes_to_send = self._h11_state.send(event) | ||
self._network_stream.write(bytes_to_send, timeout=timeout) | ||
if bytes_to_send is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'll see if I can contribute that upstream
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, this would actually be a pretty big change upstream as the following is not a valid overload:
@overload
def send(self, event: ConnectionClosed) -> None:
...
@overload
def send(self, event: Event) -> bytes:
...
def send(self, event: Event) -> Optional[bytes]:
h11/_connection.py:505: error: Overloaded function signatures 1 and 2 overlap with incompatible return types [misc]
We'd need to introduce a separate base type (like Event
) that indicates a null return.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also exploring a generic
SendType = TypeVar('SendType', None, bytes)
class Event(ABC, Generic[SendType]):
...
class Foo(Event[bytes]):
...
class ConnectionClosed(Event[None]):
...
but this would break type-checks for any downstream library that uses Event
without a value for the generic e.g.
h11/_state.py:192: error: Missing type parameters for generic type "Event" [type-arg]
I think this is ready for review now @madkinsz |
I'm seeing a weird intermittent bug with https://lite.datasette.io/ which it looks like would be resolved by merging this PR!
It looks like something deep in the complex world of Pyodide may be installing I'm going to keep on looking for a workaround, but I thought I'd drop a note here about this issue in case anyone else runs into a similar problem and lands here from a search. |
@graingert I'm not sure the remaining optimizations regarding the overload are feasible in the short term. It may be worth merging the current improvements to unblock upgrades and address the overload related checks once we can implement that upstream. Can we get another maintainer to take a look at the hot-loop cast concerns? |
Note python-hyper/h11@04cc0f7 was merged a week ago so we can remove the aforementioned hot-loop cast once there's another h11 release. |
event = cast( | ||
Union[h11.Event, h11.NEED_DATA, h11.PAUSED], | ||
self._h11_state.next_event(), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I need to do this on uvicorn as well? 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if you do following release of python-hyper/h11#144 which updates that function's signature with narrowed return types.
h11 just released v0.14.0. Does that mean this PR can remove the "hot loop cast" and move forward with another release for httpcore? I have a server I deal with which behaves poorly and the h11 update will handle its behavior, so I look forward to having it make its way through h11 > httpcore > httpx releases. |
I'll look into updating this for h11 v0.14 Edit: See #579 |
I think that's the best approach! |
Replaces #503, performing the additional work necessary for type checks to pass.
A cast is necessary because h11 does not expose the
Sentinel
type and uses it as a return type annotation instead of specifying the subtypes that can be returned.I've opened a patch to h11 to update
next_event
to have a return type that is narrower and a part of their public API: python-hyper/h11#144 — Note this is now merged resolving the need for a cast, but is not yet released.There is currently no handling in
_receive_event
for theh11.PAUSED
sentinel event. I am not sure what behavior you'd like to add there and have left a stub that should be filled in before this is merged. The documentation forPAUSED
can be found at https://h11.readthedocs.io/en/latest/api.html#flow-control — Note this is not a concern and handling is performed here.Note this partially addresses #509 by allowing a newer version of h11 to be used, but does not close the issue as it requests that the upper pin be removed entirely.
Closes #503
Closes #498