From dcd8d4ede2551c47700fae338580eeb8c413a212 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Tue, 5 Jan 2021 03:37:02 +0300 Subject: [PATCH 1/9] [stubgenc] Render classes before functions Functions may depend on class definitions, therefore should go after classes to not confuse some static analysis tools. A better solution (although way complex) would be a topological sort of all module elements (including variables) --- mypy/stubgenc.py | 8 ++++---- test-data/stubgen/pybind11_mypy_demo/basics.pyi | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index 84d064cc3449..1e6c905d1028 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -78,14 +78,14 @@ def generate_stub_for_c_module(module_name: str, output.append(line) for line in variables: output.append(line) - if output and functions: - output.append('') - for line in functions: - output.append(line) for line in types: if line.startswith('class') and output and output[-1]: output.append('') output.append(line) + if output and functions: + output.append('') + for line in functions: + output.append(line) output = add_typing_import(output) with open(target, 'w') as file: for line in output: diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index ec1b4fcef771..13c413aa774e 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -3,11 +3,6 @@ from typing import Any from typing import overload PI: float -def answer() -> int: ... -def midpoint(left: float, right: float) -> float: ... -def sum(arg0: int, arg1: int) -> int: ... -def weighted_midpoint(left: float, right: float, alpha: float = ...) -> float: ... - class Point: AngleUnit: Any = ... LengthUnit: Any = ... @@ -46,3 +41,8 @@ class Point: def y(self, val: float) -> None: ... @property def y_axis(self) -> pybind11_mypy_demo.basics.Point: ... + +def answer() -> int: ... +def midpoint(left: float, right: float) -> float: ... +def sum(arg0: int, arg1: int) -> int: ... +def weighted_midpoint(left: float, right: float, alpha: float = ...) -> float: ... From 40bc1f255a3f8c55ce406a86b39e9382af21f024 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Tue, 5 Jan 2021 00:00:49 +0300 Subject: [PATCH 2/9] [stubgenc] Show exact types for module attributes --- mypy/stubgenc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index 1e6c905d1028..9efd6d55eac1 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -69,9 +69,7 @@ def generate_stub_for_c_module(module_name: str, if name.startswith('__') and name.endswith('__'): continue if name not in done and not inspect.ismodule(obj): - type_str = type(obj).__name__ - if type_str not in ('int', 'str', 'bytes', 'float', 'bool'): - type_str = 'Any' + type_str = strip_or_import(type(obj).__name__, module, imports) variables.append('%s: %s' % (name, type_str)) output = [] for line in sorted(set(imports)): From e6b5618e2b33c3fe2f20dad9334c23c3e6205975 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Tue, 5 Jan 2021 00:10:06 +0300 Subject: [PATCH 3/9] [stubgenc] Show exact types for class attributes --- mypy/stubgenc.py | 3 ++- test-data/stubgen/pybind11_mypy_demo/basics.pyi | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index 9efd6d55eac1..da3adc1d43ef 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -311,7 +311,8 @@ def generate_c_type_stub(module: ModuleType, if is_skipped_attribute(attr): continue if attr not in done: - variables.append('%s: Any = ...' % attr) + variables.append('%s: %s = ...' % ( + attr, strip_or_import(type(value).__name__, module, imports))) all_bases = obj.mro() if all_bases[-1] is object: # TODO: Is this always object? diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index 13c413aa774e..325496d68d07 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -4,9 +4,9 @@ from typing import overload PI: float class Point: - AngleUnit: Any = ... - LengthUnit: Any = ... - origin: Any = ... + AngleUnit: pybind11_type = ... + LengthUnit: pybind11_type = ... + origin: Point = ... @overload def __init__(self) -> None: ... @overload From 9d49fd8c65d5bffe7b584c7483f7acf8ec82e097 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Mon, 4 Jan 2021 23:53:14 +0300 Subject: [PATCH 4/9] [stubgenc] Shorten property return types (add necessary imports if needed) --- mypy/stubgenc.py | 11 +++++++++-- test-data/stubgen/pybind11_mypy_demo/basics.pyi | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index da3adc1d43ef..d2d4cd911aad 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -234,11 +234,14 @@ def strip_or_import(typ: str, module: ModuleType, imports: List[str]) -> str: return stripped_type -def generate_c_property_stub(name: str, obj: object, output: List[str], readonly: bool) -> None: +def generate_c_property_stub(name: str, obj: object, output: List[str], readonly: bool, + module: Optional[ModuleType] = None, + imports: Optional[List[str]] = None) -> None: """Generate property stub using introspection of 'obj'. Try to infer type from docstring, append resulting lines to 'output'. """ + def infer_prop_type(docstr: Optional[str]) -> Optional[str]: """Infer property type from docstring or docstring signature.""" if docstr is not None: @@ -256,6 +259,9 @@ def infer_prop_type(docstr: Optional[str]) -> Optional[str]: if not inferred: inferred = 'Any' + if module is not None and imports is not None: + inferred = strip_or_import(inferred, module, imports) + output.append('@property') output.append('def {}(self) -> {}: ...'.format(name, inferred)) if not readonly: @@ -304,7 +310,8 @@ def generate_c_type_stub(module: ModuleType, class_sigs=class_sigs) elif is_c_property(value): done.add(attr) - generate_c_property_stub(attr, value, properties, is_c_property_readonly(value)) + generate_c_property_stub(attr, value, properties, is_c_property_readonly(value), + module=module, imports=imports) variables = [] for attr, value in items: diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index 325496d68d07..89fe242b9978 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -20,27 +20,27 @@ class Point: @overload def distance_to(*args, **kwargs) -> Any: ... @property - def angle_unit(self) -> pybind11_mypy_demo.basics.Point.AngleUnit: ... + def angle_unit(self) -> Point.AngleUnit: ... @angle_unit.setter - def angle_unit(self, val: pybind11_mypy_demo.basics.Point.AngleUnit) -> None: ... + def angle_unit(self, val: Point.AngleUnit) -> None: ... @property def length(self) -> float: ... @property - def length_unit(self) -> pybind11_mypy_demo.basics.Point.LengthUnit: ... + def length_unit(self) -> Point.LengthUnit: ... @length_unit.setter - def length_unit(self, val: pybind11_mypy_demo.basics.Point.LengthUnit) -> None: ... + def length_unit(self, val: Point.LengthUnit) -> None: ... @property def x(self) -> float: ... @x.setter def x(self, val: float) -> None: ... @property - def x_axis(self) -> pybind11_mypy_demo.basics.Point: ... + def x_axis(self) -> Point: ... @property def y(self) -> float: ... @y.setter def y(self, val: float) -> None: ... @property - def y_axis(self) -> pybind11_mypy_demo.basics.Point: ... + def y_axis(self) -> Point: ... def answer() -> int: ... def midpoint(left: float, right: float) -> float: ... From 7752e90979762f2bd50be6cd0395cdbb926aa0bc Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Mon, 4 Jan 2021 22:13:07 +0300 Subject: [PATCH 5/9] [stubgenc] Support nested classes --- mypy/stubgenc.py | 18 +++++++-- mypy/test/teststubgen.py | 16 ++++---- .../stubgen/pybind11_mypy_demo/basics.pyi | 40 ++++++++++++++++++- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index d2d4cd911aad..34af8abfe609 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -69,7 +69,7 @@ def generate_stub_for_c_module(module_name: str, if name.startswith('__') and name.endswith('__'): continue if name not in done and not inspect.ismodule(obj): - type_str = strip_or_import(type(obj).__name__, module, imports) + type_str = strip_or_import(get_type_fullname(type(obj)), module, imports) variables.append('%s: %s' % (name, type_str)) output = [] for line in sorted(set(imports)): @@ -286,6 +286,7 @@ def generate_c_type_stub(module: ModuleType, obj_dict = getattr(obj, '__dict__') # type: Mapping[str, Any] # noqa items = sorted(obj_dict.items(), key=lambda x: method_name_sort_key(x[0])) methods = [] # type: List[str] + types = [] # type: List[str] properties = [] # type: List[str] done = set() # type: Set[str] for attr, value in items: @@ -312,6 +313,10 @@ def generate_c_type_stub(module: ModuleType, done.add(attr) generate_c_property_stub(attr, value, properties, is_c_property_readonly(value), module=module, imports=imports) + elif is_c_type(value): + generate_c_type_stub(module, attr, value, types, imports=imports, sigs=sigs, + class_sigs=class_sigs) + done.add(attr) variables = [] for attr, value in items: @@ -319,7 +324,7 @@ def generate_c_type_stub(module: ModuleType, continue if attr not in done: variables.append('%s: %s = ...' % ( - attr, strip_or_import(type(value).__name__, module, imports))) + attr, strip_or_import(get_type_fullname(type(value)), module, imports))) all_bases = obj.mro() if all_bases[-1] is object: # TODO: Is this always object? @@ -345,10 +350,15 @@ def generate_c_type_stub(module: ModuleType, ) else: bases_str = '' - if not methods and not variables and not properties: + if not methods and not variables and not properties and not types: output.append('class %s%s: ...' % (class_name, bases_str)) else: output.append('class %s%s:' % (class_name, bases_str)) + for line in types: + if output and output[-1] and \ + not output[-1].startswith('class') and line.startswith('class'): + output.append('') + output.append(' ' + line) for variable in variables: output.append(' %s' % variable) for method in methods: @@ -358,7 +368,7 @@ def generate_c_type_stub(module: ModuleType, def get_type_fullname(typ: type) -> str: - return '%s.%s' % (typ.__module__, typ.__name__) + return '%s.%s' % (typ.__module__, getattr(typ, '__qualname__', typ.__name__)) def method_name_sort_key(name: str) -> Tuple[int, str]: diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index 5d62a1af521c..0c4a2713f690 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -625,6 +625,14 @@ def add_file(self, path: str, result: List[str], header: bool) -> None: self_arg = ArgSig(name='self') +class TestBaseClass: + pass + + +class TestClass(TestBaseClass): + pass + + class StubgencSuite(unittest.TestCase): """Unit tests for stub generation from C modules using introspection. @@ -668,7 +676,7 @@ class TestClassVariableCls: mod = ModuleType('module', '') # any module is fine generate_c_type_stub(mod, 'C', TestClassVariableCls, output, imports) assert_equal(imports, []) - assert_equal(output, ['class C:', ' x: Any = ...']) + assert_equal(output, ['class C:', ' x: int = ...']) def test_generate_c_type_inheritance(self) -> None: class TestClass(KeyError): @@ -682,12 +690,6 @@ class TestClass(KeyError): assert_equal(imports, []) def test_generate_c_type_inheritance_same_module(self) -> None: - class TestBaseClass: - pass - - class TestClass(TestBaseClass): - pass - output = [] # type: List[str] imports = [] # type: List[str] mod = ModuleType(TestBaseClass.__module__, '') diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index 89fe242b9978..d6f7eea84d5e 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -4,8 +4,44 @@ from typing import overload PI: float class Point: - AngleUnit: pybind11_type = ... - LengthUnit: pybind11_type = ... + class AngleUnit: + __entries: dict = ... + degree: Point.AngleUnit = ... + radian: Point.AngleUnit = ... + def __init__(self, value: int) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __int__(self) -> int: ... + def __ne__(self, other: object) -> bool: ... + def __setstate__(self, state: int) -> None: ... + @property + def name(self) -> Any: ... + @property + def __doc__(self) -> Any: ... + @property + def __members__(self) -> Any: ... + + class LengthUnit: + __entries: dict = ... + inch: Point.LengthUnit = ... + mm: Point.LengthUnit = ... + pixel: Point.LengthUnit = ... + def __init__(self, value: int) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __int__(self) -> int: ... + def __ne__(self, other: object) -> bool: ... + def __setstate__(self, state: int) -> None: ... + @property + def name(self) -> Any: ... + @property + def __doc__(self) -> Any: ... + @property + def __members__(self) -> Any: ... origin: Point = ... @overload def __init__(self) -> None: ... From 9a473860adfeebddd163ee1885cb94b82a92b2e6 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Wed, 13 Jan 2021 13:34:56 +0300 Subject: [PATCH 6/9] [stubgenc] Don't render overloaded function umbrella (*args, **kwargs) signature Overloaded function header in pybind11 was erroneously recognized as an extra overload. --- mypy/stubgenc.py | 8 ++++++++ test-data/stubgen/pybind11_mypy_demo/basics.pyi | 4 ---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index 34af8abfe609..f1403a80deae 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -129,6 +129,11 @@ def is_c_type(obj: object) -> bool: return inspect.isclass(obj) or type(obj) is type(int) +def is_pybind11_overloaded_function_docstring(docstr: str, name: str) -> bool: + return docstr.startswith("{}(*args, **kwargs)\n".format(name) + + "Overloaded function.\n\n") + + def generate_c_function_stub(module: ModuleType, name: str, obj: object, @@ -160,6 +165,9 @@ def generate_c_function_stub(module: ModuleType, else: docstr = getattr(obj, '__doc__', None) inferred = infer_sig_from_docstring(docstr, name) + if inferred and is_pybind11_overloaded_function_docstring(docstr, name): + # Remove pybind11 umbrella (*args, **kwargs) for overloaded functions + del inferred[-1] if not inferred: if class_name and name not in sigs: inferred = [FunctionSig(name, args=infer_method_sig(name), ret_type=ret_type)] diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index d6f7eea84d5e..936cee9a52f6 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -48,13 +48,9 @@ class Point: @overload def __init__(self, x: float, y: float) -> None: ... @overload - def __init__(*args, **kwargs) -> Any: ... - @overload def distance_to(self, x: float, y: float) -> float: ... @overload def distance_to(self, other: Point) -> float: ... - @overload - def distance_to(*args, **kwargs) -> Any: ... @property def angle_unit(self) -> Point.AngleUnit: ... @angle_unit.setter From 1d23d00b4b8356db882fefc21ef65d7a955b33cf Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Tue, 5 Jan 2021 14:28:55 +0300 Subject: [PATCH 7/9] [stubgenc] Recognize pybind property return types Pybind includes function name in the property signature in docstrings --- mypy/stubdoc.py | 11 ++++++++--- mypy/stubgenc.py | 5 ++++- test-data/stubgen/pybind11_mypy_demo/basics.pyi | 14 ++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/mypy/stubdoc.py b/mypy/stubdoc.py index 0b5b21e81a0f..801e661440d2 100644 --- a/mypy/stubdoc.py +++ b/mypy/stubdoc.py @@ -248,14 +248,19 @@ def infer_arg_sig_from_anon_docstring(docstr: str) -> List[ArgSig]: return [] -def infer_ret_type_sig_from_anon_docstring(docstr: str) -> Optional[str]: - """Convert signature in form of "(self: TestClass, arg0) -> int" to their return type.""" - ret = infer_sig_from_docstring("stub" + docstr.strip(), "stub") +def infer_ret_type_sig_from_docstring(docstr: str, name: str) -> Optional[str]: + """Convert signature in form of "func(self: TestClass, arg0) -> int" to their return type.""" + ret = infer_sig_from_docstring(docstr, name) if ret: return ret[0].ret_type return None +def infer_ret_type_sig_from_anon_docstring(docstr: str) -> Optional[str]: + """Convert signature in form of "(self: TestClass, arg0) -> int" to their return type.""" + return infer_ret_type_sig_from_docstring("stub" + docstr.strip(), "stub") + + def parse_signature(sig: str) -> Optional[Tuple[str, List[str], List[str]]]: diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index f1403a80deae..ac1e612ba640 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -14,7 +14,8 @@ from mypy.moduleinspect import is_c_module from mypy.stubdoc import ( infer_sig_from_docstring, infer_prop_type_from_docstring, ArgSig, - infer_arg_sig_from_anon_docstring, infer_ret_type_sig_from_anon_docstring, FunctionSig + infer_arg_sig_from_anon_docstring, infer_ret_type_sig_from_anon_docstring, + infer_ret_type_sig_from_docstring, FunctionSig ) # Members of the typing module to consider for importing by default. @@ -254,6 +255,8 @@ def infer_prop_type(docstr: Optional[str]) -> Optional[str]: """Infer property type from docstring or docstring signature.""" if docstr is not None: inferred = infer_ret_type_sig_from_anon_docstring(docstr) + if not inferred: + inferred = infer_ret_type_sig_from_docstring(docstr, name) if not inferred: inferred = infer_prop_type_from_docstring(docstr) return inferred diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index 936cee9a52f6..eb948d20f0bd 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -1,5 +1,3 @@ -from typing import Any - from typing import overload PI: float @@ -17,11 +15,11 @@ class Point: def __ne__(self, other: object) -> bool: ... def __setstate__(self, state: int) -> None: ... @property - def name(self) -> Any: ... + def name(self) -> str: ... @property - def __doc__(self) -> Any: ... + def __doc__(self) -> str: ... @property - def __members__(self) -> Any: ... + def __members__(self) -> dict: ... class LengthUnit: __entries: dict = ... @@ -37,11 +35,11 @@ class Point: def __ne__(self, other: object) -> bool: ... def __setstate__(self, state: int) -> None: ... @property - def name(self) -> Any: ... + def name(self) -> str: ... @property - def __doc__(self) -> Any: ... + def __doc__(self) -> str: ... @property - def __members__(self) -> Any: ... + def __members__(self) -> dict: ... origin: Point = ... @overload def __init__(self) -> None: ... From 45cee685a48883620e03673502c6013fa151c8d2 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Wed, 13 Jan 2021 20:27:03 +0300 Subject: [PATCH 8/9] [stubgenc] Recognize pybind11 static properties --- mypy/stubgenc.py | 50 ++++++++++++------- mypy/test/teststubgen.py | 4 +- .../stubgen/pybind11_mypy_demo/basics.pyi | 46 +++++++---------- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index ac1e612ba640..d6345a2487d6 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -22,6 +22,7 @@ _DEFAULT_TYPING_IMPORTS = ( 'Any', 'Callable', + 'ClassVar', 'Dict', 'Iterable', 'Iterator', @@ -243,7 +244,13 @@ def strip_or_import(typ: str, module: ModuleType, imports: List[str]) -> str: return stripped_type -def generate_c_property_stub(name: str, obj: object, output: List[str], readonly: bool, +def is_static_property(obj: object) -> bool: + return type(obj).__name__ == 'pybind11_static_property' + + +def generate_c_property_stub(name: str, obj: object, + static_properties: List[str], + properties: List[str], readonly: bool, module: Optional[ModuleType] = None, imports: Optional[List[str]] = None) -> None: """Generate property stub using introspection of 'obj'. @@ -273,11 +280,17 @@ def infer_prop_type(docstr: Optional[str]) -> Optional[str]: if module is not None and imports is not None: inferred = strip_or_import(inferred, module, imports) - output.append('@property') - output.append('def {}(self) -> {}: ...'.format(name, inferred)) - if not readonly: - output.append('@{}.setter'.format(name)) - output.append('def {}(self, val: {}) -> None: ...'.format(name, inferred)) + if is_static_property(obj): + trailing_comment = " # read-only" if readonly else "" + static_properties.append( + '{}: ClassVar[{}] = ...{}'.format(name, inferred, trailing_comment) + ) + else: # regular property + properties.append('@property') + properties.append('def {}(self) -> {}: ...'.format(name, inferred)) + if not readonly: + properties.append('@{}.setter'.format(name)) + properties.append('def {}(self, val: {}) -> None: ...'.format(name, inferred)) def generate_c_type_stub(module: ModuleType, @@ -298,6 +311,7 @@ def generate_c_type_stub(module: ModuleType, items = sorted(obj_dict.items(), key=lambda x: method_name_sort_key(x[0])) methods = [] # type: List[str] types = [] # type: List[str] + static_properties = [] # type: List[str] properties = [] # type: List[str] done = set() # type: Set[str] for attr, value in items: @@ -322,19 +336,19 @@ def generate_c_type_stub(module: ModuleType, class_sigs=class_sigs) elif is_c_property(value): done.add(attr) - generate_c_property_stub(attr, value, properties, is_c_property_readonly(value), + generate_c_property_stub(attr, value, static_properties, properties, + is_c_property_readonly(value), module=module, imports=imports) elif is_c_type(value): generate_c_type_stub(module, attr, value, types, imports=imports, sigs=sigs, class_sigs=class_sigs) done.add(attr) - variables = [] for attr, value in items: if is_skipped_attribute(attr): continue if attr not in done: - variables.append('%s: %s = ...' % ( + static_properties.append('%s: ClassVar[%s] = ...' % ( attr, strip_or_import(get_type_fullname(type(value)), module, imports))) all_bases = obj.mro() if all_bases[-1] is object: @@ -361,21 +375,21 @@ def generate_c_type_stub(module: ModuleType, ) else: bases_str = '' - if not methods and not variables and not properties and not types: - output.append('class %s%s: ...' % (class_name, bases_str)) - else: + if types or static_properties or methods or properties: output.append('class %s%s:' % (class_name, bases_str)) for line in types: if output and output[-1] and \ not output[-1].startswith('class') and line.startswith('class'): output.append('') output.append(' ' + line) - for variable in variables: - output.append(' %s' % variable) - for method in methods: - output.append(' %s' % method) - for prop in properties: - output.append(' %s' % prop) + for line in static_properties: + output.append(' %s' % line) + for line in methods: + output.append(' %s' % line) + for line in properties: + output.append(' %s' % line) + else: + output.append('class %s%s: ...' % (class_name, bases_str)) def get_type_fullname(typ: type) -> str: diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index 0c4a2713f690..0bd1d1053a4a 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -676,7 +676,7 @@ class TestClassVariableCls: mod = ModuleType('module', '') # any module is fine generate_c_type_stub(mod, 'C', TestClassVariableCls, output, imports) assert_equal(imports, []) - assert_equal(output, ['class C:', ' x: int = ...']) + assert_equal(output, ['class C:', ' x: ClassVar[int] = ...']) def test_generate_c_type_inheritance(self) -> None: class TestClass(KeyError): @@ -815,7 +815,7 @@ def get_attribute(self) -> None: attribute = property(get_attribute, doc="") output = [] # type: List[str] - generate_c_property_stub('attribute', TestClass.attribute, output, readonly=True) + generate_c_property_stub('attribute', TestClass.attribute, [], output, readonly=True) assert_equal(output, ['@property', 'def attribute(self) -> str: ...']) def test_generate_c_type_with_single_arg_generic(self) -> None: diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index eb948d20f0bd..ebfede7c3038 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -1,11 +1,15 @@ +from typing import ClassVar + from typing import overload PI: float class Point: class AngleUnit: - __entries: dict = ... - degree: Point.AngleUnit = ... - radian: Point.AngleUnit = ... + __doc__: ClassVar[str] = ... # read-only + __members__: ClassVar[dict] = ... # read-only + __entries: ClassVar[dict] = ... + degree: ClassVar[Point.AngleUnit] = ... + radian: ClassVar[Point.AngleUnit] = ... def __init__(self, value: int) -> None: ... def __eq__(self, other: object) -> bool: ... def __getstate__(self) -> int: ... @@ -16,16 +20,14 @@ class Point: def __setstate__(self, state: int) -> None: ... @property def name(self) -> str: ... - @property - def __doc__(self) -> str: ... - @property - def __members__(self) -> dict: ... class LengthUnit: - __entries: dict = ... - inch: Point.LengthUnit = ... - mm: Point.LengthUnit = ... - pixel: Point.LengthUnit = ... + __doc__: ClassVar[str] = ... # read-only + __members__: ClassVar[dict] = ... # read-only + __entries: ClassVar[dict] = ... + inch: ClassVar[Point.LengthUnit] = ... + mm: ClassVar[Point.LengthUnit] = ... + pixel: ClassVar[Point.LengthUnit] = ... def __init__(self, value: int) -> None: ... def __eq__(self, other: object) -> bool: ... def __getstate__(self) -> int: ... @@ -36,11 +38,11 @@ class Point: def __setstate__(self, state: int) -> None: ... @property def name(self) -> str: ... - @property - def __doc__(self) -> str: ... - @property - def __members__(self) -> dict: ... - origin: Point = ... + angle_unit: ClassVar[Point.AngleUnit] = ... + length_unit: ClassVar[Point.LengthUnit] = ... + x_axis: ClassVar[Point] = ... # read-only + y_axis: ClassVar[Point] = ... # read-only + origin: ClassVar[Point] = ... @overload def __init__(self) -> None: ... @overload @@ -50,27 +52,15 @@ class Point: @overload def distance_to(self, other: Point) -> float: ... @property - def angle_unit(self) -> Point.AngleUnit: ... - @angle_unit.setter - def angle_unit(self, val: Point.AngleUnit) -> None: ... - @property def length(self) -> float: ... @property - def length_unit(self) -> Point.LengthUnit: ... - @length_unit.setter - def length_unit(self, val: Point.LengthUnit) -> None: ... - @property def x(self) -> float: ... @x.setter def x(self, val: float) -> None: ... @property - def x_axis(self) -> Point: ... - @property def y(self) -> float: ... @y.setter def y(self, val: float) -> None: ... - @property - def y_axis(self) -> Point: ... def answer() -> int: ... def midpoint(left: float, right: float) -> float: ... From 951fc3748d3a1ad2bc7142e65977c5fb372852db Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Wed, 13 Jan 2021 20:31:57 +0300 Subject: [PATCH 9/9] [stubgenc] Render rw-properties as annotated class attributes --- mypy/stubgenc.py | 24 +++++++++++-------- mypy/test/teststubgen.py | 2 +- .../stubgen/pybind11_mypy_demo/basics.pyi | 10 ++------ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index d6345a2487d6..a0a783bed32f 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -250,7 +250,8 @@ def is_static_property(obj: object) -> bool: def generate_c_property_stub(name: str, obj: object, static_properties: List[str], - properties: List[str], readonly: bool, + rw_properties: List[str], + ro_properties: List[str], readonly: bool, module: Optional[ModuleType] = None, imports: Optional[List[str]] = None) -> None: """Generate property stub using introspection of 'obj'. @@ -286,11 +287,11 @@ def infer_prop_type(docstr: Optional[str]) -> Optional[str]: '{}: ClassVar[{}] = ...{}'.format(name, inferred, trailing_comment) ) else: # regular property - properties.append('@property') - properties.append('def {}(self) -> {}: ...'.format(name, inferred)) - if not readonly: - properties.append('@{}.setter'.format(name)) - properties.append('def {}(self, val: {}) -> None: ...'.format(name, inferred)) + if readonly: + ro_properties.append('@property') + ro_properties.append('def {}(self) -> {}: ...'.format(name, inferred)) + else: + rw_properties.append('{}: {}'.format(name, inferred)) def generate_c_type_stub(module: ModuleType, @@ -312,7 +313,8 @@ def generate_c_type_stub(module: ModuleType, methods = [] # type: List[str] types = [] # type: List[str] static_properties = [] # type: List[str] - properties = [] # type: List[str] + rw_properties = [] # type: List[str] + ro_properties = [] # type: List[str] done = set() # type: Set[str] for attr, value in items: if is_c_method(value) or is_c_classmethod(value): @@ -336,7 +338,7 @@ def generate_c_type_stub(module: ModuleType, class_sigs=class_sigs) elif is_c_property(value): done.add(attr) - generate_c_property_stub(attr, value, static_properties, properties, + generate_c_property_stub(attr, value, static_properties, rw_properties, ro_properties, is_c_property_readonly(value), module=module, imports=imports) elif is_c_type(value): @@ -375,7 +377,7 @@ def generate_c_type_stub(module: ModuleType, ) else: bases_str = '' - if types or static_properties or methods or properties: + if types or static_properties or rw_properties or methods or ro_properties: output.append('class %s%s:' % (class_name, bases_str)) for line in types: if output and output[-1] and \ @@ -384,9 +386,11 @@ def generate_c_type_stub(module: ModuleType, output.append(' ' + line) for line in static_properties: output.append(' %s' % line) + for line in rw_properties: + output.append(' %s' % line) for line in methods: output.append(' %s' % line) - for line in properties: + for line in ro_properties: output.append(' %s' % line) else: output.append('class %s%s: ...' % (class_name, bases_str)) diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index 0bd1d1053a4a..981035b0892b 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -815,7 +815,7 @@ def get_attribute(self) -> None: attribute = property(get_attribute, doc="") output = [] # type: List[str] - generate_c_property_stub('attribute', TestClass.attribute, [], output, readonly=True) + generate_c_property_stub('attribute', TestClass.attribute, [], [], output, readonly=True) assert_equal(output, ['@property', 'def attribute(self) -> str: ...']) def test_generate_c_type_with_single_arg_generic(self) -> None: diff --git a/test-data/stubgen/pybind11_mypy_demo/basics.pyi b/test-data/stubgen/pybind11_mypy_demo/basics.pyi index ebfede7c3038..7c83f4ad2256 100644 --- a/test-data/stubgen/pybind11_mypy_demo/basics.pyi +++ b/test-data/stubgen/pybind11_mypy_demo/basics.pyi @@ -43,6 +43,8 @@ class Point: x_axis: ClassVar[Point] = ... # read-only y_axis: ClassVar[Point] = ... # read-only origin: ClassVar[Point] = ... + x: float + y: float @overload def __init__(self) -> None: ... @overload @@ -53,14 +55,6 @@ class Point: def distance_to(self, other: Point) -> float: ... @property def length(self) -> float: ... - @property - def x(self) -> float: ... - @x.setter - def x(self, val: float) -> None: ... - @property - def y(self) -> float: ... - @y.setter - def y(self, val: float) -> None: ... def answer() -> int: ... def midpoint(left: float, right: float) -> float: ...