Skip to content

Commit fe93bd8

Browse files
authored
Merge branch 'python:master' into master
2 parents bbdb27f + ed90eaf commit fe93bd8

35 files changed

Lines changed: 921 additions & 78 deletions

mypy-requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ typing_extensions>=4.14.0; python_version>='3.15'
55
mypy_extensions>=1.0.0
66
pathspec>=1.0.0
77
tomli>=1.1.0; python_version<'3.11'
8-
librt>=0.13.0; platform_python_implementation != 'PyPy'
9-
ast-serialize>=0.6.0,<1.0.0
8+
librt>=0.15.0; platform_python_implementation != 'PyPy'
9+
ast-serialize>=0.8.0,<1.0.0

mypy/cache.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@
4848
from __future__ import annotations
4949

5050
from collections.abc import Sequence
51-
from typing import Any, Final, TypeAlias as _TypeAlias
51+
from typing import TYPE_CHECKING, Any, Final, TypeAlias as _TypeAlias
52+
53+
if TYPE_CHECKING:
54+
from mypy.types import SentinelValue
5255

5356
from librt.internal import (
5457
ReadBuffer as ReadBuffer,
@@ -69,7 +72,7 @@
6972
from mypy_extensions import u8
7073

7174
# High-level cache layout format
72-
CACHE_VERSION: Final = 10
75+
CACHE_VERSION: Final = 11
7376

7477
# Type used internally to represent errors:
7578
# (path, line, column, end_line, end_column, severity, message, code)
@@ -308,6 +311,7 @@ def read(cls, data: ReadBuffer) -> CacheMetaEx | None:
308311
LITERAL_BYTES: Final[Tag] = 5
309312
LITERAL_FLOAT: Final[Tag] = 6
310313
LITERAL_COMPLEX: Final[Tag] = 7
314+
LITERAL_SENTINEL: Final[Tag] = 8
311315

312316
# Collections.
313317
LIST_GEN: Final[Tag] = 20
@@ -328,7 +332,7 @@ def read(cls, data: ReadBuffer) -> CacheMetaEx | None:
328332
END_TAG: Final[Tag] = 255
329333

330334

331-
def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float:
335+
def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float | SentinelValue:
332336
if tag == LITERAL_INT:
333337
return read_int_bare(data)
334338
elif tag == LITERAL_STR:
@@ -339,12 +343,18 @@ def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float:
339343
return True
340344
elif tag == LITERAL_FLOAT:
341345
return read_float_bare(data)
346+
elif tag == LITERAL_SENTINEL:
347+
from mypy.types import SentinelValue as _SentinelValue
348+
349+
return _SentinelValue(read_str_bare(data), read_str_bare(data))
342350
assert False, f"Unknown literal tag {tag}"
343351

344352

345353
# There is an intentional asymmetry between read and write for literals because
346354
# None and/or complex values are only allowed in some contexts but not in others.
347-
def write_literal(data: WriteBuffer, value: int | str | bool | float | complex | None) -> None:
355+
def write_literal(
356+
data: WriteBuffer, value: int | str | bool | float | complex | SentinelValue | None
357+
) -> None:
348358
if isinstance(value, bool):
349359
write_bool(data, value)
350360
elif isinstance(value, int):
@@ -360,8 +370,12 @@ def write_literal(data: WriteBuffer, value: int | str | bool | float | complex |
360370
write_tag(data, LITERAL_COMPLEX)
361371
write_float_bare(data, value.real)
362372
write_float_bare(data, value.imag)
363-
else:
373+
elif value is None:
364374
write_tag(data, LITERAL_NONE)
375+
else:
376+
write_tag(data, LITERAL_SENTINEL)
377+
write_str_bare(data, value.fullname)
378+
write_str_bare(data, value.name)
365379

366380

367381
def read_int(data: ReadBuffer) -> int:

mypy/checker.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4409,8 +4409,10 @@ def check_multi_assignment_from_tuple(
44094409
# inferred return type for an overloaded function
44104410
# to be ambiguous.
44114411
return
4412-
assert isinstance(reinferred_rvalue_type, TupleType)
4413-
rvalue_type = reinferred_rvalue_type
4412+
if isinstance(reinferred_rvalue_type, TupleType):
4413+
# This branch will usually be taken, but in some cases context can
4414+
# e.g. select a different overload
4415+
rvalue_type = reinferred_rvalue_type
44144416

44154417
left_rv_types, star_rv_types, right_rv_types = self.split_around_star(
44164418
rvalue_type.items, star_index, len(lvalues)

mypy/messages.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2783,13 +2783,18 @@ def format_literal_value(typ: LiteralType) -> str:
27832783
modifier += "="
27842784
items.append(f"{item_name!r}{modifier}: {format(item_type)}")
27852785
return f"TypedDict({{{', '.join(items)}}})"
2786+
elif isinstance(typ, LiteralType) and typ.is_sentinel_literal():
2787+
return format_literal_value(typ)
27862788
elif isinstance(typ, LiteralType):
27872789
return f"Literal[{format_literal_value(typ)}]"
27882790
elif isinstance(typ, UnionType):
27892791
typ = get_proper_type(ignore_last_known_values(typ))
27902792
if not isinstance(typ, UnionType):
27912793
return format(typ)
27922794
literal_items, union_items = separate_union_literals(typ)
2795+
sentinel_items = [item for item in literal_items if item.is_sentinel_literal()]
2796+
literal_items = [item for item in literal_items if not item.is_sentinel_literal()]
2797+
union_items = [*sentinel_items, *union_items]
27932798

27942799
# Coalesce multiple Literal[] members. This also changes output order.
27952800
# If there's just one Literal item, retain the original ordering.

mypy/nodes.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,7 @@ def is_dynamic(self) -> bool:
14161416
"from_module_getattr",
14171417
"has_explicit_value",
14181418
"allow_incompatible_override",
1419+
"is_sentinel",
14191420
]
14201421

14211422

@@ -1454,6 +1455,7 @@ class Var(SymbolNode):
14541455
"allow_incompatible_override",
14551456
"invalid_partial_type",
14561457
"is_argument",
1458+
"is_sentinel",
14571459
)
14581460

14591461
__match_args__ = ("name", "type", "final_value")
@@ -1516,6 +1518,8 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None:
15161518
self.invalid_partial_type = False
15171519
# Is it a variable symbol for a function argument?
15181520
self.is_argument = False
1521+
# Was this variable created by PEP 661 sentinel()/Sentinel() syntax?
1522+
self.is_sentinel = False
15191523

15201524
@property
15211525
def name(self) -> str:
@@ -1598,6 +1602,7 @@ def write(self, data: WriteBuffer) -> None:
15981602
self.from_module_getattr,
15991603
self.has_explicit_value,
16001604
self.allow_incompatible_override,
1605+
self.is_sentinel,
16011606
],
16021607
)
16031608
write_literal(data, self.final_value)
@@ -1635,12 +1640,15 @@ def read(cls, data: ReadBuffer) -> Var:
16351640
v.from_module_getattr,
16361641
v.has_explicit_value,
16371642
v.allow_incompatible_override,
1638-
) = read_flags(data, num_flags=19)
1643+
v.is_sentinel,
1644+
) = read_flags(data, num_flags=20)
16391645
tag = read_tag(data)
16401646
if tag == LITERAL_COMPLEX:
16411647
v.final_value = complex(read_float_bare(data), read_float_bare(data))
16421648
elif tag != LITERAL_NONE:
1643-
v.final_value = read_literal(data, tag)
1649+
val = read_literal(data, tag)
1650+
assert not isinstance(val, mypy.types.SentinelValue)
1651+
v.final_value = val
16441652
assert read_tag(data) == END_TAG
16451653
return v
16461654

