Skip to content

Commit

Permalink
Added test data & linter/codecov fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Masara committed Apr 11, 2024
1 parent e505ecd commit 86ae2b8
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 45 deletions.
26 changes: 4 additions & 22 deletions src/safeds_stubgen/api_analyzer/_ast_walker.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,40 +47,22 @@ def __walk(self, node: MypyFile | ClassDef | Decorator | FuncDef | AssignmentStm
if isinstance(node, MypyFile):
definitions = get_mypyfile_definitions(node)
child_nodes = [
_def
for _def in definitions
if _def.__class__.__name__
in {"FuncDef", "ClassDef", "Decorator"}
_def for _def in definitions if _def.__class__.__name__ in {"FuncDef", "ClassDef", "Decorator"}
]
elif isinstance(node, ClassDef):
definitions = get_classdef_definitions(node)
child_nodes = [
_def
for _def in definitions
if _def.__class__.__name__
in {"AssignmentStmt", "FuncDef", "ClassDef", "Decorator"}
if _def.__class__.__name__ in {"AssignmentStmt", "FuncDef", "ClassDef", "Decorator"}
]
elif isinstance(node, FuncDef) and node.name == "__init__":
definitions = get_funcdef_definitions(node)
child_nodes = [
_def
for _def in definitions
if _def.__class__.__name__ == "AssignmentStmt"
]
child_nodes = [_def for _def in definitions if _def.__class__.__name__ == "AssignmentStmt"]

for child_node in child_nodes:
# Ignore global variables and function attributes if the function is an __init__
if isinstance(child_node, AssignmentStmt):
if isinstance(node, MypyFile):
continue
if isinstance(node, FuncDef) and node.name != "__init__":
continue

# The '__mypy-replace' name is a mypy placeholer which we don't want to parse.
if (
(isinstance(child_node, FuncDef) and isinstance(node, FuncDef))
or getattr(child_node, "name", "") == "__mypy-replace"
):
if getattr(child_node, "name", "") == "__mypy-replace": # pragma: no cover
continue

self.__walk(child_node, visited_nodes)
Expand Down
4 changes: 1 addition & 3 deletions src/safeds_stubgen/api_analyzer/cli/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def _get_args() -> argparse.Namespace:
parser.add_argument(
"-s",
"--src",
help=(
"Source directory containing the Python code of the package."
),
help=("Source directory containing the Python code of the package."),
type=Path,
required=True,
default=None,
Expand Down
37 changes: 19 additions & 18 deletions src/safeds_stubgen/docstring_parsing/_docstring_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
from griffe.dataclasses import Object
from mypy import nodes

from safeds_stubgen.api_analyzer import AbstractType


class DocstringParser(AbstractDocstringParser):
def __init__(self, parser: Parser, package_path: Path):
Expand Down Expand Up @@ -246,7 +244,11 @@ def _get_matching_docstrings(

return []

def _griffe_annotation_to_api_type(self, annotation: Expr | str, docstring: Docstring) -> AbstractType | None:
def _griffe_annotation_to_api_type(
self,
annotation: Expr | str,
docstring: Docstring,
) -> sds_types.AbstractType | None:
if isinstance(annotation, ExprName | ExprAttribute):
if annotation.canonical_path == "typing.Any":
return sds_types.NamedType(name="Any", qname="typing.Any")
Expand All @@ -268,14 +270,13 @@ def _griffe_annotation_to_api_type(self, annotation: Expr | str, docstring: Docs
elif isinstance(annotation, ExprSubscript):
any_type = sds_types.NamedType(name="Any", qname="typing.Any")
slices = annotation.slice
types: list[sds_types.AbstractType] = []
if isinstance(slices, ExprTuple):
types = []
for slice_ in slices.elements:
new_type = self._griffe_annotation_to_api_type(slice_, docstring)
if new_type is not None:
types.append(new_type)
else:
types = []
type_ = self._griffe_annotation_to_api_type(slices, docstring)
if type_ is not None:
types.append(type_)
Expand All @@ -286,13 +287,14 @@ def _griffe_annotation_to_api_type(self, annotation: Expr | str, docstring: Docs
return sds_types.TupleType(types=types)
elif annotation.canonical_path == "set":
return sds_types.SetType(types=types)
elif annotation.canonical_path == "collections.abc.Callable":
parameter_types = types[0] if len(types) >= 1 else [any_type]
if isinstance(parameter_types, sds_types.ListType):
parameter_types = parameter_types.types
elif annotation.canonical_path in {"collections.abc.Callable", "typing.Callable"}:
param_type = types[0] if len(types) >= 1 else [any_type]
if not isinstance(param_type, sds_types.AbstractType): # pragma: no cover
raise TypeError(f"Expected AbstractType object, received {type(param_type)}")
parameter_types = param_type.types if isinstance(param_type, sds_types.ListType) else [param_type]
return_type = types[1] if len(types) >= 2 else any_type
return sds_types.CallableType(parameter_types=parameter_types, return_type=return_type)
elif annotation.canonical_path in {"dict", "collections.abc.Mapping"}:
elif annotation.canonical_path in {"dict", "collections.abc.Mapping", "typing.Mapping"}:
key_type = types[0] if len(types) >= 1 else any_type
value_type = types[1] if len(types) >= 2 else any_type
return sds_types.DictType(key_type=key_type, value_type=value_type)
Expand All @@ -311,26 +313,25 @@ def _griffe_annotation_to_api_type(self, annotation: Expr | str, docstring: Docs
elif isinstance(annotation, ExprBoolOp):
types = []
for value in annotation.values:
value_type = self._griffe_annotation_to_api_type(value, docstring)
if value_type is not None:
types.append(value_type)
value_type_ = self._griffe_annotation_to_api_type(value, docstring)
if value_type_ is not None:
types.append(value_type_)
return sds_types.UnionType(types=types)
elif isinstance(annotation, ExprTuple):
elements = []
# Todo Remove the "optional" related part of the code once issue #99 is solved.
has_optional = False
for element in annotation.elements:
if not isinstance(element, str) and element.canonical_path == "optional":
for element_ in annotation.elements:
if not isinstance(element_, str) and element_.canonical_path == "optional":
has_optional = True
else:
new_element = self._griffe_annotation_to_api_type(element, docstring)
new_element = self._griffe_annotation_to_api_type(element_, docstring)
if new_element is not None:
elements.append(new_element)
if has_optional:
elements.append(sds_types.NamedType(name="None", qname="builtins.None"))
return sds_types.UnionType(elements)
else:
return sds_types.TupleType(elements)
return sds_types.TupleType(elements)
elif isinstance(annotation, str):
new_annotation = self._remove_default_from_griffe_annotation(annotation)
parsed_annotation = parse_annotation(new_annotation, docstring)
Expand Down
21 changes: 20 additions & 1 deletion tests/data/docstring_parser_package/googledoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A module for testing the various docstring types.
"""
from enum import Enum
from typing import Optional, Any
from typing import Optional, Any, Callable, Mapping
from tests.data.various_modules_package.another_path.another_module import AnotherClass


Expand Down Expand Up @@ -230,6 +230,10 @@ class ClassWithVariousAttributeTypes:
optional_type_2 (Optional[int]):
class_type (ClassWithAttributes):
imported_type (AnotherClass):
callable_type (Callable[[int], str]):
mapping_type (Mapping[int, str]):
bool_op_type (int or str or bool):
list_type_5 ([int]):
"""
no_type = ""
optional_type = ""
Expand All @@ -255,6 +259,21 @@ class ClassWithVariousAttributeTypes:
optional_type_2: Optional[int]
class_type: ClassWithAttributes
imported_type: AnotherClass
callable_type: Callable[[int], str]
mapping_type: Mapping[int, str]
bool_op_type: int | str | bool
list_type_5: list[int]


def uninferable_return_doc():
"""
uninferable_return_doc.
Dolor sit amet.
Returns:
'True' is something happens, else 'False'.
"""


def infer_types():
Expand Down
22 changes: 21 additions & 1 deletion tests/data/docstring_parser_package/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A module for testing the various docstring types.
"""
from typing import Any, Optional
from typing import Any, Optional, Callable, Mapping
from enum import Enum
from tests.data.various_modules_package.another_path.another_module import AnotherClass

Expand Down Expand Up @@ -363,6 +363,10 @@ class ClassWithVariousAttributeTypes:
optional_type_2 : Optional[int]
class_type : ClassWithAttributes
imported_type : AnotherClass
callable_type : Callable[[int], str]
mapping_type : Mapping[int, str]
bool_op_type : int or str or bool
list_type_5 : [int]
"""
no_type = ""
optional_type = ""
Expand All @@ -388,6 +392,22 @@ class ClassWithVariousAttributeTypes:
optional_type_2: Optional[int]
class_type: ClassWithAttributes
imported_type: AnotherClass
callable_type: Callable[[int], str]
mapping_type: Mapping[int, str]
bool_op_type: int | str | bool
list_type_5: list[int]


def uninferable_return_doc():
"""
uninferable_return_doc.
Dolor sit amet.
Returns
-------
'True' is something happens, else 'False'.
"""


def infer_types():
Expand Down
23 changes: 23 additions & 0 deletions tests/data/docstring_parser_package/restdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ def infer_types2(a, b):
return True


def uninferable_return_doc():
"""
uninferable_return_doc.
Dolor sit amet.
:return: return value
:rtype: 'True' is something happens, else 'False'.
"""


# Todo Currently disabled, since Griffe can't analyze ReST (Sphinx) attributes (see issue #98)
# class ClassWithVariousAttributeTypes:
# """
Expand Down Expand Up @@ -295,6 +306,14 @@ def infer_types2(a, b):
# :type any_type: Any
# :var optional_type_2:
# :type optional_type_2: Optional[int]
# :var callable_type:
# :type callable_type: Callable[[int], str]
# :var mapping_type:
# :type mapping_type: Mapping[int, str]
# :var bool_op_type:
# :type bool_op_type: int or str or bool
# :var list_type_5:
# :type list_type_5: [int]
# """
# has_default = 1
# optional_int = None
Expand Down Expand Up @@ -324,3 +343,7 @@ def infer_types2(a, b):
# optional_type_2: Optional[int]
# class_type: ClassWithMethod
# imported_type: AnotherClass
# callable_type: Callable[[int], str]
# mapping_type: Mapping[int, str]
# bool_op_type: int | str | bool
# list_type_5: list[int]
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ fun functionWithMultipleResults() -> (result1: Int, result2: Boolean)
@PythonName("function_without_return_value")
fun functionWithoutReturnValue()

// TODO Result type information missing.
/**
* uninferable_return_doc.
*
* Dolor sit amet.
*
* @result result1 'True' is something happens, else 'False'.
*/
@Pure
@PythonName("uninferable_return_doc")
fun uninferableReturnDoc()

/**
* property_method_with_docstring.
*
Expand Down Expand Up @@ -288,6 +300,15 @@ class ClassWithVariousAttributeTypes() {
static attr classType: ClassWithAttributes
@PythonName("imported_type")
static attr importedType: AnotherClass
// TODO Attribute has no type information.
@PythonName("callable_type")
static attr callableType
@PythonName("mapping_type")
static attr mappingType: Map<Int, String>
@PythonName("bool_op_type")
static attr boolOpType: union<Boolean, Int, String>
@PythonName("list_type_5")
static attr listType5: List<Int>
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ fun functionWithMultipleResults() -> (firstResult: Int, secondResult: Boolean)
@PythonName("function_without_result_value")
fun functionWithoutResultValue()

// TODO Result type information missing.
/**
* uninferable_return_doc.
*
* Dolor sit amet.
*/
@Pure
@PythonName("uninferable_return_doc")
fun uninferableReturnDoc()

/**
* property_method_with_docstring.
*
Expand Down Expand Up @@ -385,6 +395,15 @@ class ClassWithVariousAttributeTypes() {
static attr classType: ClassWithAttributes
@PythonName("imported_type")
static attr importedType: AnotherClass
// TODO Attribute has no type information.
@PythonName("callable_type")
static attr callableType
@PythonName("mapping_type")
static attr mappingType: Map<Int, String>
@PythonName("bool_op_type")
static attr boolOpType: union<Boolean, Int, String>
@PythonName("list_type_5")
static attr listType5: List<Int>
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ fun inferTypes2(
b
) -> result1: union<Boolean, String>

// TODO Result type information missing.
/**
* uninferable_return_doc.
*
* Dolor sit amet.
*
* @result result1 return value
*/
@Pure
@PythonName("uninferable_return_doc")
fun uninferableReturnDoc()

/**
* ClassWithDocumentation. Code::
*
Expand Down

0 comments on commit 86ae2b8

Please sign in to comment.