Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stubgenc] Generate Inline generics from __type_params__ #17559

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
95b5ed8
testing for classes
InvincibleRMC Jul 22, 2024
1ed6c91
works for classes
InvincibleRMC Jul 22, 2024
1a197f7
added support for inline functions
InvincibleRMC Jul 22, 2024
4725cbf
Merge branch 'master' into generate-inline-generic
InvincibleRMC Jul 22, 2024
5b631cc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 22, 2024
7e07a99
add version guard
InvincibleRMC Jul 22, 2024
a2d2328
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 22, 2024
f351cdb
use version_info over version
InvincibleRMC Jul 22, 2024
82ff3c2
if versino check inside function
InvincibleRMC Jul 22, 2024
e2d5aa8
use python3.8 Tuple
InvincibleRMC Jul 22, 2024
1521c90
Add syntax validation
InvincibleRMC Jul 22, 2024
8426c40
Merge branch 'master' into generate-inline-generic
InvincibleRMC Jul 22, 2024
d0b6f2e
Add generic for class with properties
InvincibleRMC Jul 22, 2024
b4e40b5
Add rest of test case
InvincibleRMC Jul 23, 2024
e5ef50a
add mypy guard
InvincibleRMC Jul 23, 2024
7004be5
use type ignore
InvincibleRMC Jul 23, 2024
c4467bc
fix type: ignore
InvincibleRMC Jul 23, 2024
893cd54
more general?
InvincibleRMC Jul 23, 2024
0bd3c1e
no_type_check
InvincibleRMC Jul 23, 2024
23c1a30
add enable
InvincibleRMC Jul 23, 2024
74027b7
use exec
InvincibleRMC Jul 23, 2024
fb0e698
fix grammar
InvincibleRMC Jul 23, 2024
b889170
simpler exec
InvincibleRMC Jul 23, 2024
3c943dc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 23, 2024
53a5979
pass in None
InvincibleRMC Jul 23, 2024
f4cfbc8
Merge branch 'master' into generate-inline-generic
InvincibleRMC Jul 25, 2024
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
10 changes: 8 additions & 2 deletions mypy/stubdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)}"
Expand Down
30 changes: 27 additions & 3 deletions mypy/stubgenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -33,6 +34,7 @@
ClassInfo,
FunctionContext,
SignatureGenerator,
generate_inline_generic,
infer_method_arg_types,
infer_method_ret_type,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -847,23 +861,33 @@ 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}] = ...")

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
Expand All @@ -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.
Expand Down
40 changes: 38 additions & 2 deletions mypy/stubutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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}]"
83 changes: 82 additions & 1 deletion mypy/test/teststubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
InvincibleRMC marked this conversation as resolved.
Show resolved Hide resolved
__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
Expand Down
Loading