diff --git a/mypy/cache.py b/mypy/cache.py index 013a286fae2c8..13b3041a6e224 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -48,10 +48,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Final, TypeAlias as _TypeAlias - -if TYPE_CHECKING: - from mypy.types import SentinelValue +from typing import Any, Final, TypeAlias as _TypeAlias from librt.internal import ( ReadBuffer as ReadBuffer, @@ -72,7 +69,7 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 11 +CACHE_VERSION: Final = 12 # Type used internally to represent errors: # (path, line, column, end_line, end_column, severity, message, code) @@ -311,7 +308,6 @@ def read(cls, data: ReadBuffer) -> CacheMetaEx | None: LITERAL_BYTES: Final[Tag] = 5 LITERAL_FLOAT: Final[Tag] = 6 LITERAL_COMPLEX: Final[Tag] = 7 -LITERAL_SENTINEL: Final[Tag] = 8 # Collections. LIST_GEN: Final[Tag] = 20 @@ -332,7 +328,7 @@ def read(cls, data: ReadBuffer) -> CacheMetaEx | None: END_TAG: Final[Tag] = 255 -def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float | SentinelValue: +def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float: if tag == LITERAL_INT: return read_int_bare(data) elif tag == LITERAL_STR: @@ -343,18 +339,12 @@ def read_literal(data: ReadBuffer, tag: Tag) -> int | str | bool | float | Senti return True elif tag == LITERAL_FLOAT: return read_float_bare(data) - elif tag == LITERAL_SENTINEL: - from mypy.types import SentinelValue as _SentinelValue - - return _SentinelValue(read_str_bare(data), read_str_bare(data)) assert False, f"Unknown literal tag {tag}" # There is an intentional asymmetry between read and write for literals because # None and/or complex values are only allowed in some contexts but not in others. -def write_literal( - data: WriteBuffer, value: int | str | bool | float | complex | SentinelValue | None -) -> None: +def write_literal(data: WriteBuffer, value: int | str | bool | float | complex | None) -> None: if isinstance(value, bool): write_bool(data, value) elif isinstance(value, int): @@ -370,12 +360,8 @@ def write_literal( write_tag(data, LITERAL_COMPLEX) write_float_bare(data, value.real) write_float_bare(data, value.imag) - elif value is None: - write_tag(data, LITERAL_NONE) else: - write_tag(data, LITERAL_SENTINEL) - write_str_bare(data, value.fullname) - write_str_bare(data, value.name) + write_tag(data, LITERAL_NONE) def read_int(data: ReadBuffer) -> int: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 172d44555b946..315daaf6c8cd9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -85,6 +85,7 @@ PromoteExpr, RefExpr, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -6456,6 +6457,9 @@ def visit_type_var_tuple_expr(self, e: TypeVarTupleExpr) -> Type: def visit_newtype_expr(self, e: NewTypeExpr) -> Type: return AnyType(TypeOfAny.special_form) + def visit_sentinel_expr(self, e: SentinelExpr) -> Type: + return Instance(e.info, [], line=e.line, column=e.column) + def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type: tuple_type = e.info.tuple_type if tuple_type: diff --git a/mypy/evalexpr.py b/mypy/evalexpr.py index f46e5c23c3e43..c8b2a8101ca84 100644 --- a/mypy/evalexpr.py +++ b/mypy/evalexpr.py @@ -186,6 +186,9 @@ def visit_typeddict_expr(self, o: mypy.nodes.TypedDictExpr) -> object: def visit_newtype_expr(self, o: mypy.nodes.NewTypeExpr) -> object: return UNKNOWN + def visit_sentinel_expr(self, o: mypy.nodes.SentinelExpr) -> object: + return UNKNOWN + def visit__promote_expr(self, o: mypy.nodes.PromoteExpr) -> object: return UNKNOWN diff --git a/mypy/literals.py b/mypy/literals.py index f572a6f9c624f..628a5275e7620 100644 --- a/mypy/literals.py +++ b/mypy/literals.py @@ -36,6 +36,7 @@ ParamSpecExpr, PromoteExpr, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -311,6 +312,9 @@ def visit_typeddict_expr(self, e: TypedDictExpr) -> None: def visit_newtype_expr(self, e: NewTypeExpr) -> None: return None + def visit_sentinel_expr(self, e: SentinelExpr) -> None: + return None + def visit__promote_expr(self, e: PromoteExpr) -> None: return None diff --git a/mypy/messages.py b/mypy/messages.py index b58c9e7ac4b6c..61700e310104f 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -103,6 +103,7 @@ flatten_nested_unions, get_proper_type, get_proper_types, + sentinel_display_name, ) from mypy.typetraverser import TypeTraverserVisitor from mypy.util import plural_s, unmangle @@ -2722,7 +2723,9 @@ def format_literal_value(typ: LiteralType) -> str: if itype.type.fullname == "typing._SpecialForm": # This is not a real type but used for some typing-related constructs. return "" - if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames): + if itype.type.is_sentinel: + base_str = sentinel_display_name(itype.type) + elif verbosity >= 2 or (fullnames and itype.type.fullname in fullnames): base_str = itype.type.fullname else: base_str = itype.type.name @@ -2783,8 +2786,6 @@ def format_literal_value(typ: LiteralType) -> str: modifier += "=" items.append(f"{item_name!r}{modifier}: {format(item_type)}") return f"TypedDict({{{', '.join(items)}}})" - elif isinstance(typ, LiteralType) and typ.is_sentinel_literal(): - return format_literal_value(typ) elif isinstance(typ, LiteralType): return f"Literal[{format_literal_value(typ)}]" elif isinstance(typ, UnionType): @@ -2792,9 +2793,6 @@ def format_literal_value(typ: LiteralType) -> str: if not isinstance(typ, UnionType): return format(typ) literal_items, union_items = separate_union_literals(typ) - sentinel_items = [item for item in literal_items if item.is_sentinel_literal()] - literal_items = [item for item in literal_items if not item.is_sentinel_literal()] - union_items = [*sentinel_items, *union_items] # Coalesce multiple Literal[] members. This also changes output order. # If there's just one Literal item, retain the original ordering. diff --git a/mypy/mixedtraverser.py b/mypy/mixedtraverser.py index 40f3e640ef032..950e33d6db7c4 100644 --- a/mypy/mixedtraverser.py +++ b/mypy/mixedtraverser.py @@ -10,6 +10,7 @@ NamedTupleExpr, NewTypeExpr, PromoteExpr, + SentinelExpr, TypeAlias, TypeAliasExpr, TypeAliasStmt, @@ -95,6 +96,10 @@ def visit_newtype_expr(self, o: NewTypeExpr, /) -> None: self.process_type_info(o.info) self.visit_optional_type(o.old_type) + def visit_sentinel_expr(self, o: SentinelExpr, /) -> None: + super().visit_sentinel_expr(o) + self.process_type_info(o.info) + # Statements def visit_assignment_stmt(self, o: AssignmentStmt, /) -> None: diff --git a/mypy/nodes.py b/mypy/nodes.py index cbeda29ec74a2..595a8ee06ea5b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import sys from abc import abstractmethod from collections import defaultdict from collections.abc import Callable, Iterator, Sequence @@ -79,6 +80,11 @@ from mypy.util import is_sunder, is_typeshed_file, short_type from mypy.visitor import ExpressionVisitor, NodeVisitor, StatementVisitor +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + if TYPE_CHECKING: from mypy.patterns import Pattern @@ -1646,9 +1652,7 @@ def read(cls, data: ReadBuffer) -> Var: if tag == LITERAL_COMPLEX: v.final_value = complex(read_float_bare(data), read_float_bare(data)) elif tag != LITERAL_NONE: - val = read_literal(data, tag) - assert not isinstance(val, mypy.types.SentinelValue) - v.final_value = val + v.final_value = read_literal(data, tag) assert read_tag(data) == END_TAG return v @@ -3544,6 +3548,30 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_newtype_expr(self) +class SentinelExpr(Expression): + """PEP 661 sentinel()/Sentinel() call expression. + + Marks the rvalue of a sentinel declaration (`X = sentinel("X")`) so that its type + is the synthetic per-declaration class in `info`, rather than whatever ordinary + call-checking against sentinel's/Sentinel's __init__ signature would produce. + """ + + __slots__ = ("info",) + + __match_args__ = ("info",) + + # The synthesized class representing this specific sentinel. + info: TypeInfo + + def __init__(self, info: TypeInfo, line: int, column: int) -> None: + super().__init__(line=line, column=column) + self.info = info + + @override + def accept(self, visitor: ExpressionVisitor[T]) -> T: + return visitor.visit_sentinel_expr(self) + + class AwaitExpr(Expression): """Await expression (await ...).""" @@ -3666,6 +3694,7 @@ class is generic then it will be a type constructor of higher kind. "is_named_tuple", "typeddict_type", "is_newtype", + "is_sentinel", "is_intersection", "metadata", "alt_promote", @@ -3806,6 +3835,9 @@ class is generic then it will be a type constructor of higher kind. # Is this a newtype type? is_newtype: bool + # Is this a synthetic type generated for a PEP 661 sentinel()/Sentinel() declaration? + is_sentinel: bool + # Is this a synthesized intersection type? is_intersection: bool @@ -3860,6 +3892,7 @@ class is generic then it will be a type constructor of higher kind. "meta_fallback_to_any", "is_named_tuple", "is_newtype", + "is_sentinel", "is_protocol", "runtime_protocol", "is_final", @@ -3907,6 +3940,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.is_named_tuple = False self.typeddict_type = None self.is_newtype = False + self.is_sentinel = False self.is_intersection = False self.metadata = {} self.self_type = None @@ -4350,6 +4384,7 @@ def write(self, data: WriteBuffer) -> None: self.meta_fallback_to_any, self.is_named_tuple, self.is_newtype, + self.is_sentinel, self.is_protocol, self.runtime_protocol, self.is_final, @@ -4423,12 +4458,13 @@ def read(cls, data: ReadBuffer) -> TypeInfo: ti.meta_fallback_to_any, ti.is_named_tuple, ti.is_newtype, + ti.is_sentinel, ti.is_protocol, ti.runtime_protocol, ti.is_final, ti.is_disjoint_base, ti.is_intersection, - ) = read_flags(data, num_flags=11) + ) = read_flags(data, num_flags=12) ti.metadata = read_json(data) tag = read_tag(data) if tag != LITERAL_NONE: diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a511e714ac6b4..facaec6386b71 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -63,7 +63,6 @@ LiteralType, NoneType, ProperType, - SentinelValue, TupleType, Type, TypeOfAny, @@ -800,11 +799,11 @@ def _is_kw_only_type(self, node: Type | None) -> bool: if node is None: return False node_type = get_proper_type(node) - if isinstance(node_type, LiteralType) and isinstance(node_type.value, SentinelValue): - # PEP 661 sentinel: `KW_ONLY = sentinel("KW_ONLY")` (Python 3.15+). - return node_type.value.fullname == "dataclasses.KW_ONLY" if not isinstance(node_type, Instance): return False + if node_type.type.is_sentinel: + # Clean up synthetic class's mangled fullname + return node_type.type.fullname.removesuffix("'") == "dataclasses.KW_ONLY" return node_type.type.fullname == "dataclasses.KW_ONLY" def _add_dataclass_fields_magic_attribute(self) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f961687a8aee..41ca60da6982d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -156,6 +156,7 @@ RefExpr, ReturnStmt, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -290,7 +291,6 @@ ParamSpecType, PlaceholderType, ProperType, - SentinelValue, TrivialSyntheticTypeTranslator, TupleType, Type, @@ -3435,16 +3435,32 @@ def sentinel_type_for_var(self, var: Var, rvalue: Expression) -> Instance | None typ = self.named_type_or_none(callee.fullname) if typ is None: return None - name = f"{self.type.name}.{var.name}" if self.type is not None else var.name - return typ.copy_modified( - last_known_value=LiteralType( - SentinelValue(var.fullname, name), - fallback=typ, - line=rvalue.line, - column=rvalue.column, - ) + + # Give this sentinel its own synthetic nominal type (like NewType), rather than + # tagging the shared sentinel/Sentinel class with a Literal[...] value: a sentinel + # has no way to identify itself other than this type, unlike e.g. an enum member. + # The mangled name avoids colliding with the Var of the same name in this scope. + mangled_name = f"{var.name}'" + info = self.basic_new_typeinfo(mangled_name, typ, rvalue.line) + info.is_sentinel = True + + # Insert directly rather than via add_symbol(): redefining the sentinel Var (e.g. + # reassigning MISSING = sentinel(...) again) is already reported once for the Var + # itself, and would otherwise also be (redundantly) reported for this mangled name. + symbol_table = self.type.names if self.type is not None else self.globals + symbol_table[mangled_name] = SymbolTableNode( + kind=MDEF if self.type is not None else GDEF, + node=info, + module_public=False, + module_hidden=True, ) + # Redirect type-checking of this call expression to visit_sentinel_expr, so its + # type is this synthetic class rather than whatever ordinary call-checking + # against sentinel's/Sentinel's __init__ signature would otherwise produce. + rvalue.analyzed = SentinelExpr(info, line=rvalue.line, column=rvalue.column) + return Instance(info, [], line=rvalue.line, column=rvalue.column) + def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool: """Special case 'X = X' in global scope. @@ -4816,7 +4832,6 @@ def store_declared_types(self, lvalue: Lvalue, typ: Type) -> None: var.is_final and isinstance(typ, Instance) and typ.last_known_value - and not isinstance(typ.last_known_value.value, SentinelValue) and (not self.type or not self.type.is_enum) ): var.final_value = typ.last_known_value.value diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 1de2d23849d8b..1d0674ed317af 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -158,7 +158,6 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ParamSpecType, PartialType, ProperType, - SentinelValue, TupleType, Type, TypeAliasType, @@ -975,7 +974,13 @@ def get_type_triggers(self, typ: Type) -> list[str]: return get_type_triggers(typ, self.use_logical_deps, self.seen_aliases) def visit_instance(self, typ: Instance) -> list[str]: - trigger = make_trigger(typ.type.fullname) + if typ.type.is_sentinel: + # The synthetic per-declaration class has a mangled fullname (see + # semanal.py's sentinel_type_for_var) that doesn't correspond to any + # externally visible symbol; trigger on the sentinel Var's fullname instead. + trigger = make_trigger(typ.type.fullname.removesuffix("'")) + else: + trigger = make_trigger(typ.type.fullname) triggers = [trigger] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) @@ -1097,10 +1102,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> list[str]: return triggers def visit_literal_type(self, typ: LiteralType) -> list[str]: - triggers = self.get_type_triggers(typ.fallback) - if isinstance(typ.value, SentinelValue): - triggers.append(make_trigger(typ.value.fullname)) - return triggers + return self.get_type_triggers(typ.fallback) def visit_unbound_type(self, typ: UnboundType) -> list[str]: return [] diff --git a/mypy/strconv.py b/mypy/strconv.py index b26f1d8d71a8e..dea18f34acaf8 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -594,6 +594,9 @@ def visit__promote_expr(self, o: mypy.nodes.PromoteExpr) -> str: def visit_newtype_expr(self, o: mypy.nodes.NewTypeExpr) -> str: return f"NewTypeExpr:{o.line}({o.name}, {self.dump([o.old_type], o)})" + def visit_sentinel_expr(self, o: mypy.nodes.SentinelExpr) -> str: + return f"SentinelExpr:{o.line}({o.info.fullname})" + def visit_lambda_expr(self, o: mypy.nodes.LambdaExpr) -> str: a = self.func_helper(o) return self.dump(a, o) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index b287e82b3d4af..6de922cb7f234 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -5,9 +5,6 @@ import re from unittest import TestCase, skipUnless -from librt.internal import ReadBuffer, WriteBuffer - -from mypy.cache import read_tag from mypy.erasetype import erase_type, remove_instance_last_known_values from mypy.indirection import TypeIndirectionVisitor from mypy.join import join_types @@ -33,7 +30,6 @@ from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture from mypy.typeops import false_only, make_simplified_union, true_only from mypy.types import ( - LITERAL_TYPE, AnyType, CallableType, Instance, @@ -41,7 +37,6 @@ NoneType, Overloaded, ProperType, - SentinelValue, TupleType, Type, TypedDictType, @@ -71,25 +66,6 @@ def setUp(self) -> None: def test_any(self) -> None: assert_equal(str(AnyType(TypeOfAny.special_form)), "Any") - def test_sentinel_literal_json_roundtrip(self) -> None: - literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a) - assert_equal(str(literal), "MISSING") - data = literal.serialize() - assert isinstance(data, dict) - roundtrip = LiteralType.deserialize(data) - self.assertEqual(roundtrip.value, literal.value) - self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname) - - def test_sentinel_literal_ff_roundtrip(self) -> None: - literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a) - data = WriteBuffer() - literal.write(data) - buffer = ReadBuffer(data.getvalue()) - assert read_tag(buffer) == LITERAL_TYPE - roundtrip = LiteralType.read(buffer) - self.assertEqual(roundtrip.value, literal.value) - self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname) - def test_simple_unbound_type(self) -> None: u = UnboundType("Foo") assert_equal(str(u), "Foo?") diff --git a/mypy/traverser.py b/mypy/traverser.py index 6fdb54298f85c..f0bddc39a1c75 100644 --- a/mypy/traverser.py +++ b/mypy/traverser.py @@ -62,6 +62,7 @@ RaiseStmt, ReturnStmt, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -503,6 +504,9 @@ def visit_typeddict_expr(self, o: TypedDictExpr, /) -> None: def visit_newtype_expr(self, o: NewTypeExpr, /) -> None: return None + def visit_sentinel_expr(self, o: SentinelExpr, /) -> None: + return None + def visit__promote_expr(self, o: PromoteExpr, /) -> None: return None @@ -895,6 +899,11 @@ def visit_newtype_expr(self, o: NewTypeExpr, /) -> None: return super().visit_newtype_expr(o) + def visit_sentinel_expr(self, o: SentinelExpr, /) -> None: + if not self.visit(o): + return + super().visit_sentinel_expr(o) + def visit_await_expr(self, o: AwaitExpr, /) -> None: if not self.visit(o): return diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 25092de66a149..8b3c32d2f1f63 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -69,6 +69,7 @@ RefExpr, ReturnStmt, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -697,6 +698,9 @@ def visit_newtype_expr(self, node: NewTypeExpr) -> NewTypeExpr: res.info = node.info return res + def visit_sentinel_expr(self, node: SentinelExpr) -> SentinelExpr: + return SentinelExpr(node.info, line=node.line, column=node.column) + def visit_namedtuple_expr(self, node: NamedTupleExpr) -> NamedTupleExpr: return NamedTupleExpr(node.info) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 3e493502d0dad..018278a8986ad 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1060,13 +1060,8 @@ def analyze_unbound_type_without_type_info( if isinstance(sym.node, Var) and sym.node.is_sentinel: typ = get_proper_type(sym.node.type) - if isinstance(typ, Instance) and typ.last_known_value is not None: - return LiteralType( - value=typ.last_known_value.value, - fallback=typ.last_known_value.fallback, - line=t.line, - column=t.column, - ) + if isinstance(typ, Instance): + return Instance(typ.type, [], line=t.line, column=t.column) # None of the above options worked. We parse the args (if there are any) # to make sure there are no remaining semanal-only types, then give up. diff --git a/mypy/typeops.py b/mypy/typeops.py index 8453da4dd31c0..55bc70123c0c2 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -37,7 +37,6 @@ from mypy.types import ( ELLIPSIS_TYPE_NAMES, NOT_IMPLEMENTED_TYPE_NAMES, - SENTINEL_TYPE_NAMES, AnyType, CallableType, ExtraAttrs, @@ -1057,10 +1056,10 @@ def is_singleton_identity_type(typ: ProperType) -> bool: (typ.type.is_enum and len(typ.type.enum_members) == 1) or (typ.type.fullname in ELLIPSIS_TYPE_NAMES) or (typ.type.fullname in NOT_IMPLEMENTED_TYPE_NAMES) - or (typ.type.fullname in SENTINEL_TYPE_NAMES) + or typ.type.is_sentinel ) if isinstance(typ, LiteralType): - return typ.is_enum_literal() or typ.is_sentinel_literal() or isinstance(typ.value, bool) + return typ.is_enum_literal() or isinstance(typ.value, bool) if isinstance(typ, TypeType) and isinstance(typ.item, Instance) and typ.item.type.is_final: return True if isinstance(typ, FunctionLike) and typ.is_type_obj() and typ.type_object().is_final: diff --git a/mypy/types.py b/mypy/types.py index 7a1470964d251..f41567587f51b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -98,12 +98,7 @@ # # Note: Float values are only used internally. They are not accepted within # Literal[...]. -class SentinelValue(NamedTuple): - fullname: str - name: str - - -LiteralValue: _TypeAlias = int | str | bool | float | SentinelValue +LiteralValue: _TypeAlias = int | str | bool | float TUPLE_NAMES: Final = ("builtins.tuple", "typing.Tuple") @@ -124,6 +119,18 @@ class SentinelValue(NamedTuple): "typing_extensions.Sentinel", ) + +def sentinel_display_name(info: mypy.nodes.TypeInfo) -> str: + """User-facing name for a synthetic per-declaration sentinel class. + + Its fullname is mangled (see semanal.py's sentinel_type_for_var) to avoid + colliding with the Var of the same name; this strips that mangling marker + and the module prefix, e.g. "mod.Cls.IN_CLASS'" -> "Cls.IN_CLASS". + """ + name = info.fullname.removesuffix("'") + return name.removeprefix(f"{info.module_name}.") + + TYPED_NAMEDTUPLE_NAMES: Final = ("typing.NamedTuple", "typing_extensions.NamedTuple") # Supported names of TypedDict type constructors. @@ -3354,15 +3361,11 @@ def __init__( # almost no test cases where we would redundantly compute # `can_be_false`/`can_be_true`. def can_be_false_default(self) -> bool: - if isinstance(self.value, SentinelValue): - return False if self.fallback.type.is_enum: return self.fallback.can_be_false return not self.value def can_be_true_default(self) -> bool: - if isinstance(self.value, SentinelValue): - return True if self.fallback.type.is_enum: return self.fallback.can_be_true return bool(self.value) @@ -3383,9 +3386,6 @@ def __eq__(self, other: object) -> bool: def is_enum_literal(self) -> bool: return self.fallback.type.is_enum - def is_sentinel_literal(self) -> bool: - return isinstance(self.value, SentinelValue) - def value_repr(self) -> str: """Returns the string representation of the underlying type. @@ -3393,9 +3393,6 @@ def value_repr(self) -> str: except it includes some additional logic to correctly handle cases where the value is a string, byte string, a unicode string, or an enum. """ - if isinstance(self.value, SentinelValue): - return self.value.name - raw = repr(self.value) fallback_name = self.fallback.type.fullname @@ -3414,19 +3411,16 @@ def value_repr(self) -> str: return raw def serialize(self) -> JsonDict | str: - value: LiteralValue | JsonDict = self.value - if isinstance(value, SentinelValue): - value = {".class": "SentinelValue", "fullname": value.fullname, "name": value.name} - return {".class": "LiteralType", "value": value, "fallback": self.fallback.serialize()} + return { + ".class": "LiteralType", + "value": self.value, + "fallback": self.fallback.serialize(), + } @classmethod def deserialize(cls, data: JsonDict) -> LiteralType: assert data[".class"] == "LiteralType" - value = data["value"] - if isinstance(value, dict): - assert value[".class"] == "SentinelValue" - value = SentinelValue(value["fullname"], value["name"]) - return LiteralType(value=value, fallback=Instance.deserialize(data["fallback"])) + return LiteralType(value=data["value"], fallback=Instance.deserialize(data["fallback"])) def write(self, data: WriteBuffer) -> None: write_tag(data, LITERAL_TYPE) @@ -3888,7 +3882,9 @@ def visit_instance(self, t: Instance, /) -> str: fullname = t.type.fullname if not self.options.reveal_verbose_types and fullname.startswith("builtins."): fullname = t.type.name - if t.last_known_value and not t.args: + if t.type.is_sentinel: + s = sentinel_display_name(t.type) + elif t.last_known_value and not t.args: # Instances with a literal fallback should never be generic. If they are, # something went wrong so we fall back to showing the full Instance repr. s = f"{t.last_known_value.accept(self)}?" @@ -4044,7 +4040,7 @@ def visit_callable_type(self, t: CallableType, /) -> str: ) else: vs.append( - f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}" + f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}" ) else: # For other TypeVarLikeTypes, use the name and default @@ -4103,8 +4099,6 @@ def visit_raw_expression_type(self, t: RawExpressionType, /) -> str: return repr(t.literal_value) def visit_literal_type(self, t: LiteralType, /) -> str: - if isinstance(t.value, SentinelValue): - return t.value_repr() return f"Literal[{t.value_repr()}]" def visit_union_type(self, t: UnionType, /) -> str: diff --git a/mypy/visitor.py b/mypy/visitor.py index de754c408f97d..4e0c99557c0e2 100644 --- a/mypy/visitor.py +++ b/mypy/visitor.py @@ -191,6 +191,10 @@ def visit_typeddict_expr(self, o: mypy.nodes.TypedDictExpr, /) -> T: def visit_newtype_expr(self, o: mypy.nodes.NewTypeExpr, /) -> T: pass + @abstractmethod + def visit_sentinel_expr(self, o: mypy.nodes.SentinelExpr, /) -> T: + pass + @abstractmethod def visit__promote_expr(self, o: mypy.nodes.PromoteExpr, /) -> T: pass @@ -603,6 +607,9 @@ def visit_typeddict_expr(self, o: mypy.nodes.TypedDictExpr, /) -> T: def visit_newtype_expr(self, o: mypy.nodes.NewTypeExpr, /) -> T: raise NotImplementedError() + def visit_sentinel_expr(self, o: mypy.nodes.SentinelExpr, /) -> T: + raise NotImplementedError() + def visit__promote_expr(self, o: mypy.nodes.PromoteExpr, /) -> T: raise NotImplementedError() diff --git a/mypyc/irbuild/visitor.py b/mypyc/irbuild/visitor.py index 594ec414fa657..c5636d9706544 100644 --- a/mypyc/irbuild/visitor.py +++ b/mypyc/irbuild/visitor.py @@ -60,6 +60,7 @@ RaiseStmt, ReturnStmt, RevealExpr, + SentinelExpr, SetComprehension, SetExpr, SliceExpr, @@ -362,6 +363,9 @@ def visit_namedtuple_expr(self, o: NamedTupleExpr) -> Value: def visit_newtype_expr(self, o: NewTypeExpr) -> Value: assert False, "can't compile analysis-only expressions" + def visit_sentinel_expr(self, o: SentinelExpr) -> Value: + assert False, "can't compile analysis-only expressions" + def visit_temp_node(self, o: TempNode) -> Value: assert False, "can't compile analysis-only expressions" diff --git a/mypyc/lib-rt/internal/librt_internal.c b/mypyc/lib-rt/internal/librt_internal.c index dc811efbabad1..04de7610736c0 100644 --- a/mypyc/lib-rt/internal/librt_internal.c +++ b/mypyc/lib-rt/internal/librt_internal.c @@ -930,7 +930,6 @@ write_tag(PyObject *self, PyObject *const *args, size_t nargs) { #define LITERAL_BYTES 5 #define LITERAL_FLOAT 6 #define LITERAL_COMPLEX 7 -#define LITERAL_SENTINEL 8 // Supported builtin collections. #define LIST_GEN 20 @@ -1162,11 +1161,6 @@ _skip_object(PyObject *data, uint8_t tag) { return _skip(data, 8); if (tag == LITERAL_COMPLEX) return _skip(data, 16); - if (tag == LITERAL_SENTINEL) { - if (unlikely(_skip_str_bytes(data) == CPY_NONE_ERROR)) - return CPY_NONE_ERROR; - return _skip_str_bytes(data); - } PyErr_Format(PyExc_ValueError, "Unsupported tag: %d", tag); return CPY_NONE_ERROR; } diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 8203de6feee2d..20a621b863c40 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2999,7 +2999,7 @@ from mypy_extensions import u8 from librt.internal import ( ReadBuffer, WriteBuffer, write_bool, read_bool, write_str, read_str, write_float, read_float, write_int, read_int, write_tag, read_tag, write_bytes, read_bytes, - cache_version, extract_symbol, + cache_version, ) from testutil import assertRaises @@ -3217,16 +3217,6 @@ def test_buffer_str_size() -> None: b = ReadBuffer(b.getvalue()) assert read_str(b) == s -def test_extract_symbol_sentinel_literal() -> None: - data = WriteBuffer() - write_tag(data, 8) # LITERAL_SENTINEL - write_str(data, "__main__.MISSING") - write_str(data, "MISSING") - write_tag(data, 255) # END_TAG - - payload = data.getvalue() - assert extract_symbol(ReadBuffer(payload)) == payload - [file driver.py] from native import * @@ -3241,7 +3231,6 @@ test_buffer_str_size() test_buffer_int_powers() test_positive_long_int_serialized_bytes() test_negative_long_int_serialized_bytes() -test_extract_symbol_sentinel_literal() def test_buffer_basic_interpreted() -> None: b = WriteBuffer() diff --git a/test-data/unit/check-sentinels.test b/test-data/unit/check-sentinels.test index 6d39c11375bb5..4e29ac3e09fa6 100644 --- a/test-data/unit/check-sentinels.test +++ b/test-data/unit/check-sentinels.test @@ -181,11 +181,38 @@ from typing_extensions import sentinel, assert_type MISSING = sentinel("MISSING") ALIAS = MISSING -assert_type(ALIAS, sentinel) +# The value still identifies as the same sentinel... +assert_type(ALIAS, MISSING) def func(x: int | MISSING = MISSING) -> None: pass func(MISSING) -func(ALIAS) # E: Argument 1 to "func" has incompatible type "Sentinel"; expected "int | MISSING" +func(ALIAS) + +# ...but the reassignment does not make ALIAS usable as a type alias. +def uses_alias_as_type(x: ALIAS) -> None: # E: Variable "__main__.ALIAS" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases + pass [builtins fixtures/tuple.pyi] + +[case testSentinelPreservedThroughGenericSubstitution] +from typing import assert_type +from typing_extensions import sentinel + +Unknown = sentinel("Unknown") + +def func(d: dict[str, str]) -> None: + var = d.get("key", Unknown) + assert_type(var, str | Unknown) +[builtins fixtures/dict-full.pyi] + +[case testSentinelPreservedInErrorMessages] +from typing_extensions import sentinel + +Unknown = sentinel("Unknown") + +def func(d: dict[str, str]) -> None: + var = d.get("key", Unknown) + x: int = var # E: Incompatible types in assignment (expression has type "str | Unknown", variable has type "int") +[builtins fixtures/dict-full.pyi]