diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4a38939a03907..d4c530ff92354 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -863,6 +863,8 @@ def _verify_arg_default_value( and type(runtime_arg.default) is not object # And ellipsis and runtime_arg.default is not ... + # And sentinels + and not is_sentinel(runtime_type) and not is_subtype_helper(runtime_type, stub_type) ): yield ( @@ -2090,6 +2092,19 @@ def is_subtype_helper(left: mypy.types.Type, right: mypy.types.Type) -> bool: return mypy.subtypes.is_subtype(left, right) +def is_sentinel(runtime: mypy.types.Type) -> bool: + """Checks whether ``runtime`` is the type of a ``sentinel()`` marker object.""" + runtime = mypy.types.get_proper_type(runtime) + return isinstance(runtime, mypy.types.Instance) and runtime.type.fullname in ( + "builtins.sentinel", + "typing_extensions.sentinel", + # typing_extensions 4.14.0-4.15.x defined the class as `Sentinel`, with + # no lowercase alias; the rename (and `Sentinel = sentinel` alias) + # landed in 4.16.0. + "typing_extensions.Sentinel", + ) + + def get_mypy_node_for_name(module: str, type_name: str) -> mypy.nodes.SymbolNode | None: stub = get_stub(module) if stub is None: diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index fb83ff0ef528c..788f1ed545115 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -123,6 +123,11 @@ class frozenset(Generic[T]): ... class function: pass +class sentinel: + __name__: str + __module__: str + def __new__(cls, name: str, /, *, repr: str | None = None) -> sentinel: ... + class int: ... class float: ... class bool(int): ... @@ -3074,6 +3079,58 @@ def __init__(self, formatter_class=HelpFormatter): ... error=None, ) + @collect_cases + def test_object_marker(self) -> Iterator[Case]: + yield Case( + stub=""" + def f(x: int = ...) -> None: ... + """, + runtime=""" + _MISSING = object() + + + def f(x=_MISSING): + pass + """, + error=None, + ) + + @collect_cases + def test_sentinel_object_like_marker(self) -> Iterator[Case]: + yield Case( + stub=""" + def f(x: int = ...) -> None: ... + """, + runtime=""" + from typing_extensions import sentinel + + _MISSING = sentinel("_MISSING") + + + def f(x=_MISSING): + pass + """, + error=None, + ) + + @collect_cases + def test_legacy_sentinel_object_like_marker(self) -> Iterator[Case]: + yield Case( + stub=""" + def f(x: int = ...) -> None: ... + """, + runtime=""" + from typing_extensions import Sentinel + + _MISSING = Sentinel("_MISSING") + + + def f(x=_MISSING): + pass + """, + error=None, + ) + def remove_color_code(s: str) -> str: return re.sub("\\x1b.*?m", "", s) # this works!