mypy/plugins/dataclasses.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
LiteralType,
6464
NoneType,
6565
ProperType,
66+
SentinelValue,
6667
TupleType,
6768
Type,
6869
TypeOfAny,
@@ -799,6 +800,9 @@ def _is_kw_only_type(self, node: Type | None) -> bool:
799800
if node is None:
800801
return False
801802
node_type = get_proper_type(node)
803+
if isinstance(node_type, LiteralType) and isinstance(node_type.value, SentinelValue):
804+
# PEP 661 sentinel: `KW_ONLY = sentinel("KW_ONLY")` (Python 3.15+).
805+
return node_type.value.fullname == "dataclasses.KW_ONLY"
802806
if not isinstance(node_type, Instance):
803807
return False
804808
return node_type.type.fullname == "dataclasses.KW_ONLY"

mypy/semanal.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271
OVERRIDE_DECORATOR_NAMES,
272272
PROTOCOL_NAMES,
273273
REVEAL_TYPE_NAMES,
274+
SENTINEL_TYPE_NAMES,
274275
TPDICT_NAMES,
275276
TYPE_ALIAS_NAMES,
276277
TYPE_CHECK_ONLY_NAMES,
@@ -289,6 +290,7 @@
289290
ParamSpecType,
290291
PlaceholderType,
291292
ProperType,
293+
SentinelValue,
292294
TrivialSyntheticTypeTranslator,
293295
TupleType,
294296
Type,
@@ -3377,9 +3379,15 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
33773379
# may be set to True while there were still placeholders due to forward refs.
33783380
s.is_alias_def = False
33793381

