-
-
Notifications
You must be signed in to change notification settings - Fork 343
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
Permit referring to some generic types in generic ways #908
Changes from all commits
b93ed9e
ac9ce99
eff920e
25e82dd
77edcbb
2b8d2b8
22747c2
6fda343
870d0d3
7c2d2d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
:class:`~trio.abc.SendChannel`, :class:`~trio.abc.ReceiveChannel`, :class:`~trio.abc.Listener`, | ||
and :func:`~trio.open_memory_channel` can now be referenced using a generic type parameter | ||
(the type of object sent over the channel or produced by the listener) using PEP 484 syntax: | ||
``trio.abc.SendChannel[bytes]``, ``trio.abc.Listener[trio.SocketStream]``, | ||
``trio.open_memory_channel[MyMessage](5)``, etc. The added type information does not change | ||
the runtime semantics, but permits better integration with external static type checkers. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
import signal | ||
import sys | ||
import pathlib | ||
from functools import wraps | ||
from functools import wraps, update_wrapper | ||
import typing as t | ||
|
||
import async_generator | ||
|
@@ -22,6 +22,7 @@ | |
"ConflictDetector", | ||
"fixup_module_metadata", | ||
"fspath", | ||
"generic_function", | ||
] | ||
|
||
# Equivalent to the C function raise(), which Python doesn't wrap | ||
|
@@ -171,7 +172,15 @@ def decorator(func): | |
|
||
|
||
def fixup_module_metadata(module_name, namespace): | ||
seen_ids = set() | ||
|
||
def fix_one(obj): | ||
# avoid infinite recursion (relevant when using | ||
# typing.Generic, for example) | ||
if id(obj) in seen_ids: | ||
return | ||
seen_ids.add(id(obj)) | ||
|
||
mod = getattr(obj, "__module__", None) | ||
if mod is not None and mod.startswith("trio."): | ||
obj.__module__ = module_name | ||
|
@@ -242,3 +251,31 @@ def fspath(path) -> t.Union[str, bytes]: | |
|
||
if hasattr(os, "fspath"): | ||
fspath = os.fspath # noqa | ||
|
||
|
||
class generic_function: | ||
"""Decorator that makes a function indexable, to communicate | ||
non-inferrable generic type parameters to a static type checker. | ||
|
||
If you write:: | ||
|
||
@generic_function | ||
def open_memory_channel(max_buffer_size: int) -> Tuple[ | ||
SendChannel[T], ReceiveChannel[T] | ||
]: ... | ||
|
||
it is valid at runtime to say ``open_memory_channel[bytes](5)``. | ||
This behaves identically to ``open_memory_channel(5)`` at runtime, | ||
and currently won't type-check without a mypy plugin or clever stubs, | ||
but at least it becomes possible to write those. | ||
""" | ||
|
||
def __init__(self, fn): | ||
update_wrapper(self, fn) | ||
self._fn = fn | ||
|
||
def __call__(self, *args, **kwargs): | ||
return self._fn(*args, **kwargs) | ||
|
||
def __getitem__(self, _): | ||
return self | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So you actually can make But I think it does require inlining this class into the definition of If that's where were going to end up, then maybe setting up a standalone There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with that approach is that AFAICT, when you write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh ick. |
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 the
bound=AsyncResource
will disappear when we fix #636. We could start skipping it now, or we could put it in and then take it out later, doesn't make much difference IMO.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.
You think we'll have Listeners that provide objects not representable as an AsyncResource? I'm a bit surprised at that, can you give an example?
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 don't have an example in mind. But the reason we restrict it to
AsyncResource
right now is becauseserve_listeners
automatically closes the object after the handler returns:trio/trio/_highlevel_serve_listeners.py
Lines 25 to 29 in 0a815a0
With #636, this code goes away, because the
Listener
itself is responsible for managing the handler lifetime. So... at that point it's really between the person developing aListener[X]
and their users, what kind of APIX
should support :-).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.
Ah, that makes sense! I think it's probably sensible to keep the AsyncResource bound for now, since it reflects the current requirements, and we can easily change it later.