diff --git a/mypy/erasetype.py b/mypy/erasetype.py index cb8d66f292dd..9e8dc42fb924 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -246,6 +246,10 @@ class LastKnownValueEraser(TypeTranslator): def visit_instance(self, t: Instance) -> Type: if not t.last_known_value and not t.args: return t + if t.last_known_value is not None and t.last_known_value.is_sentinel_literal(): + # Sentinel values (PEP 661) have no other way to identify themselves than + # via their literal, unlike e.g. enum members, so it must be preserved. + return t return t.copy_modified(args=[a.accept(self) for a in t.args], last_known_value=None) def visit_type_alias_type(self, t: TypeAliasType) -> Type: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index fd507216a6be..d98ab582cc83 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -244,6 +244,10 @@ def visit_type_var(self, t: TypeVarType) -> Type: t = t.copy_modified(upper_bound=t.upper_bound.accept(self)) repl = self.variables.get(t.id, t) if isinstance(repl, ProperType) and isinstance(repl, Instance): + if repl.last_known_value is not None and repl.last_known_value.is_sentinel_literal(): + # Sentinel values (PEP 661) have no other way to identify themselves than + # via their literal, unlike e.g. enum members, so it must survive expansion. + return repl # TODO: do we really need to do this? # If I try to remove this special-casing ~40 tests fail on reveal_type(). return repl.copy_modified(last_known_value=None) diff --git a/mypy/messages.py b/mypy/messages.py index b58c9e7ac4b6..6103ecfac4c6 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2712,6 +2712,12 @@ def format_literal_value(typ: LiteralType) -> str: if isinstance(typ, Instance): itype = typ + if itype.last_known_value is not None and itype.last_known_value.is_sentinel_literal(): + # Sentinel values (PEP 661) have no other way to identify themselves + # than via their literal, so use it instead of the shared fallback + # class name (unlike other literals, sentinels are always formatted + # this way, e.g. "MISSING" rather than "Literal[MISSING]"). + return format_literal_value(itype.last_known_value) # Get the short name of the type. if itype.type.fullname == "types.ModuleType": # Make some common error messages simpler and tidier. @@ -3525,6 +3531,12 @@ def ignore_last_known_values(t: UnionType) -> Type: seen_instances = set() for item in t.items: if isinstance(item, ProperType) and isinstance(item, Instance): + if item.last_known_value is not None and item.last_known_value.is_sentinel_literal(): + # Sentinel values (PEP 661) have no other way to identify themselves + # than via their literal, unlike e.g. enum members, so it must be + # preserved (see mypy/erasetype.py for the same exemption). + union_items.append(item) + continue erased = item.copy_modified(last_known_value=None) if erased in seen_instances: continue diff --git a/test-data/unit/check-sentinels.test b/test-data/unit/check-sentinels.test index 6d39c11375bb..4e29ac3e09fa 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]