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 Type Annotations from __annotations__ #17463

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
36 changes: 31 additions & 5 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
import sys
from types import FunctionType, ModuleType
from typing import Any, Callable, Mapping

Expand Down Expand Up @@ -241,7 +242,7 @@ def __init__(
self.module_name = module_name
if self.is_c_module:
# Add additional implicit imports.
# C-extensions are given more lattitude since they do not import the typing module.
# C-extensions are given more latitude since they do not import the typing module.
self.known_imports.update(
{
"typing": [
Expand Down Expand Up @@ -847,13 +848,28 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) ->
else:
attrs.append((attr, value))

# Gets annotations if they exist
if sys.version_info >= (3, 10):
annotations = inspect.get_annotations(cls)
else:
annotations = getattr(cls, "__annotations__", {})

for attr, value in attrs:
if attr == "__hash__" and value is None:
# special case for __hash__
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}] = ...")
if attr in annotations:
prop_type_name = self.strip_or_import(annotations[attr])
static_properties.append(f"{self._indent}{attr}: {prop_type_name} = ...")

if prop_type_name.startswith("ClassVar["):
self.add_name("typing.ClassVar")
else:
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()

Expand Down Expand Up @@ -893,7 +909,17 @@ def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> N
if self.is_private_name(name, f"{self.module_name}.{name}") or self.is_not_in_all(name):
return
self.record_name(name)
type_str = self.strip_or_import(self.get_type_annotation(obj))

# Gets annotations if they exist
if sys.version_info >= (3, 10):
annotations = inspect.get_annotations(self.module)
else:
annotations = getattr(self.module, "__annotations__", {})

if name in annotations:
type_str = self.strip_or_import(annotations[name])
else:
type_str = self.strip_or_import(self.get_type_annotation(obj))
output.append(f"{name}: {type_str}")


Expand Down
25 changes: 25 additions & 0 deletions mypy/test/teststubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,31 @@ def test(self, arg0: str) -> None:
assert_equal(output, ["def test(self, arg0: int) -> Any: ..."])
assert_equal(gen.get_imports().splitlines(), [])

def test_generate_c_class_fields_from__annotations__(self) -> None:
class TestClass:
__annotations__ = {"x": "dict[str, int]", "y": "ClassVar[dict[int, bool]]"}
x = {} # type:ignore [var-annotated]
y = {} # type:ignore [var-annotated]

output: list[str] = []
mod = ModuleType("module", "") # any module is fine
gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod)
gen.generate_class_stub("C", TestClass, output)
assert_equal(
output,
["class C:", " x: dict[str, int] = ...", " y: ClassVar[dict[int, bool]] = ..."],
)
assert_equal(gen.get_imports().splitlines(), ["from typing import ClassVar"])

def test_generate_c_module_fields_from__annotations__(self) -> None:
mod = ModuleType("module", "") # any module is fine
mod.__annotations__ = {"x": "dict[str, int]"}
mod.x = {} # type:ignore [attr-defined]
gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod)
gen.generate_module()
assert_equal(gen.output(), "x: dict[str, int]\n")
assert_equal(gen.get_imports().splitlines(), [])

def test_generate_c_type_classmethod(self) -> None:
class TestClass:
@classmethod
Expand Down
Loading