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
39 changes: 39 additions & 0 deletions mypy/stubgenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def __get__(self) -> None: # noqa: PLE0302
_Missing = enum.Enum("_Missing", "VALUE")


def _is_sentinel_object(obj: object) -> bool:
"""Whether obj is a sentinel object."""
typ = type(obj)
return typ.__module__ in ("builtins", "typing_extensions") and typ.__name__ in (
"sentinel", # 4.16+
"Sentinel", # 4.14 - 4.15
)


class InspectionStubGenerator(BaseStubGenerator):
"""Stub generator that does not parse code.

Expand Down Expand Up @@ -323,6 +332,12 @@ def add_args(
if default_value is not _Missing.VALUE:
if arg in annotations:
argtype = get_annotation(arg)
elif _is_sentinel_object(default_value):
# Reference the sentinel itself by name when possible, e.g.
# `Incomplete | _MISSING`.
incomplete = self.add_name("_typeshed.Incomplete")
ref = self._sentinel_type_ref(default_value)
argtype = f"{incomplete} | {ref}" if ref else incomplete
else:
argtype = self.get_type_annotation(default_value)
if argtype == "None":
Expand Down Expand Up @@ -536,9 +551,27 @@ def get_type_annotation(self, obj: object) -> str:
return self.add_name("typing.Callable")
elif isinstance(obj, ModuleType):
return self.add_name("types.ModuleType", require=False)
elif _is_sentinel_object(obj):
# Works across all supported Python versions
return "typing_extensions.sentinel"
Comment thread
edgarrmondragon marked this conversation as resolved.
else:
return self.get_type_fullname(type(obj))

def _sentinel_type_ref(self, sentinel_obj: object) -> str | None:
"""Return a reference to the name a sentinel object is bound to.

The sentinel spec says __name__ and __module__ are always present,
typing-extensions 4.16+ implements these, and stubs always use the
latest typing-extensions.

- https://docs.python.org/3.15/builtins/functions.html#sentinel.__name__
- https://docs.python.org/3.15/builtins/functions.html#sentinel.__module__
"""
if (name := getattr(sentinel_obj, "__name__", None)) and name.isidentifier():
return f"{sentinel_obj.__module__}.{name}"

return None

def is_function(self, obj: object) -> bool:
if self.is_c_module:
return inspect.isbuiltin(obj)
Expand Down Expand Up @@ -913,6 +946,12 @@ def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> N
The result lines will be appended to 'output'. If necessary, any
required names will be added to 'imports'.
"""
# A sentinel's name is emitted regardless of privacy/`__all__`.
if _is_sentinel_object(obj):
self.record_name(name)
self.add_import_line("import typing_extensions\n")
output.append(f"{name} = typing_extensions.sentinel('{name}')")
return
if self.is_private_name(name, f"{self.module_name}.{name}") or self.is_not_in_all(name):
return
self.record_name(name)
Expand Down
26 changes: 26 additions & 0 deletions mypy/test/teststubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,32 @@ def test(self, arg0=1, arg1=None) -> None: # type: ignore[no-untyped-def]
output, ["def test(self, arg0: int = ..., arg1: Incomplete | None = ...) -> None: ..."]
)

def test_generate_c_type_sentinel_default(self) -> None:
from typing_extensions import sentinel

_MISSING = sentinel("_MISSING")

class TestClass:
def test(self, arg0=_MISSING) -> None: # type: ignore[no-untyped-def]
pass

output: list[str] = []
mod = ModuleType(TestClass.__module__, "")
gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod)
gen.is_c_module = False
gen.generate_function_stub(
"test",
TestClass.test,
output=output,
class_info=ClassInfo(
self_var="self",
cls=TestClass,
name="TestClass",
docstring=getattr(TestClass, "__doc__", None),
),
)
assert_equal(output, ["def test(self, arg0: Incomplete | _MISSING = ...) -> None: ..."])

def test_non_c_generate_signature_with_kw_only_args(self) -> None:
class TestClass:
def test(
Expand Down
49 changes: 49 additions & 0 deletions test-data/unit/stubgen.test
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,55 @@ def f(x=ord): ...
[out]
def f(x=...) -> None: ...

[case testDefaultArgSentinel]
from typing_extensions import sentinel

_MISSING = sentinel('_MISSING')

def f(x=_MISSING): ...
[out]
def f(x=...) -> None: ...

[case testDefaultArgSentinel_inspect]
from typing_extensions import sentinel

_MISSING = sentinel('_MISSING')

def f(x=_MISSING): ...
[out]
import typing_extensions
from _typeshed import Incomplete

_MISSING = typing_extensions.sentinel('_MISSING')
def f(x: Incomplete | _MISSING = ...): ...

[case testDefaultArgSentinelCrossModule_inspect]
from other import MISSING

def f(x=MISSING): ...
[file other.py]
from typing_extensions import sentinel

MISSING = sentinel('MISSING')
[out]
import other
from _typeshed import Incomplete

def f(x: Incomplete | other.MISSING = ...): ...

[case testDefaultArgSentinelNotValidIdentifier_inspect]
from typing_extensions import sentinel

_MISSING = sentinel('not an identifier')

def f(x=_MISSING): ...
[out]
import typing_extensions
from _typeshed import Incomplete

_MISSING = typing_extensions.sentinel('_MISSING')
def f(x: Incomplete = ...): ...

[case testPreserveFunctionAnnotation]
def f(x: Foo) -> Bar: ...
def g(x: Foo = Foo()) -> Bar: ...
Expand Down
Loading