Skip to content

Commit

Permalink
Fixing a few bugs and replacing some raises with warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
Masara committed Aug 18, 2024
1 parent f53d93b commit 84c7237
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 38 deletions.
48 changes: 38 additions & 10 deletions src/safeds_stubgen/api_analyzer/_ast_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None:
variance_type = mypy_variance_parser(generic_type.variance)
variance_values: sds_types.AbstractType | None = None
if variance_type == VarianceKind.INVARIANT:
values = [self.mypy_type_to_abstract_type(value) for value in generic_type.values]
values = []
if hasattr(generic_type, "values"):
values = [self.mypy_type_to_abstract_type(value) for value in generic_type.values]

if values:
variance_values = sds_types.UnionType(
[self.mypy_type_to_abstract_type(value) for value in generic_type.values],
Expand Down Expand Up @@ -840,10 +843,11 @@ def _parse_attributes(
elif hasattr(lvalue, "items"):
lvalues = list(lvalue.items)
for lvalue_ in lvalues:
if not hasattr(lvalue_, "name"): # pragma: no cover
raise AttributeError("Expected value to have attribute 'name'.")

if self._is_attribute_already_defined(lvalue_.name):
if (
hasattr(lvalue_, "name")
and self._is_attribute_already_defined(lvalue_.name)
or isinstance(lvalue_, mp_nodes.IndexExpr)
):
continue

attributes.append(
Expand Down Expand Up @@ -920,7 +924,9 @@ def _create_attribute(
if unanalyzed_type is not None and hasattr(unanalyzed_type, "args"):
attribute_type.args = unanalyzed_type.args
else: # pragma: no cover
raise AttributeError("Could not get argument information for attribute.")
logging.warning("Could not get argument information for attribute.")
attribute_type = None
type_ = sds_types.UnknownType()

# Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase
if (
Expand Down Expand Up @@ -966,8 +972,10 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis
default_is_none = False

# Get type information for parameter
if mypy_type is None: # pragma: no cover
raise ValueError("Argument has no type.")
if mypy_type is None:
msg = f"Could not parse the type for parameter {argument.variable.name} of function {node.fullname}."
logging.warning(msg)
arg_type = sds_types.UnknownType()
elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any):
# We try to infer the type through the default value later, if possible
pass
Expand Down Expand Up @@ -1122,8 +1130,24 @@ def mypy_type_to_abstract_type(
types = [self.mypy_type_to_abstract_type(arg) for arg in getattr(unanalyzed_type, "args", [])]
if len(types) == 1:
return sds_types.FinalType(type_=types[0])
elif len(types) == 0: # pragma: no cover
raise ValueError("Final type has no type arguments.")
elif len(types) == 0:
if hasattr(mypy_type, "items"):
literals = [
self.mypy_type_to_abstract_type(item.last_known_value)
for item in mypy_type.items
if isinstance(item.last_known_value, mp_types.LiteralType)
]

if literals:
all_literals = []
for literal_type in literals:
if isinstance(literal_type, sds_types.LiteralType):
all_literals += literal_type.literals

return sds_types.FinalType(type_=sds_types.LiteralType(literals=all_literals))

logging.warning("Final type has no type arguments.") # pragma: no cover
return sds_types.FinalType(type_=sds_types.UnknownType()) # pragma: no cover
return sds_types.FinalType(type_=sds_types.UnionType(types=types))
elif unanalyzed_type_name in {"list", "set"}:
type_args = getattr(mypy_type, "args", [])
Expand Down Expand Up @@ -1168,6 +1192,10 @@ def mypy_type_to_abstract_type(
if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type:
# If the Any type is generated b/c of from_unimported_type, then we can parse the type
# from the import information
if mypy_type.missing_import_name is None: # pragma: no cover
logging.warning("Could not parse a type, added unknown type instead.")
return sds_types.UnknownType()

missing_import_name = mypy_type.missing_import_name.split(".")[-1] # type: ignore[union-attr]
name, qname = self._find_alias(missing_import_name)

Expand Down
65 changes: 42 additions & 23 deletions src/safeds_stubgen/docstring_parsing/_docstring_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ def get_class_documentation(self, class_node: nodes.ClassDef) -> ClassDocstring:
if griffe_node.docstring is not None:
docstring = griffe_node.docstring.value.strip("\n")

for docstring_section in griffe_node.docstring.parsed:
if docstring_section.kind == DocstringSectionKind.text:
description = docstring_section.value.strip("\n")
elif docstring_section.kind == DocstringSectionKind.examples:
for example_data in docstring_section.value:
examples.append(example_data[1].strip("\n"))
try:
for docstring_section in griffe_node.docstring.parsed:
if docstring_section.kind == DocstringSectionKind.text:
description = docstring_section.value.strip("\n")
elif docstring_section.kind == DocstringSectionKind.examples:
for example_data in docstring_section.value:
examples.append(example_data[1].strip("\n"))
except IndexError as _: # pragma: no cover
msg = f"There was an error while parsing the following docstring:\n{docstring}."
logging.warning(msg)

return ClassDocstring(
description=description,
Expand All @@ -75,12 +79,17 @@ def get_function_documentation(self, function_node: nodes.FuncDef) -> FunctionDo
griffe_docstring = self.__get_cached_docstring(function_node.fullname)
if griffe_docstring is not None:
docstring = griffe_docstring.value.strip("\n")
for docstring_section in griffe_docstring.parsed:
if docstring_section.kind == DocstringSectionKind.text:
description = docstring_section.value.strip("\n")
elif docstring_section.kind == DocstringSectionKind.examples:
for example_data in docstring_section.value:
examples.append(example_data[1].strip("\n"))

try:
for docstring_section in griffe_docstring.parsed:
if docstring_section.kind == DocstringSectionKind.text:
description = docstring_section.value.strip("\n")
elif docstring_section.kind == DocstringSectionKind.examples:
for example_data in docstring_section.value:
examples.append(example_data[1].strip("\n"))
except IndexError as _: # pragma: no cover
msg = f"There was an error while parsing the following docstring:\n{docstring}."
logging.warning(msg)

return FunctionDocstring(
description=description,
Expand Down Expand Up @@ -193,10 +202,15 @@ def get_result_documentation(self, function_qname: str) -> list[ResultDocstring]
return []

all_returns = None
for docstring_section in griffe_docstring.parsed:
if docstring_section.kind == DocstringSectionKind.returns:
all_returns = docstring_section
break
try:
for docstring_section in griffe_docstring.parsed:
if docstring_section.kind == DocstringSectionKind.returns:
all_returns = docstring_section
break
except IndexError as _: # pragma: no cover
msg = f"There was an error while parsing the following docstring:\n{griffe_docstring.value}."
logging.warning(msg)
return []

if not all_returns:
return []
Expand Down Expand Up @@ -240,13 +254,18 @@ def _get_matching_docstrings(
type_: Literal["attr", "param"],
) -> list[DocstringAttribute | DocstringParameter]:
all_docstrings = None
for docstring_section in function_doc.parsed:
section_kind = docstring_section.kind
if (type_ == "attr" and section_kind == DocstringSectionKind.attributes) or (
type_ == "param" and section_kind == DocstringSectionKind.parameters
):
all_docstrings = docstring_section
break
try:
for docstring_section in function_doc.parsed:
section_kind = docstring_section.kind
if (type_ == "attr" and section_kind == DocstringSectionKind.attributes) or (
type_ == "param" and section_kind == DocstringSectionKind.parameters
):
all_docstrings = docstring_section
break
except IndexError as _: # pragma: no cover
msg = f"There was an error while parsing the following docstring:\n{function_doc.value}."
logging.warning(msg)
return []

if all_docstrings:
name = name.lstrip("*")
Expand Down
15 changes: 11 additions & 4 deletions src/safeds_stubgen/stubs_generator/_stub_string_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,9 @@ def _create_internal_class_string(
) -> str:
superclass_class = self._get_class_in_package(superclass)

if superclass_class is None: # pragma: no cover
return ""

# Methods
superclass_methods_text, existing_names = self._create_class_method_string(
superclass_class.methods,
Expand All @@ -881,6 +884,10 @@ def _create_internal_class_string(
already_defined_names = already_defined_names.union(existing_names)

for superclass_superclass in superclass_class.superclasses:
if superclass_superclass == superclass: # pragma: no cover
# If the class somehow has itself as a superclass
continue

name = superclass_superclass.split(".")[-1]
if is_internal(name):
superclass_methods_text += self._create_internal_class_string(
Expand Down Expand Up @@ -1107,7 +1114,7 @@ def _create_todo_msg(self, indentations: str) -> str:

return indentations + f"\n{indentations}".join(todo_msgs) + "\n"

def _get_class_in_package(self, class_qname: str) -> Class:
def _get_class_in_package(self, class_qname: str) -> Class | None:
class_qname = class_qname.replace(".", "/")
class_path = "/".join(class_qname.split("/")[:-1])
class_name = class_qname.split("/")[-1]
Expand All @@ -1122,9 +1129,9 @@ def _get_class_in_package(self, class_qname: str) -> Class:
):
return self.api.classes[class_]

raise LookupError(
f"Expected finding class '{class_name}' in module '{self._get_module_id(get_actual_id=True)}'.",
) # pragma: no cover
msg = f"Expected finding class '{class_name}' in module '{self._get_module_id(get_actual_id=True)}'." # pragma: no cover
logging.warning(msg) # pragma: no cover
return None # pragma: no cover

@staticmethod
def _create_docstring_description_part(description: str, indentations: str) -> str:
Expand Down
2 changes: 2 additions & 0 deletions tests/data/various_modules_package/attribute_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def some_func() -> bool:
str_attr_with_none_value: str = None

optional: Optional[int]
final_int: Final = (101, 32741, 2147483621)
no_final_type: Final
final: Final[str] = "Value"
finals: Final[str, int] = "Value"
final_union: Final[str | int] = "Value"
Expand Down
6 changes: 5 additions & 1 deletion tests/data/various_modules_package/class_module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Self, overload
from typing import Self, overload, no_type_check
from . import unknown_source

from tests.data.main_package.another_path.another_module import yetAnotherClass
Expand All @@ -18,6 +18,10 @@ def __init__(self, a: int, b: ClassModuleEmptyClassA | None):
def __enter__(self):
return self

@no_type_check
def _apply(self, f, *args, **kwargs):
pass

def f(self): ...


Expand Down
61 changes: 61 additions & 0 deletions tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,27 @@
}),
}),
}),
dict({
'docstring': dict({
'description': '',
'type': None,
}),
'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/final_int',
'is_public': True,
'is_static': True,
'name': 'final_int',
'type': dict({
'kind': 'FinalType',
'type': dict({
'kind': 'LiteralType',
'literals': list([
101,
32741,
2147483621,
]),
}),
}),
}),
dict({
'docstring': dict({
'description': '',
Expand Down Expand Up @@ -685,6 +706,17 @@
]),
}),
}),
dict({
'docstring': dict({
'description': '',
'type': None,
}),
'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/no_final_type',
'is_public': True,
'is_static': True,
'name': 'no_final_type',
'type': None,
}),
dict({
'docstring': dict({
'description': '',
Expand Down Expand Up @@ -1237,6 +1269,30 @@
'tests/data/various_modules_package/class_module/ClassModuleClassB/__enter__/result_1',
]),
}),
dict({
'docstring': dict({
'description': '',
'examples': list([
]),
'full_docstring': '',
}),
'id': 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply',
'is_class_method': False,
'is_property': False,
'is_public': False,
'is_static': False,
'name': '_apply',
'parameters': list([
'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/self',
'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/f',
'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/args',
'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/kwargs',
]),
'reexported_by': list([
]),
'results': list([
]),
}),
dict({
'docstring': dict({
'description': '',
Expand Down Expand Up @@ -2138,6 +2194,7 @@
'is_public': True,
'methods': list([
'tests/data/various_modules_package/class_module/ClassModuleClassB/__enter__',
'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply',
'tests/data/various_modules_package/class_module/ClassModuleClassB/f',
]),
'name': 'ClassModuleClassB',
Expand Down Expand Up @@ -7485,6 +7542,10 @@
'alias': None,
'qualified_name': 'typing.overload',
}),
dict({
'alias': None,
'qualified_name': 'typing.no_type_check',
}),
dict({
'alias': None,
'qualified_name': 'unknown_source',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class AttributesClassB() {
@PythonName("str_attr_with_none_value")
static attr strAttrWithNoneValue: String
static attr optional: Int?
@PythonName("final_int")
static attr finalInt: literal<101, 32741, 2147483621>
// TODO Attribute has no type information.
@PythonName("no_final_type")
static attr noFinalType
static attr final: String
static attr finals: union<Int, String>
@PythonName("final_union")
Expand Down

0 comments on commit 84c7237

Please sign in to comment.