-
Notifications
You must be signed in to change notification settings - Fork 2.1k
[ty] Add support for sentinels (PEP 661) #25082
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
Changes from all commits
a273993
dc68a38
3378274
04024d5
aa243d0
bc460ea
ba9db8b
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,145 @@ | ||
| # Sentinels | ||
|
|
||
| ## `typing_extensions.Sentinel` | ||
|
|
||
| Sentinels constructed with `typing_extensions.Sentinel` can be used directly in type expressions: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel, assert_type | ||
|
|
||
| MISSING = Sentinel("MISSING") | ||
| OTHER = Sentinel("OTHER") | ||
| WITH_REPR = Sentinel("WITH_REPR", "<with repr>") | ||
| WITH_REPR_KEYWORD = Sentinel("WITH_REPR_KEYWORD", repr="<with repr keyword>") | ||
|
|
||
| reveal_type(MISSING) # revealed: MISSING | ||
| reveal_type(OTHER) # revealed: OTHER | ||
| reveal_type(WITH_REPR) # revealed: WITH_REPR | ||
| reveal_type(WITH_REPR_KEYWORD) # revealed: WITH_REPR_KEYWORD | ||
|
|
||
| def accepts_missing(x: MISSING) -> None: ... | ||
| def accepts_other(x: OTHER) -> None: ... | ||
|
|
||
| accepts_missing(MISSING) | ||
| accepts_missing(OTHER) # error: [invalid-argument-type] | ||
| accepts_other(OTHER) | ||
| accepts_other(MISSING) # error: [invalid-argument-type] | ||
|
|
||
| def bad_default(x: int = MISSING) -> None: # error: [invalid-parameter-default] | ||
| pass | ||
|
|
||
| def good_default(x: int | MISSING | OTHER = MISSING) -> None: | ||
| if x is MISSING: | ||
| assert_type(x, MISSING) | ||
| reveal_type(x) # revealed: MISSING | ||
| else: | ||
| assert_type(x, int | OTHER) | ||
| reveal_type(x) # revealed: int | OTHER | ||
|
|
||
| good_default(1) | ||
| good_default(MISSING) | ||
| good_default(OTHER) | ||
|
|
||
| def reverse_check(x: int | MISSING | OTHER) -> None: | ||
| if MISSING is x: | ||
| assert_type(x, MISSING) | ||
| reveal_type(x) # revealed: MISSING | ||
| else: | ||
| assert_type(x, int | OTHER) | ||
| reveal_type(x) # revealed: int | OTHER | ||
|
|
||
| def negative_check(x: int | MISSING | OTHER) -> None: | ||
| if x is not MISSING: | ||
| assert_type(x, int | OTHER) | ||
| reveal_type(x) # revealed: int | OTHER | ||
| else: | ||
| assert_type(x, MISSING) | ||
| reveal_type(x) # revealed: MISSING | ||
|
|
||
| def reverse_negative_check(x: int | MISSING | OTHER) -> None: | ||
| if MISSING is not x: | ||
| assert_type(x, int | OTHER) | ||
| reveal_type(x) # revealed: int | OTHER | ||
| else: | ||
| assert_type(x, MISSING) | ||
| reveal_type(x) # revealed: MISSING | ||
| ``` | ||
|
|
||
| Sentinel objects are always truthy, expose the standard sentinel metadata attributes, and are | ||
| rejected as class bases: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel | ||
|
|
||
| MISSING = Sentinel("MISSING") | ||
|
|
||
| reveal_type(bool(MISSING)) # revealed: Literal[True] | ||
| reveal_type(MISSING.__module__) # revealed: str | ||
|
|
||
| class MissingSubclass(MISSING): # error: [invalid-base] | ||
| pass | ||
| ``` | ||
|
|
||
| Sentinels declared in class scope can also be used in type expressions: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel, assert_type | ||
|
|
||
| class C: | ||
| MARKER = Sentinel("C.MARKER") | ||
|
|
||
| def accepts_marker(x: C.MARKER) -> None: ... | ||
|
|
||
| accepts_marker(C.MARKER) | ||
|
|
||
| def class_default(x: int | C.MARKER = C.MARKER) -> None: | ||
| if x is C.MARKER: | ||
| assert_type(x, C.MARKER) | ||
| reveal_type(x) # revealed: MARKER | ||
| else: | ||
| assert_type(x, int) | ||
| reveal_type(x) # revealed: int | ||
|
|
||
| def class_reverse_negative(x: int | C.MARKER) -> None: | ||
| if C.MARKER is not x: | ||
| assert_type(x, int) | ||
| reveal_type(x) # revealed: int | ||
| else: | ||
| assert_type(x, C.MARKER) | ||
| reveal_type(x) # revealed: MARKER | ||
| ``` | ||
|
|
||
| Sentinel declarations are recognized only in module and class scope: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel | ||
|
|
||
| def outer(): | ||
| LOCAL = Sentinel("LOCAL") | ||
|
|
||
| def inner(x: LOCAL) -> None: ... # error: [invalid-type-form] | ||
| ``` | ||
|
|
||
| Sentinels are not generic: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel | ||
|
|
||
| MISSING = Sentinel("MISSING") | ||
|
|
||
| def f(x: MISSING[int]) -> None: ... # error: [invalid-type-form] | ||
| ``` | ||
|
|
||
| Invalid sentinel constructor calls fall back to the normal call path: | ||
|
|
||
| ```py | ||
| from typing_extensions import Sentinel | ||
|
|
||
| NAME = "NAME" | ||
|
|
||
| NON_LITERAL_NAME = Sentinel(NAME) | ||
| UNKNOWN_NAME = Sentinel(UNKNOWN) # error: [unresolved-reference] | ||
| NON_LITERAL_REPR = Sentinel("NON_LITERAL_REPR", repr=NAME) | ||
| UNKNOWN_REPR = Sentinel("UNKNOWN_REPR", repr=UNKNOWN) # error: [unresolved-reference] | ||
| UNKNOWN_KEYWORD = Sentinel("UNKNOWN_KEYWORD", unknown=NAME) # error: [unknown-argument] | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,6 +117,7 @@ pub enum KnownClass { | |
| Mapping, | ||
| // typing_extensions | ||
| ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features | ||
| Sentinel, | ||
|
Contributor
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. It looks like
Member
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. I think we won't recognise the builtin symbol as existing until we upgrade our vendored version of typeshed in a couple of days
Contributor
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. Thanks. I'm merging this for now, but it would be great to add a few more tests once that lands. |
||
| // Collections | ||
| ChainMap, | ||
| Counter, | ||
|
|
@@ -180,6 +181,7 @@ impl KnownClass { | |
| | Self::ParamSpecArgs | ||
| | Self::ParamSpecKwargs | ||
| | Self::TypeVarTuple | ||
| | Self::Sentinel | ||
| | Self::Super | ||
| | Self::WrapperDescriptorType | ||
| | Self::UnionType | ||
|
|
@@ -325,6 +327,7 @@ impl KnownClass { | |
| | KnownClass::ParamSpecArgs | ||
| | KnownClass::ParamSpecKwargs | ||
| | KnownClass::TypeVarTuple | ||
| | KnownClass::Sentinel | ||
| | KnownClass::TypeAliasType | ||
| | KnownClass::NoDefaultType | ||
| | KnownClass::NewType | ||
|
|
@@ -419,6 +422,7 @@ impl KnownClass { | |
| | KnownClass::ParamSpecArgs | ||
| | KnownClass::ParamSpecKwargs | ||
| | KnownClass::TypeVarTuple | ||
| | KnownClass::Sentinel | ||
| | KnownClass::TypeAliasType | ||
| | KnownClass::NoDefaultType | ||
| | KnownClass::NewType | ||
|
|
@@ -513,6 +517,7 @@ impl KnownClass { | |
| | KnownClass::ParamSpecArgs | ||
| | KnownClass::ParamSpecKwargs | ||
| | KnownClass::TypeVarTuple | ||
| | KnownClass::Sentinel | ||
| | KnownClass::TypeAliasType | ||
| | KnownClass::NoDefaultType | ||
| | KnownClass::NewType | ||
|
|
@@ -610,6 +615,7 @@ impl KnownClass { | |
| | Self::ParamSpecArgs | ||
| | Self::ParamSpecKwargs | ||
| | Self::TypeVarTuple | ||
| | Self::Sentinel | ||
| | Self::TypeAliasType | ||
| | Self::NoDefaultType | ||
| | Self::NewType | ||
|
|
@@ -720,6 +726,7 @@ impl KnownClass { | |
| | KnownClass::ParamSpecKwargs | ||
| | KnownClass::ProtocolMeta | ||
| | KnownClass::TypeVarTuple | ||
| | KnownClass::Sentinel | ||
| | KnownClass::TypeAliasType | ||
| | KnownClass::NoDefaultType | ||
| | KnownClass::NewType | ||
|
|
@@ -797,6 +804,7 @@ impl KnownClass { | |
| Self::ParamSpecArgs => "ParamSpecArgs", | ||
| Self::ParamSpecKwargs => "ParamSpecKwargs", | ||
| Self::TypeVarTuple => "TypeVarTuple", | ||
| Self::Sentinel => "Sentinel", | ||
| Self::TypeAliasType => "TypeAliasType", | ||
| Self::NoDefaultType => "_NoDefaultType", | ||
| Self::NewType => "NewType", | ||
|
|
@@ -1193,6 +1201,7 @@ impl KnownClass { | |
| Self::TypeAliasType | ||
| | Self::ExtensionsTypeVar | ||
| | Self::TypeVarTuple | ||
| | Self::Sentinel | ||
| | Self::ExtensionsParamSpec | ||
| | Self::ParamSpecArgs | ||
| | Self::ParamSpecKwargs | ||
|
|
@@ -1315,6 +1324,7 @@ impl KnownClass { | |
| | Self::ParamSpecArgs | ||
| | Self::ParamSpecKwargs | ||
| | Self::TypeVarTuple | ||
| | Self::Sentinel | ||
| | Self::Enum | ||
| | Self::EnumType | ||
| | Self::Auto | ||
|
|
@@ -1413,6 +1423,7 @@ impl KnownClass { | |
| | Self::ParamSpecArgs | ||
| | Self::ParamSpecKwargs | ||
| | Self::TypeVarTuple | ||
| | Self::Sentinel | ||
| | Self::Enum | ||
| | Self::EnumType | ||
| | Self::Auto | ||
|
|
@@ -1506,6 +1517,7 @@ impl KnownClass { | |
| "ParamSpecArgs" => &[Self::ParamSpecArgs], | ||
| "ParamSpecKwargs" => &[Self::ParamSpecKwargs], | ||
| "TypeVarTuple" => &[Self::TypeVarTuple], | ||
| "Sentinel" => &[Self::Sentinel], | ||
| "ChainMap" => &[Self::ChainMap], | ||
| "Counter" => &[Self::Counter], | ||
| "defaultdict" => &[Self::DefaultDict], | ||
|
|
@@ -1632,6 +1644,7 @@ impl KnownClass { | |
| | Self::ExtensionsTypeVar | ||
| | Self::ParamSpec | ||
| | Self::ExtensionsParamSpec | ||
| | Self::Sentinel | ||
| | Self::NamedTupleLike | ||
| | Self::ConstraintSet | ||
| | Self::GenericContext | ||
|
|
||
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.
A few other things that we could test (from reading your PEP, please feel free to leave some of these out if you think they don't make sense):
bool(MISSING)is of typeLiteral[True]..__name__and.__module__on a sentinel does not lead to an error, and that the type of those attributes is reasonable / "not obviously wrong".sentinelcan not be subclassed.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.
Thanks, adding some tests.
We currently have only typing_extensions.Sentinel, which we didn't make
@final. builtins.sentinel will be, though testing that specifically in ty doesn't feel that useful, as it should just listen to the@finalin the stub.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.
Oh and
__name__does error right now because the attribute isn't there in typeshed fortyping_extensions.Sentinel. We'll add it soon. This also seems not critical to test since the behavior is specified by the stub.