diff --git a/mypy/stubdoc.py b/mypy/stubdoc.py index 928d024514f3..561ab7d650d7 100644 --- a/mypy/stubdoc.py +++ b/mypy/stubdoc.py @@ -108,6 +108,7 @@ def format_sig( is_async: bool = False, any_val: str | None = None, docstring: str | None = None, + generic: str = "", ) -> str: args: list[str] = [] for arg in self.args: @@ -141,8 +142,13 @@ def format_sig( retfield = " -> " + ret_type prefix = "async " if is_async else "" - sig = "{indent}{prefix}def {name}({args}){ret}:".format( - indent=indent, prefix=prefix, name=self.name, args=", ".join(args), ret=retfield + sig = "{indent}{prefix}def {name}{generic}({args}){ret}:".format( + indent=indent, + prefix=prefix, + name=self.name, + args=", ".join(args), + ret=retfield, + generic=generic, ) if docstring: suffix = f"\n{indent} {mypy.util.quote_docstring(docstring)}" diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index 7ab500b4fe12..d0df9309e495 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -11,6 +11,7 @@ import inspect import keyword import os.path +from contextlib import suppress from types import FunctionType, ModuleType from typing import Any, Callable, Mapping @@ -33,6 +34,7 @@ ClassInfo, FunctionContext, SignatureGenerator, + generate_inline_generic, infer_method_arg_types, infer_method_ret_type, ) @@ -496,6 +498,8 @@ def is_skipped_attribute(self, attr: str) -> bool: "__firstlineno__", "__static_attributes__", "__annotate__", + "__orig_bases__", + "__parameters__", ) or attr in self.IGNORED_DUNDERS or is_pybind_skipped_attribute(attr) # For pickling @@ -646,7 +650,17 @@ def generate_function_stub( if docstring: docstring = self._indent_docstring(docstring) - output.extend(self.format_func_def(inferred, decorators=decorators, docstring=docstring)) + + if hasattr(obj, "__type_params__"): + generic = generate_inline_generic(obj.__type_params__) + else: + generic = "" + + output.extend( + self.format_func_def( + inferred, decorators=decorators, docstring=docstring, generic=generic + ) + ) self._fix_iter(ctx, inferred, output) def _indent_docstring(self, docstring: str) -> str: @@ -847,10 +861,16 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) -> else: attrs.append((attr, value)) + inline_generic = "" + for attr, value in attrs: if attr == "__hash__" and value is None: # special case for __hash__ continue + elif attr == "__type_params__": + inline_generic = generate_inline_generic(value) + continue + prop_type_name = self.strip_or_import(self.get_type_annotation(value)) classvar = self.add_name("typing.ClassVar") static_properties.append(f"{self._indent}{attr}: {classvar}[{prop_type_name}] = ...") @@ -858,12 +878,16 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) -> self.dedent() bases = self.get_base_types(cls) + if inline_generic != "": + # Removes typing.Generic form bases if it exists for python3.12 inline generics + with suppress(ValueError): + bases.remove("typing.Generic") if bases: bases_str = "(%s)" % ", ".join(bases) else: bases_str = "" if types or static_properties or rw_properties or methods or ro_properties: - output.append(f"{self._indent}class {class_name}{bases_str}:") + output.append(f"{self._indent}class {class_name}{inline_generic}{bases_str}:") for line in types: if ( output @@ -882,7 +906,7 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) -> for line in ro_properties: output.append(line) else: - output.append(f"{self._indent}class {class_name}{bases_str}: ...") + output.append(f"{self._indent}class {class_name}{inline_generic}{bases_str}: ...") def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> None: """Generate stub for a single variable using runtime introspection. diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 2f2db0dbbe53..193c82c5c0c4 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -8,8 +8,8 @@ from abc import abstractmethod from collections import defaultdict from contextlib import contextmanager -from typing import Final, Iterable, Iterator, Mapping -from typing_extensions import overload +from typing import Final, Iterable, Iterator, Mapping, TypeVar, cast +from typing_extensions import ParamSpec, TypeVarTuple, overload from mypy_extensions import mypyc_attr @@ -766,6 +766,7 @@ def format_func_def( is_coroutine: bool = False, decorators: list[str] | None = None, docstring: str | None = None, + generic: str = "", ) -> list[str]: lines: list[str] = [] if decorators is None: @@ -781,6 +782,7 @@ def format_func_def( indent=self._indent, is_async=is_coroutine, docstring=docstring if self._include_docstrings else None, + generic=generic, ) ) return lines @@ -842,3 +844,37 @@ def should_reexport(self, name: str, full_module: str, name_is_alias: bool) -> b if self._all_: return name in self._all_ return True + + +def generate_inline_generic(type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]) -> str: + """Generate stub for inline generic from __type_params__""" + + if len(type_params) == 0: + return "" + + generic_arg_list: list[str] = [] + + for type_param in type_params: + # Not done with isinstance checks so compiled code doesn't necessarily need to use + # typing.TypeVar, just something with similar duck typing. + if hasattr(type_param, "__constraints__"): + # Is TypeVar + typevar = cast(TypeVar, type_param) + if typevar.__bound__: + generic_arg_list.append(f"{typevar.__name__}: {typevar.__bound__.__name__}") + elif typevar.__constraints__ != (): + generic_arg_list.append( + f"{typevar.__name__}: ({', '.join([constraint.__name__ for constraint in typevar.__constraints__])})" + ) + else: + generic_arg_list.append(f"{typevar.__name__}") + elif hasattr(type_param, "__bound__"): + # Is ParamSpec + param_spec = cast(ParamSpec, type_param) + generic_arg_list.append(f"**{param_spec.__name__}") + else: + # Is TypeVarTuple + generic_arg_list.append(f"*{type_param.__name__}") + + flat_internals = ", ".join(generic_arg_list) + return f"[{flat_internals}]" diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index e65a16c8f395..09e821fadb18 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -8,7 +8,8 @@ import tempfile import unittest from types import ModuleType -from typing import Any +from typing import Any, Tuple, TypeVar, cast +from typing_extensions import ParamSpec, TypeVarTuple import pytest @@ -39,6 +40,7 @@ from mypy.stubutil import ( ClassInfo, common_dir_prefix, + generate_inline_generic, infer_method_ret_type, remove_misplaced_type_comments, walk_packages, @@ -612,6 +614,29 @@ def test_common_dir_prefix_win(self) -> None: assert common_dir_prefix([r"foo\bar/x.pyi"]) == r"foo\bar" assert common_dir_prefix([r"foo/bar/x.pyi"]) == r"foo\bar" + def test_generate_inline_generic(self) -> None: + assert generate_inline_generic(()) == "" + T = TypeVar("T") + assert generate_inline_generic((T,)) == "[T]" + TBound = TypeVar("TBound", bound=int) + assert generate_inline_generic((TBound,)) == "[TBound: int]" + TBoundTuple = TypeVar("TBoundTuple", int, str) + assert generate_inline_generic((TBoundTuple,)) == "[TBoundTuple: (int, str)]" + P = ParamSpec("P") + p_tuple = cast(Tuple[ParamSpec], (P,)) + assert generate_inline_generic(p_tuple) == "[**P]" + U = TypeVarTuple("U") + u_tuple = cast(Tuple[TypeVarTuple], (U,)) + assert generate_inline_generic(u_tuple) == "[*U]" + all_tuple = cast( + Tuple[TypeVar, TypeVar, TypeVar, ParamSpec, TypeVarTuple], + (T, TBound, TBoundTuple, P, U), + ) + assert ( + generate_inline_generic(all_tuple) + == "[T, TBound: int, TBoundTuple: (int, str), **P, *U]" + ) + class StubgenHelpersSuite(unittest.TestCase): def test_is_blacklisted_path(self) -> None: @@ -906,6 +931,62 @@ class TestClass(argparse.Action): assert_equal(output, ["class C(argparse.Action): ..."]) assert_equal(gen.get_imports().splitlines(), ["import argparse"]) + @unittest.skipIf(sys.version_info < (3, 12), "Inline Generics not supported before Python3.12") + def test_inline_generic_class(self) -> None: + T = TypeVar("T") + + class TestClass: + __type_params__ = (T,) + + output: list[str] = [] + mod = ModuleType("module", "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + gen.generate_class_stub("C", TestClass, output) + assert_equal(output, ["class C[T]: ..."]) + + @unittest.skipIf(sys.version_info < (3, 12), "Inline Generics not supported before Python3.12") + def test_generic_class(self) -> None: + # This class declaration lives in exec to avoid syntax version on python versions < 3.12 + local: dict[str, Any] = {} + exec("class Test[A]: ...", None, local) + + output: list[str] = [] + mod = ModuleType("module", "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + gen.generate_class_stub("C", local["Test"], output) + assert_equal(output, ["class C[A]: ..."]) + + @unittest.skipIf(sys.version_info < (3, 12), "Inline Generics not supported before Python3.12") + def test_inline_generic_function(self) -> None: + + if sys.version_info < ( + 3, + 12, + ): # Done to prevent mypy [attr-defined] error on __type_params__ in older versions of python + return + + T = TypeVar("T", bound=int) + + class TestClass: + def test(self, arg0: T) -> T: + """ + test(self, arg0: T) -> T + """ + return arg0 + + test.__type_params__ = (T,) + + output: list[str] = [] + mod = ModuleType(TestClass.__module__, "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + gen.generate_function_stub( + "test", + TestClass.test, + output=output, + class_info=ClassInfo(self_var="self", cls=TestClass, name="TestClass"), + ) + assert_equal(output, ["def test[T: int](self, arg0: T) -> T: ..."]) + def test_generate_c_type_inheritance_builtin_type(self) -> None: class TestClass(type): pass