Skip to content

Update types for h11 v0.13 - #526

Closed
zanieb wants to merge 15 commits into
encode:masterfrom
zanieb:h11-pin
Closed

Update types for h11 v0.13#526
zanieb wants to merge 15 commits into
encode:masterfrom
zanieb:h11-pin

Conversation

@zanieb

@zaniebzanieb commented Mar 25, 2022

Copy link
Copy Markdown
Contributor

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.

event=cast(
Union[h11.Event, h11.NEED_DATA, h11.PAUSED],
self._h11_state.next_event(),
)

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 the h11.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 for PAUSED 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

fsecada01and others added 6 commits March 25, 2022 11:12
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.
@GalaxySnail

Copy link
Copy Markdown

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/

@zanieb

Copy link
Copy Markdown
ContributorAuthor

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.

Comment threadhttpcore/_async/http11.py Outdated

self._h11_state.receive_data(data)
elif event is h11.PAUSED:
# TODO: Implement handling for paused

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment threadhttpcore/_sync/http11.py Outdated

self._h11_state.receive_data(data)
elif event is h11.PAUSED:
# TODO: Implement handling for paused

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +177 to +182
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):

Copy link
Copy Markdown
Contributor

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

@graingertgraingertJun 19, 2022

Copy link
Copy Markdown
Contributor

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

ifTYPE_CHECKING:
fromtyping_extensionsimportLiteral, 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):
asyncdef__call__(self) ->Union[h11.Event, _PausedType, _NeedDataType]:
...
else:
_PausedType=_PAUSED=h11.PAUSED
_NeedDataType=_NEED_DATA=h11.NEED_DATA
_Sentinel=_NextEventType=object

asyncdef_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)
whileTrue:
withmap_exceptions({h11.RemoteProtocolError: RemoteProtocolError}):
event=next_event()
ifeventis_NEED_DATA:
data=awaitself._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

@graingertgraingertJun 19, 2022

Copy link
Copy Markdown
Contributor

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,
]
Suggested change
self, event: h11.Event, timeout: Optional[float] =None
self, event: H11SendEvent, timeout: Optional[float] =None

Copy link
Copy Markdown
ContributorAuthor

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we know we don't send ConnectionClosed so this check is redundant

send should be defined using overload

Copy link
Copy Markdown
ContributorAuthor

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

@zaniebzaniebAug 26, 2022

Copy link
Copy Markdown
ContributorAuthor

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:

@overloaddefsend(self, event: ConnectionClosed) ->None:
...
@overloaddefsend(self, event: Event) ->bytes:
...
defsend(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.

@zaniebzaniebAug 26, 2022

Copy link
Copy Markdown
ContributorAuthor

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)
classEvent(ABC, Generic[SendType]):
...
classFoo(Event[bytes]):
...
classConnectionClosed(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]

@graingert

Copy link
Copy Markdown
Contributor

I think this is ready for review now @madkinsz

@graingert
graingert marked this pull request as ready for review June 19, 2022 17:34
@simonw

simonw commented Aug 15, 2022

Copy link
Copy Markdown
Contributor

I'm seeing a weird intermittent bug with https://lite.datasette.io/ which it looks like would be resolved by merging this PR!

 File "/lib/python3.10/asyncio/tasks.py", line 232, in __step
result = coro.send(None)
File "/lib/python3.10/site-packages/micropip/_micropip.py", line 310, in add_requirement
return await self.add_requirement_inner(req)
File "/lib/python3.10/site-packages/micropip/_micropip.py", line 391, in add_requirement_inner
if self.check_version_satisfied(req):
File "/lib/python3.10/site-packages/micropip/_micropip.py", line 337, in check_version_satisfied
raise ValueError(
ValueError: Requested 'h11<0.13,>=0.11', but h11==0.13.0 is already installed

It looks like something deep in the complex world of Pyodide may be installing h11==0.13 - even though I've tried in my code to ensure that h11==0.12 is installed first, here: https://github.com/simonw/datasette-lite/blob/d64455c2b7d89a32b8537c63f46db3579f0de91f/webworker.js#L53-L54

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.

@zanieb

zanieb commented Aug 26, 2022

Copy link
Copy Markdown
ContributorAuthor

@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?

@zanieb

Copy link
Copy Markdown
ContributorAuthor

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.

Comment on lines +177 to +180
event = cast(
Union[h11.Event, h11.NEED_DATA, h11.PAUSED],
self._h11_state.next_event(),
)

Copy link
Copy Markdown
Contributor

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? 😞

Copy link
Copy Markdown
ContributorAuthor

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.

@eseglem

Copy link
Copy Markdown

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.

@zanieb

zanieb commented Sep 25, 2022

Copy link
Copy Markdown
ContributorAuthor

I'll look into updating this for h11 v0.14

Edit: See #579

@zaniebzanieb mentioned this pull request Sep 25, 2022
@lovelydinosaur

Copy link
Copy Markdown
Contributor

Thanks @madkinsz - should we close this off in favour of #579?

@zanieb

Copy link
Copy Markdown
ContributorAuthor

I think that's the best approach!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@zanieb@GalaxySnail@graingert@simonw@eseglem@lovelydinosaur@Kludex@fsecada01