menderbot.typing   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 110
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 86
dl 0
loc 110
rs 10
c 0
b 0
f 0
wmc 16

4 Functions

Rating   Name   Duplication   Size   Complexity  
A parse_type_hint_answer() 0 11 1
A what_needs_typing() 0 14 3
A process_untyped_functions() 0 11 3
C add_type_hints() 0 57 9
1
import logging
2
import os
3
import re
4
5
from menderbot import python_cst
6
from menderbot.source_file import Insertion, SourceFile
7
8
logger = logging.getLogger("typing")
9
10
11
def process_untyped_functions(source_file: SourceFile):
12
    path = source_file.path
13
    logger.info('Processing "%s"...', path)
14
    _, file_extension = os.path.splitext(path)
15
    if not file_extension == ".py":
16
        logger.info('"%s" is not a Python file, skipping.', path)
17
        return
18
    source = source_file.load_source_as_utf8()
19
20
    for fn_ast in python_cst.collect_function_asts(source):
21
        yield (fn_ast, what_needs_typing(fn_ast))
22
23
24
def parse_type_hint_answer(text: str) -> list:
25
    def line_to_tuple(line: str) -> tuple:
26
        [ident, new_type] = line.split(":")
27
        new_type = re.sub(r"\bList\b", "list", new_type)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable new_type does not seem to be defined.
Loading history...
28
        # NoReturn has valid uses but is unlikely, assume we mean None
29
        new_type = re.sub(r"\bNoReturn\b", "None", new_type)
30
        return (ident.strip(), new_type.strip())
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ident does not seem to be defined.
Loading history...
31
32
    lines = text.strip().splitlines()
33
    hints = [line_to_tuple(line) for line in lines if ":" in line]
34
    return [hint for hint in hints if hint[0] != "self" and hint[1].lower() != "any"]
35
36
37
def add_type_hints(
38
    function_ast: python_cst.AstNode,
39
    hints: list[tuple[str, str]],
40
    imports: list[tuple[str, str]],
41
) -> list:
42
    print("function_node", function_ast.props)
43
    function_name = function_ast.props["name"]
44
    sig_ast = function_ast.children_filtered(kind=python_cst.KIND_SIGNATURE)[0]
45
    def_param_nodes = sig_ast.children_filtered(kind=python_cst.KIND_PARAM)
46
    return_type = function_ast.props.get(python_cst.PROP_RETURN_TYPE)
47
    insertions = []
48
    common_typing_import_names = ["Optional", "Callable", "NamedTuple", "Any", "Type"]
49
50
    def add_needed_imports(new_type: str):
51
        new_type_symbols = re.findall(r"\b\w+\b", new_type)
52
        for new_type_symbol in new_type_symbols:
53
            if (
54
                new_type_symbol in common_typing_import_names
55
                and ("typing", new_type_symbol) not in imports
56
            ):
57
                imports.append(("typing", new_type_symbol))
58
                insertions.append(
59
                    Insertion(
60
                        text=f"from typing import {new_type_symbol}",
61
                        line_number=1,
62
                        label="type_import",
63
                    )
64
                )
65
66
    for ident, new_type in hints:
67
        add_needed_imports(new_type)
68
        for param_node in def_param_nodes:
69
            if param_node.props.get("name") == ident:
70
                insertions.append(
71
                    Insertion(
72
                        text=f": {new_type}",
73
                        line_number=param_node.src_range.end.line,
74
                        col=param_node.src_range.end.col,
75
                        inline=True,
76
                        label=function_name,
77
                    )
78
                )
79
        if ident == "return" and not return_type:
80
            signature_ast = function_ast.children_filtered(
81
                kind=python_cst.KIND_SIGNATURE
82
            )[0]
83
            insertions.append(
84
                Insertion(
85
                    text=f" -> {new_type}",
86
                    line_number=signature_ast.src_range.end.line,
87
                    col=signature_ast.src_range.end.col,
88
                    inline=True,
89
                    label=function_name,
90
                )
91
            )
92
93
    return insertions
94
95
96
def what_needs_typing(fn_ast: python_cst.AstNode) -> list[str]:
97
    name = fn_ast.props["name"]
98
    sig_ast = fn_ast.children_filtered(kind=python_cst.KIND_SIGNATURE)[0]
99
    param_asts = sig_ast.children_filtered(kind=python_cst.KIND_PARAM)
100
    return_type = fn_ast.props.get(python_cst.PROP_RETURN_TYPE)
101
    needs_typing = [
102
        param_ast.props["name"]
103
        for param_ast in param_asts
104
        if "type" not in param_ast.props
105
        and param_ast.props["name"] not in ["self", "cls"]
106
    ]
107
    if not return_type and name != "__init__":
108
        needs_typing.append("return")
109
    return needs_typing
110
111
112
# def get_function_param_nodes(
113
#     node: PythonParser.FuncdefContext,
114
# ) -> Generator[PythonParser.Def_parameterContext, None, None]:
115
#     args_list_node: PythonParser.TypedargslistContext = node.typedargslist()
116
#     # args_node : Optional[PythonParser.ArgsContext] = params_node.args()
117
#     # def_params_node: Optional[PythonParser.Def_parametersContext] = params_node.def_parameters()
118
#     # kwargs_node: Optional[PythonParser.KwargsContext] = params_node.kwargs()
119
#     if args_list_node:
120
#         def_params_nodes: list[
121
#             PythonParser.Def_parameterContext
122
#         ] = args_list_node.def_parameters()
123
#         for def_params_node_untyped in def_params_nodes:
124
#             def_params_node: PythonParser.Def_parametersContext = def_params_node_untyped
125
#             for def_param_node_untyped in def_params_node.def_parameter():
126
#                 def_param_node: PythonParser.Def_parameterContext = def_param_node_untyped
127
#                 yield def_param_node
128