3382+
sentinel_definition = self.is_sentinel_declaration(s)
3383+
33803384
# OK, this is a regular assignment, perform the necessary analysis steps.
33813385
s.is_final_def = self.unwrap_final(s)
3386+
if sentinel_definition:
3387+
s.is_final_def = True
33823388
self.analyze_lvalues(s)
3389+
if sentinel_definition:
3390+
self.setup_sentinel_var(s)
33833391
self.check_final_implicit_def(s)
33843392
self.store_final_status(s)
33853393
self.check_classvar(s)
@@ -3392,6 +3400,51 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
33923400
self.process__deletable__(s)
33933401
self.process__slots__(s)
33943402

3403+
def is_sentinel_declaration(self, s: AssignmentStmt) -> bool:
3404+
"""Does this assignment define a PEP 661 sentinel singleton?"""
3405+
if self.is_nested_within_func_scope() or s.unanalyzed_type is not None:
3406+
return False
3407+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
3408+
return False
3409+
if not isinstance(s.rvalue, CallExpr):
3410+
return False
3411+
call = s.rvalue
3412+
if not isinstance(call.callee, RefExpr):
3413+
return False
3414+
if call.callee.fullname not in SENTINEL_TYPE_NAMES:
3415+
return False
3416+
if not call.args or call.arg_kinds[0] != ARG_POS or not isinstance(call.args[0], StrExpr):
3417+
return False
3418+
return True
3419+
3420+
def setup_sentinel_var(self, s: AssignmentStmt) -> None:
3421+
lvalue = s.lvalues[0]
3422+
assert isinstance(lvalue, NameExpr)
3423+
if not isinstance(lvalue.node, Var):
3424+
return
3425+
var = lvalue.node
3426+
var.is_sentinel = True
3427+
typ = self.sentinel_type_for_var(var, s.rvalue)
3428+
if typ is not None:
3429+
s.type = typ
3430+
3431+
def sentinel_type_for_var(self, var: Var, rvalue: Expression) -> Instance | None:
3432+
assert isinstance(rvalue, CallExpr)
3433+
callee = rvalue.callee
3434+
assert isinstance(callee, RefExpr)
3435+
typ = self.named_type_or_none(callee.fullname)
3436+
if typ is None:
3437+
return None
3438+
name = f"{self.type.name}.{var.name}" if self.type is not None else var.name
3439+
return typ.copy_modified(
3440+
last_known_value=LiteralType(
3441+
SentinelValue(var.fullname, name),
3442+
fallback=typ,
3443+
line=rvalue.line,
3444+
column=rvalue.column,
3445+
)
3446+
)
3447+
33953448
def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool:
33963449
"""Special case 'X = X' in global scope.
33973450
@@ -4763,6 +4816,7 @@ def store_declared_types(self, lvalue: Lvalue, typ: Type) -> None:
47634816
var.is_final
47644817
and isinstance(typ, Instance)
47654818
and typ.last_known_value
4819+
and not isinstance(typ.last_known_value.value, SentinelValue)
47664820
and (not self.type or not self.type.is_enum)
47674821
):
47684822
var.final_value = typ.last_known_value.value

mypy/server/deps.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a
158158
ParamSpecType,
159159
PartialType,
160160
ProperType,
161+
SentinelValue,
161162
TupleType,
162163
Type,
163164
TypeAliasType,
@@ -1096,7 +1097,10 @@ def visit_typeddict_type(self, typ: TypedDictType) -> list[str]:
10961097
return triggers
10971098

10981099
def visit_literal_type(self, typ: LiteralType) -> list[str]:
1099-
return self.get_type_triggers(typ.fallback)
1100+
triggers = self.get_type_triggers(typ.fallback)
1101+
if isinstance(typ.value, SentinelValue):
1102+
triggers.append(make_trigger(typ.value.fullname))
1103+
return triggers
11001104

11011105
def visit_unbound_type(self, typ: UnboundType) -> list[str]:
11021106
return []

mypy/test/testtypes.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import re
66
from unittest import TestCase, skipUnless
77

8+
from librt.internal import ReadBuffer, WriteBuffer
9+
10+
from mypy.cache import read_tag
811
from mypy.erasetype import erase_type, remove_instance_last_known_values
912
from mypy.indirection import TypeIndirectionVisitor
1013
from mypy.join import join_types
@@ -30,13 +33,15 @@
3033
from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture
3134
from mypy.typeops import false_only, make_simplified_union, true_only
3235
from mypy.types import (
36+
LITERAL_TYPE,
3337
AnyType,
3438
CallableType,
3539
Instance,
3640
LiteralType,
3741
NoneType,
3842
Overloaded,
3943
ProperType,
44+
SentinelValue,
4045
TupleType,
4146
Type,
4247
TypedDictType,
@@ -66,6 +71,25 @@ def setUp(self) -> None:
6671
def test_any(self) -> None:
6772
assert_equal(str(AnyType(TypeOfAny.special_form)), "Any")
6873

74+
def test_sentinel_literal_json_roundtrip(self) -> None:
75+
literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a)
76+
assert_equal(str(literal), "MISSING")
77+
data = literal.serialize()
78+
assert isinstance(data, dict)
79+
roundtrip = LiteralType.deserialize(data)
80+
self.assertEqual(roundtrip.value, literal.value)
81+
self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname)
82+
83+
def test_sentinel_literal_ff_roundtrip(self) -> None:
84+
literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a)
85+
data = WriteBuffer()
86+
literal.write(data)
87+
buffer = ReadBuffer(data.getvalue())
88+
assert read_tag(buffer) == LITERAL_TYPE
89+
roundtrip = LiteralType.read(buffer)
90+
self.assertEqual(roundtrip.value, literal.value)
91+
self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname)
92+
6993
def test_simple_unbound_type(self) -> None:
7094
u = UnboundType("Foo")
7195
assert_equal(str(u), "Foo?")

mypy/typeanal.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,16 @@ def analyze_unbound_type_without_type_info(
10501050
column=t.column,
10511051
)
10521052

1053+
if isinstance(sym.node, Var) and sym.node.is_sentinel:
1054+
typ = get_proper_type(sym.node.type)
1055+
if isinstance(typ, Instance) and typ.last_known_value is not None:
1056+
return LiteralType(
1057+
value=typ.last_known_value.value,
1058+
fallback=typ.last_known_value.fallback,
1059+
line=t.line,
1060+
column=t.column,
1061+
)
1062+
10531063
# None of the above options worked. We parse the args (if there are any)
10541064
# to make sure there are no remaining semanal-only types, then give up.
10551065
t = t.copy_modified(args=self.anal_array(t.args))

0 commit comments

Comments
 (0)