Skip to content

Commit 679d7b2

Browse files
[ty] Add support for sentinels (PEP 661) (#25082)
## Summary Add support for `typing_extensions.Sentinel` (PEP 661). Support for the other variants (`builtins.sentinel`, `typing_extensions.sentinel`) can be added later as stubs are updated. See PEP 661 and python/typing#2277 ## Test Plan New tests added.
1 parent a988c5f commit 679d7b2

9 files changed

Lines changed: 327 additions & 7 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Sentinels
2+
3+
## `typing_extensions.Sentinel`
4+
5+
Sentinels constructed with `typing_extensions.Sentinel` can be used directly in type expressions:
6+
7+
```py
8+
from typing_extensions import Sentinel, assert_type
9+
10+
MISSING = Sentinel("MISSING")
11+
OTHER = Sentinel("OTHER")
12+
WITH_REPR = Sentinel("WITH_REPR", "<with repr>")
13+
WITH_REPR_KEYWORD = Sentinel("WITH_REPR_KEYWORD", repr="<with repr keyword>")
14+
15+
reveal_type(MISSING) # revealed: MISSING
16+
reveal_type(OTHER) # revealed: OTHER
17+
reveal_type(WITH_REPR) # revealed: WITH_REPR
18+
reveal_type(WITH_REPR_KEYWORD) # revealed: WITH_REPR_KEYWORD
19+
20+
def accepts_missing(x: MISSING) -> None: ...
21+
def accepts_other(x: OTHER) -> None: ...
22+
23+
accepts_missing(MISSING)
24+
accepts_missing(OTHER) # error: [invalid-argument-type]
25+
accepts_other(OTHER)
26+
accepts_other(MISSING) # error: [invalid-argument-type]
27+
28+
def bad_default(x: int = MISSING) -> None: # error: [invalid-parameter-default]
29+
pass
30+
31+
def good_default(x: int | MISSING | OTHER = MISSING) -> None:
32+
if x is MISSING:
33+
assert_type(x, MISSING)
34+
reveal_type(x) # revealed: MISSING
35+
else:
36+
assert_type(x, int | OTHER)
37+
reveal_type(x) # revealed: int | OTHER
38+
39+
good_default(1)
40+
good_default(MISSING)
41+
good_default(OTHER)
42+
43+
def reverse_check(x: int | MISSING | OTHER) -> None:
44+
if MISSING is x:
45+
assert_type(x, MISSING)
46+
reveal_type(x) # revealed: MISSING
47+
else:
48+
assert_type(x, int | OTHER)
49+
reveal_type(x) # revealed: int | OTHER
50+
51+
def negative_check(x: int | MISSING | OTHER) -> None:
52+
if x is not MISSING:
53+
assert_type(x, int | OTHER)
54+
reveal_type(x) # revealed: int | OTHER
55+
else:
56+
assert_type(x, MISSING)
57+
reveal_type(x) # revealed: MISSING
58+
59+
def reverse_negative_check(x: int | MISSING | OTHER) -> None:
60+
if MISSING is not x:
61+
assert_type(x, int | OTHER)
62+
reveal_type(x) # revealed: int | OTHER
63+
else:
64+
assert_type(x, MISSING)
65+
reveal_type(x) # revealed: MISSING
66+
```
67+
68+
Sentinel objects are always truthy, expose the standard sentinel metadata attributes, and are
69+
rejected as class bases:
70+
71+
```py
72+
from typing_extensions import Sentinel
73+
74+
MISSING = Sentinel("MISSING")
75+
76+
reveal_type(bool(MISSING)) # revealed: Literal[True]
77+
reveal_type(MISSING.__module__) # revealed: str
78+
79+
class MissingSubclass(MISSING): # error: [invalid-base]
80+
pass
81+
```
82+
83+
Sentinels declared in class scope can also be used in type expressions:
84+
85+
```py
86+
from typing_extensions import Sentinel, assert_type
87+
88+
class C:
89+
MARKER = Sentinel("C.MARKER")
90+
91+
def accepts_marker(x: C.MARKER) -> None: ...
92+
93+
accepts_marker(C.MARKER)
94+
95+
def class_default(x: int | C.MARKER = C.MARKER) -> None:
96+
if x is C.MARKER:
97+
assert_type(x, C.MARKER)
98+
reveal_type(x) # revealed: MARKER
99+
else:
100+
assert_type(x, int)
101+
reveal_type(x) # revealed: int
102+
103+
def class_reverse_negative(x: int | C.MARKER) -> None:
104+
if C.MARKER is not x:
105+
assert_type(x, int)
106+
reveal_type(x) # revealed: int
107+
else:
108+
assert_type(x, C.MARKER)
109+
reveal_type(x) # revealed: MARKER
110+
```
111+
112+
Sentinel declarations are recognized only in module and class scope:
113+
114+
```py
115+
from typing_extensions import Sentinel
116+
117+
def outer():
118+
LOCAL = Sentinel("LOCAL")
119+
120+
def inner(x: LOCAL) -> None: ... # error: [invalid-type-form]
121+
```
122+
123+
Sentinels are not generic:
124+
125+
```py
126+
from typing_extensions import Sentinel
127+
128+
MISSING = Sentinel("MISSING")
129+
130+
def f(x: MISSING[int]) -> None: ... # error: [invalid-type-form]
131+
```
132+
133+
Invalid sentinel constructor calls fall back to the normal call path:
134+
135+
```py
136+
from typing_extensions import Sentinel
137+
138+
NAME = "NAME"
139+
140+
NON_LITERAL_NAME = Sentinel(NAME)
141+
UNKNOWN_NAME = Sentinel(UNKNOWN) # error: [unresolved-reference]
142+
NON_LITERAL_REPR = Sentinel("NON_LITERAL_REPR", repr=NAME)
143+
UNKNOWN_REPR = Sentinel("UNKNOWN_REPR", repr=UNKNOWN) # error: [unresolved-reference]
144+
UNKNOWN_KEYWORD = Sentinel("UNKNOWN_KEYWORD", unknown=NAME) # error: [unknown-argument]
145+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ use crate::types::generics::{
6868
ApplySpecialization, InferableTypeVars, Specialization, bind_typevar,
6969
};
7070
use crate::types::infer::InferenceFlags;
71-
use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance};
71+
use crate::types::known_instance::{
72+
InternedConstraintSet, InternedType, SentinelInstance, UnionTypeInstance,
73+
};
7274
pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind};
7375
use crate::types::mro::{MroIterator, StaticMroError};
7476
pub(crate) use crate::types::narrow::{NarrowingConstraint, infer_narrowing_constraint};
@@ -2237,6 +2239,7 @@ impl<'db> Type<'db> {
22372239
!(special_form.check_module(KnownModule::Typing)
22382240
&& special_form.check_module(KnownModule::TypingExtensions))
22392241
}
2242+
Type::KnownInstance(KnownInstanceType::Sentinel(_)) => true,
22402243
Type::KnownInstance(_) => false,
22412244
Type::Callable(_) => {
22422245
// A callable type is never a singleton because for any given signature,
@@ -5402,6 +5405,9 @@ impl<'db> Type<'db> {
54025405
}
54035406
KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)),
54045407
KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)),
5408+
KnownInstanceType::Sentinel(sentinel) => {
5409+
Ok(Type::KnownInstance(KnownInstanceType::Sentinel(*sentinel)))
5410+
}
54055411
KnownInstanceType::FunctoolsPartial(_) => Err(InvalidTypeExpressionError {
54065412
invalid_expressions: smallvec_inline![InvalidTypeExpression::InvalidType(
54075413
*self, scope_id
@@ -6139,6 +6145,7 @@ impl<'db> Type<'db> {
61396145
| KnownInstanceType::LiteralStringAlias(_)
61406146
| KnownInstanceType::NamedTupleSpec(_)
61416147
| KnownInstanceType::NewType(_)
6148+
| KnownInstanceType::Sentinel(_)
61426149
| KnownInstanceType::FunctoolsPartial(_) => {
61436150
// TODO: For some of these, we may need to try to find legacy typevars in inner types.
61446151
}

crates/ty_python_semantic/src/types/class/known.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub enum KnownClass {
117117
Mapping,
118118
// typing_extensions
119119
ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features
120+
Sentinel,
120121
// Collections
121122
ChainMap,
122123
Counter,
@@ -180,6 +181,7 @@ impl KnownClass {
180181
| Self::ParamSpecArgs
181182
| Self::ParamSpecKwargs
182183
| Self::TypeVarTuple
184+
| Self::Sentinel
183185
| Self::Super
184186
| Self::WrapperDescriptorType
185187
| Self::UnionType
@@ -325,6 +327,7 @@ impl KnownClass {
325327
| KnownClass::ParamSpecArgs
326328
| KnownClass::ParamSpecKwargs
327329
| KnownClass::TypeVarTuple
330+
| KnownClass::Sentinel
328331
| KnownClass::TypeAliasType
329332
| KnownClass::NoDefaultType
330333
| KnownClass::NewType
@@ -419,6 +422,7 @@ impl KnownClass {
419422
| KnownClass::ParamSpecArgs
420423
| KnownClass::ParamSpecKwargs
421424
| KnownClass::TypeVarTuple
425+
| KnownClass::Sentinel
422426
| KnownClass::TypeAliasType
423427
| KnownClass::NoDefaultType
424428
| KnownClass::NewType
@@ -513,6 +517,7 @@ impl KnownClass {
513517
| KnownClass::ParamSpecArgs
514518
| KnownClass::ParamSpecKwargs
515519
| KnownClass::TypeVarTuple
520+
| KnownClass::Sentinel
516521
| KnownClass::TypeAliasType
517522
| KnownClass::NoDefaultType
518523
| KnownClass::NewType
@@ -610,6 +615,7 @@ impl KnownClass {
610615
| Self::ParamSpecArgs
611616
| Self::ParamSpecKwargs
612617
| Self::TypeVarTuple
618+
| Self::Sentinel
613619
| Self::TypeAliasType
614620
| Self::NoDefaultType
615621
| Self::NewType
@@ -720,6 +726,7 @@ impl KnownClass {
720726
| KnownClass::ParamSpecKwargs
721727
| KnownClass::ProtocolMeta
722728
| KnownClass::TypeVarTuple
729+
| KnownClass::Sentinel
723730
| KnownClass::TypeAliasType
724731
| KnownClass::NoDefaultType
725732
| KnownClass::NewType
@@ -797,6 +804,7 @@ impl KnownClass {
797804
Self::ParamSpecArgs => "ParamSpecArgs",
798805
Self::ParamSpecKwargs => "ParamSpecKwargs",
799806
Self::TypeVarTuple => "TypeVarTuple",
807+
Self::Sentinel => "Sentinel",
800808
Self::TypeAliasType => "TypeAliasType",
801809
Self::NoDefaultType => "_NoDefaultType",
802810
Self::NewType => "NewType",
@@ -1193,6 +1201,7 @@ impl KnownClass {
11931201
Self::TypeAliasType
11941202
| Self::ExtensionsTypeVar
11951203
| Self::TypeVarTuple
1204+
| Self::Sentinel
11961205
| Self::ExtensionsParamSpec
11971206
| Self::ParamSpecArgs
11981207
| Self::ParamSpecKwargs
@@ -1314,6 +1323,7 @@ impl KnownClass {
13141323
| Self::ParamSpecArgs
13151324
| Self::ParamSpecKwargs
13161325
| Self::TypeVarTuple
1326+
| Self::Sentinel
13171327
| Self::Enum
13181328
| Self::EnumType
13191329
| Self::Auto
@@ -1413,6 +1423,7 @@ impl KnownClass {
14131423
| Self::ParamSpecArgs
14141424
| Self::ParamSpecKwargs
14151425
| Self::TypeVarTuple
1426+
| Self::Sentinel
14161427
| Self::Enum
14171428
| Self::EnumType
14181429
| Self::Auto
@@ -1506,6 +1517,7 @@ impl KnownClass {
15061517
"ParamSpecArgs" => &[Self::ParamSpecArgs],
15071518
"ParamSpecKwargs" => &[Self::ParamSpecKwargs],
15081519
"TypeVarTuple" => &[Self::TypeVarTuple],
1520+
"Sentinel" => &[Self::Sentinel],
15091521
"ChainMap" => &[Self::ChainMap],
15101522
"Counter" => &[Self::Counter],
15111523
"defaultdict" => &[Self::DefaultDict],
@@ -1632,6 +1644,7 @@ impl KnownClass {
16321644
| Self::ExtensionsTypeVar
16331645
| Self::ParamSpec
16341646
| Self::ExtensionsParamSpec
1647+
| Self::Sentinel
16351648
| Self::NamedTupleLike
16361649
| Self::ConstraintSet
16371650
| Self::GenericContext

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ impl<'db> ClassBase<'db> {
191191
| KnownInstanceType::Literal(_)
192192
| KnownInstanceType::LiteralStringAlias(_)
193193
| KnownInstanceType::NamedTupleSpec(_)
194+
| KnownInstanceType::Sentinel(_)
194195
// A class inheriting from a newtype would make intuitive sense, but newtype
195196
// wrappers are just identity callables at runtime, so this sort of inheritance
196197
// doesn't work and isn't allowed.

crates/ty_python_semantic/src/types/display.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,9 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
31093109
f.with_type(ty).write_str(declaration.name(self.db))?;
31103110
f.write_str("'>")
31113111
}
3112+
KnownInstanceType::Sentinel(sentinel) => {
3113+
f.with_type(ty).write_str(sentinel.name(self.db).as_str())
3114+
}
31123115
KnownInstanceType::NamedTupleSpec(_) => f.write_str("NamedTupleSpec"),
31133116
KnownInstanceType::FunctoolsPartial(partial) => {
31143117
f.write_str("partial[")?;

0 commit comments

Comments
 (0)