Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 19 additions & 2 deletions test-data/unit/check-sentinels.test
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,28 @@ 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more useful now and still in line with #21647 (comment) and the spec:

https://typing.python.org/en/latest/spec/special-types.html#sentinels


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]
Loading