Skip to content
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

Closed
wants to merge 15 commits into from
Closed

Update types for h11 v0.13 #526

wants to merge 15 commits into from

Conversation

zanieb
Copy link
Contributor

@zanieb zanieb commented Mar 25, 2022

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

fsecada01 and 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

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
Contributor Author

zanieb commented Apr 9, 2022

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.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

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

@@ -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
Copy link
Member

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
Member

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

Copy link
Member

@graingert graingert Jun 19, 2022

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

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

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
Copy link
Member

@graingert graingert Jun 19, 2022

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
Contributor Author

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
Member

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
Contributor Author

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

Copy link
Contributor Author

@zanieb zanieb Aug 26, 2022

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.

Copy link
Contributor Author

@zanieb zanieb Aug 26, 2022

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]

@graingert
Copy link
Member

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
Copy link
Contributor

simonw commented Aug 15, 2022

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
Copy link
Contributor Author

zanieb commented Aug 26, 2022

@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
Contributor Author

zanieb commented Sep 1, 2022

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
Sponsor Member

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
Contributor Author

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

eseglem commented Sep 25, 2022

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
Copy link
Contributor Author

zanieb commented Sep 25, 2022

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

Edit: See #579

@tomchristie
Copy link
Member

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

@zanieb
Copy link
Contributor Author

zanieb commented Sep 26, 2022

I think that's the best approach!

@zanieb zanieb closed this Sep 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants