From 9a4c6e4f133e11ca2d4ce75f9ec89bb18b424ac5 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 7 Mar 2023 14:31:57 -0800 Subject: [PATCH] Fix #5197. --- .../apistub/nodes/_class_node.py | 12 +++++++----- .../tests/class_parsing_test.py | 19 ++++++++++++++++--- .../apistubgentest/models/__init__.py | 2 ++ .../apistubgentest/models/_models.py | 10 +++++++++- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/python-packages/api-stub-generator/apistub/nodes/_class_node.py b/packages/python-packages/api-stub-generator/apistub/nodes/_class_node.py index 1189df53c428..40cbd434f2b2 100644 --- a/packages/python-packages/api-stub-generator/apistub/nodes/_class_node.py +++ b/packages/python-packages/api-stub-generator/apistub/nodes/_class_node.py @@ -110,8 +110,7 @@ def _should_include_function(self, func_obj): return function_module and function_module.startswith(self.pkg_root_namespace) and not function_module.endswith("_model_base") return False - def _handle_class_variable(self, child_obj, name, *, type_string=None, value=None): - # Add any public class level variables + def _handle_variable(self, child_obj, name, *, type_string=None, value=None): allowed_types = (str, int, dict, list, float, bool) if not isinstance(child_obj, allowed_types): return @@ -124,6 +123,9 @@ def _handle_class_variable(self, child_obj, name, *, type_string=None, value=Non if type_string: var_match[0].type = type_string else: + is_ivar = True + if type_string: + is_ivar = not type_string.startswith("ClassVar") self.child_nodes.append( VariableNode( namespace=self.namespace, @@ -131,7 +133,7 @@ def _handle_class_variable(self, child_obj, name, *, type_string=None, value=Non name=name, type_name=type_string, value=value, - is_ivar=False + is_ivar=is_ivar ) ) @@ -222,7 +224,7 @@ def _inspect(self): ) else: type_string = get_qualified_name(item_type, self.namespace) - self._handle_class_variable(child_obj, item_name, type_string=type_string) + self._handle_variable(child_obj, item_name, type_string=type_string) # now that we've looked at the specific dunder properties we are # willing to include, anything with a leading underscore should be ignored. @@ -253,7 +255,7 @@ def _inspect(self): # Add instance properties self.child_nodes.append(PropertyNode(self.namespace, self, name, child_obj)) else: - self._handle_class_variable(child_obj, name, value=str(child_obj)) + self._handle_variable(child_obj, name, value=str(child_obj)) def _parse_ivars(self): # This method will add instance variables by parsing docstring diff --git a/packages/python-packages/api-stub-generator/tests/class_parsing_test.py b/packages/python-packages/api-stub-generator/tests/class_parsing_test.py index b96fddcc38b3..07f24b3cecae 100644 --- a/packages/python-packages/api-stub-generator/tests/class_parsing_test.py +++ b/packages/python-packages/api-stub-generator/tests/class_parsing_test.py @@ -10,6 +10,7 @@ AliasNewType, AliasUnion, ClassWithDecorators, + ClassWithIvarsAndCvars, FakeTypedDict, FakeObject, GenericStack, @@ -40,6 +41,18 @@ class TestClassParsing: pkg_namespace = "apistubgentest.models" + def test_class_with_ivars_and_cvars(self): + obj = ClassWithIvarsAndCvars + class_node = ClassNode(name=obj.__name__, namespace=obj.__name__, parent_node=None, obj=obj, pkg_root_namespace=self.pkg_namespace) + actuals = _render_lines(_tokenize(class_node)) + expected = [ + "class ClassWithIvarsAndCvars:", + 'ivar captain: str = "Picard"', + "ivar damage: int", + "cvar stats: ClassVar[Dict[str, int]] = {}" + ] + _check_all(actuals, expected, obj) + def test_class_with_decorators(self): obj = ClassWithDecorators class_node = ClassNode(name=obj.__name__, namespace=obj.__name__, parent_node=None, obj=obj, pkg_root_namespace=self.pkg_namespace) @@ -75,7 +88,7 @@ def test_object(self): actuals = _render_lines(_tokenize(class_node)) expected = [ "class FakeObject:", - 'cvar PUBLIC_CONST: str = "SOMETHING"', + 'ivar PUBLIC_CONST: str = "SOMETHING"', 'ivar age: int', 'ivar name: str', 'ivar union: Union[bool, PetEnumPy3MetaclassAlt]' @@ -90,8 +103,8 @@ def test_public_private(self): actuals = _render_lines(_tokenize(class_node)) expected = [ "class PublicPrivateClass:", - "cvar public_dict: dict = {'a': 'b'}", - 'cvar public_var: str = "SOMEVAL"', + "ivar public_dict: dict = {'a': 'b'}", + 'ivar public_var: str = "SOMEVAL"', ] _check_all(actuals, expected, obj) assert actuals[4].lstrip() == "def __init__(self)" diff --git a/packages/python-packages/apistubgentest/apistubgentest/models/__init__.py b/packages/python-packages/apistubgentest/apistubgentest/models/__init__.py index 32df3cef3dc8..6fcf0f263b3d 100644 --- a/packages/python-packages/apistubgentest/apistubgentest/models/__init__.py +++ b/packages/python-packages/apistubgentest/apistubgentest/models/__init__.py @@ -11,6 +11,7 @@ AliasNewType, AliasUnion, ClassWithDecorators, + ClassWithIvarsAndCvars, DocstringClass, FakeError, FakeObject, @@ -41,6 +42,7 @@ "AliasNewType", "AliasUnion", "ClassWithDecorators", + "ClassWithIvarsAndCvars", "DataClassSimple", "DataClassWithFields", "DataClassDynamic", diff --git a/packages/python-packages/apistubgentest/apistubgentest/models/_models.py b/packages/python-packages/apistubgentest/apistubgentest/models/_models.py index 74ea0e5e99d5..414a844c1d33 100644 --- a/packages/python-packages/apistubgentest/apistubgentest/models/_models.py +++ b/packages/python-packages/apistubgentest/apistubgentest/models/_models.py @@ -11,7 +11,7 @@ from collections.abc import Sequence from enum import Enum, EnumMeta import functools -from typing import Any, overload, Dict, TypedDict, Union, Optional, Generic, TypeVar, NewType, TypeAlias +from typing import Any, overload, Dict, TypedDict, Union, Optional, Generic, TypeVar, NewType, ClassVar from ._mixin import MixinWithOverloads @@ -45,10 +45,18 @@ def __init__(self, id, *args, **kwargs): cls.__init__ = __init__ return cls + @add_id class ClassWithDecorators: pass + +class ClassWithIvarsAndCvars: + captain: str = "Picard" # instance var w/ default + damage: int # instance var w/out default + stats: ClassVar[Dict[str, int]] = {} # class var + + class PublicCaseInsensitiveEnumMeta(EnumMeta): def __getitem__(self, name: str): pass