pycodecommenter 2.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyCodeCommenter/__init__.py +26 -0
- PyCodeCommenter/cli.py +72 -0
- PyCodeCommenter/commenter.py +450 -0
- PyCodeCommenter/coverage.py +124 -0
- PyCodeCommenter/docstring_parser.py +121 -0
- PyCodeCommenter/parameter_descriptions.py +76 -0
- PyCodeCommenter/templates.py +95 -0
- PyCodeCommenter/type_analyzer.py +203 -0
- PyCodeCommenter/validator.py +563 -0
- pycodecommenter-2.0.3.dist-info/METADATA +281 -0
- pycodecommenter-2.0.3.dist-info/RECORD +15 -0
- pycodecommenter-2.0.3.dist-info/WHEEL +5 -0
- pycodecommenter-2.0.3.dist-info/entry_points.txt +2 -0
- pycodecommenter-2.0.3.dist-info/licenses/LICENSE +21 -0
- pycodecommenter-2.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docstring parsing module for PyCodeCommenter.
|
|
3
|
+
|
|
4
|
+
This module provides intelligent parsing of existing Python docstrings in multiple
|
|
5
|
+
formats (Google, Sphinx, NumPy). It extracts structured information including
|
|
6
|
+
summary, description, parameters, and return values for smart docstring merging.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
DocstringParser: Main parser class for extracting docstring information
|
|
10
|
+
"""
|
|
11
|
+
import re
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Dict, Any, List, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class DocstringParser:
|
|
18
|
+
"""
|
|
19
|
+
Parses existing docstrings in both Google and Sphinx styles.
|
|
20
|
+
Extracts summary, description, parameters, and return information.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, docstring: Optional[str] = None):
|
|
24
|
+
self.raw_docstring = docstring or ""
|
|
25
|
+
self.summary = ""
|
|
26
|
+
self.description = ""
|
|
27
|
+
self.params = {} # type: Dict[str, str]
|
|
28
|
+
self.returns = ""
|
|
29
|
+
|
|
30
|
+
if self.raw_docstring:
|
|
31
|
+
self.parse()
|
|
32
|
+
|
|
33
|
+
def parse(self) -> None:
|
|
34
|
+
"""Main entry point for parsing the docstring."""
|
|
35
|
+
if not self.raw_docstring.strip():
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
lines = self.raw_docstring.strip().splitlines()
|
|
39
|
+
self.summary = lines[0].strip()
|
|
40
|
+
|
|
41
|
+
remaining_content = "\n".join(lines[1:]).strip()
|
|
42
|
+
|
|
43
|
+
# Determine if it's likely Google or Sphinx
|
|
44
|
+
if ":param" in remaining_content or ":return" in remaining_content:
|
|
45
|
+
self._parse_sphinx(remaining_content)
|
|
46
|
+
else:
|
|
47
|
+
self._parse_google(remaining_content)
|
|
48
|
+
|
|
49
|
+
def _parse_sphinx(self, content: str) -> None:
|
|
50
|
+
"""Parses Sphinx style documentation (:param name: desc)."""
|
|
51
|
+
desc_lines = []
|
|
52
|
+
current_param = None
|
|
53
|
+
|
|
54
|
+
for line in content.splitlines():
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line: continue
|
|
57
|
+
|
|
58
|
+
if line.startswith(':param'):
|
|
59
|
+
match = re.match(r':param\s+(\w+):\s*(.*)', line)
|
|
60
|
+
if match:
|
|
61
|
+
current_param = match.group(1)
|
|
62
|
+
self.params[current_param] = match.group(2).strip()
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if line.startswith(':return'):
|
|
66
|
+
match = re.match(r':returns?:\s*(.*)', line)
|
|
67
|
+
if match:
|
|
68
|
+
self.returns = match.group(1).strip()
|
|
69
|
+
current_param = None
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if current_param and not line.startswith(':'):
|
|
73
|
+
self.params[current_param] += " " + line
|
|
74
|
+
elif not line.startswith(':'):
|
|
75
|
+
desc_lines.append(line)
|
|
76
|
+
|
|
77
|
+
self.description = " ".join(desc_lines).strip()
|
|
78
|
+
|
|
79
|
+
def _parse_google(self, content: str) -> None:
|
|
80
|
+
"""Parses Google style documentation (Args:, Returns:)."""
|
|
81
|
+
# Split by sections, allowing headers to be at the start or after a newline
|
|
82
|
+
sections = re.split(r'(?m)^ *(Args|Returns|Attributes|Methods):$', content)
|
|
83
|
+
|
|
84
|
+
# If the first part doesn't match a header, it's the description
|
|
85
|
+
self.description = sections[0].strip()
|
|
86
|
+
|
|
87
|
+
for i in range(1, len(sections), 2):
|
|
88
|
+
header = sections[i]
|
|
89
|
+
body = sections[i+1] if i+1 < len(sections) else ""
|
|
90
|
+
|
|
91
|
+
if header == "Args":
|
|
92
|
+
self._parse_google_args(body)
|
|
93
|
+
elif header == "Returns":
|
|
94
|
+
self.returns = body.strip()
|
|
95
|
+
|
|
96
|
+
def _parse_google_args(self, body: str) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Helper to parse the Args section of a Google docstring.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
body (str): The body of the Args section.
|
|
102
|
+
"""
|
|
103
|
+
current_arg = None
|
|
104
|
+
for line in body.splitlines():
|
|
105
|
+
# Match " name (type): desc" or " name: desc"
|
|
106
|
+
# Improved regex to handle various spacing and optional types more robustly
|
|
107
|
+
match = re.match(r'^\s+(\w+)\s*(?:\(([^)]+)\))?\s*:\s*(.*)', line)
|
|
108
|
+
if match:
|
|
109
|
+
current_arg = match.group(1)
|
|
110
|
+
self.params[current_arg] = match.group(3).strip()
|
|
111
|
+
elif current_arg and line.startswith(' '): # Continuation line
|
|
112
|
+
self.params[current_arg] += " " + line.strip()
|
|
113
|
+
|
|
114
|
+
def get_info(self) -> Dict[str, Any]:
|
|
115
|
+
"""Returns the parsed information as a dictionary."""
|
|
116
|
+
return {
|
|
117
|
+
"summary": self.summary,
|
|
118
|
+
"description": self.description,
|
|
119
|
+
"params": self.params,
|
|
120
|
+
"returns": self.returns
|
|
121
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
parameter_descriptions = {
|
|
2
|
+
"calculate_area": {
|
|
3
|
+
"length": "Length of the rectangle.",
|
|
4
|
+
"width": "Width of the rectangle."
|
|
5
|
+
},
|
|
6
|
+
"calculate_volume": {
|
|
7
|
+
"radius": "Radius of the sphere.",
|
|
8
|
+
"height": "Height of the cylinder."
|
|
9
|
+
},
|
|
10
|
+
"calculate_perimeter": {
|
|
11
|
+
"side": "Side length of the square.",
|
|
12
|
+
"length": "Length of the rectangle.",
|
|
13
|
+
"width": "Width of the rectangle."
|
|
14
|
+
},
|
|
15
|
+
"calculate_surface_area": {
|
|
16
|
+
"radius": "Radius of the sphere.",
|
|
17
|
+
"height": "Height of the cylinder.",
|
|
18
|
+
"length": "Length of the cuboid.",
|
|
19
|
+
"width": "Width of the cuboid.",
|
|
20
|
+
"depth": "Depth of the cuboid."
|
|
21
|
+
},
|
|
22
|
+
"calculate_density": {
|
|
23
|
+
"mass": "Mass of the object.",
|
|
24
|
+
"volume": "Volume of the object."
|
|
25
|
+
},
|
|
26
|
+
"sort_list": {
|
|
27
|
+
"lst": "List to be sorted.",
|
|
28
|
+
"reverse": "Sort in descending order if True."
|
|
29
|
+
},
|
|
30
|
+
"find_max": {
|
|
31
|
+
"numbers": "List of numbers to find the maximum from."
|
|
32
|
+
},
|
|
33
|
+
"find_min": {
|
|
34
|
+
"numbers": "List of numbers to find the minimum from."
|
|
35
|
+
},
|
|
36
|
+
"search_item": {
|
|
37
|
+
"lst": "List to search the item in.",
|
|
38
|
+
"item": "Item to search for in the list."
|
|
39
|
+
},
|
|
40
|
+
"concatenate_strings": {
|
|
41
|
+
"str1": "First string.",
|
|
42
|
+
"str2": "Second string."
|
|
43
|
+
},
|
|
44
|
+
"split_string": {
|
|
45
|
+
"string": "String to be split.",
|
|
46
|
+
"delimiter": "Delimiter to split the string by."
|
|
47
|
+
},
|
|
48
|
+
"read_file": {
|
|
49
|
+
"file_path": "Path to the file to be read."
|
|
50
|
+
},
|
|
51
|
+
"write_file": {
|
|
52
|
+
"file_path": "Path to the file to be written.",
|
|
53
|
+
"content": "Content to write to the file."
|
|
54
|
+
},
|
|
55
|
+
"connect_to_database": {
|
|
56
|
+
"host": "Database host address.",
|
|
57
|
+
"port": "Port number for the database connection.",
|
|
58
|
+
"username": "Username for the database connection.",
|
|
59
|
+
"password": "Password for the database connection."
|
|
60
|
+
},
|
|
61
|
+
"execute_query": {
|
|
62
|
+
"query": "SQL query to be executed.",
|
|
63
|
+
"connection": "Database connection object."
|
|
64
|
+
},
|
|
65
|
+
"send_request": {
|
|
66
|
+
"url": "URL to send the request to.",
|
|
67
|
+
"method": "HTTP method (GET, POST, etc.).",
|
|
68
|
+
"headers": "Headers to include in the request.",
|
|
69
|
+
"data": "Data to send in the request body."
|
|
70
|
+
},
|
|
71
|
+
"send_email": {
|
|
72
|
+
"to_address": "Recipient email address.",
|
|
73
|
+
"subject": "Subject of the email.",
|
|
74
|
+
"body": "Body content of the email."
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# function that maps common verbs to templates
|
|
2
|
+
def get_function_description(func_name: str) -> str:
|
|
3
|
+
"""
|
|
4
|
+
Returns a template-based description for a function based on its name.
|
|
5
|
+
|
|
6
|
+
Args:
|
|
7
|
+
func_name (str): The name of the function to describe.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
str: A descriptive sentence for the function.
|
|
11
|
+
"""
|
|
12
|
+
verb_templates = {
|
|
13
|
+
"calculate": f"Calculates the {func_name.split('_', 1)[-1]}.",
|
|
14
|
+
"get": f"Retrieves the {func_name.split('_', 1)[-1]} from a given source.",
|
|
15
|
+
"create": f"Creates a new {func_name.split('_', 1)[-1]}.",
|
|
16
|
+
"fetch": f"Fetches the {func_name.split('_', 1)[-1]}.",
|
|
17
|
+
"process": f"Processes the input to produce a {func_name.split('_', 1)[-1]}.",
|
|
18
|
+
"update": f"Updates the {func_name.split('_', 1)[-1]}.",
|
|
19
|
+
"delete": f"Deletes the {func_name.split('_', 1)[-1]}.",
|
|
20
|
+
"remove": f"Removes the {func_name.split('_', 1)[-1]}.",
|
|
21
|
+
"add": f"Adds a new {func_name.split('_', 1)[-1]}.",
|
|
22
|
+
"set": f"Sets the {func_name.split('_', 1)[-1]}.",
|
|
23
|
+
"initialize": f"Initializes the {func_name.split('_', 1)[-1]}.",
|
|
24
|
+
"load": f"Loads the {func_name.split('_', 1)[-1]}.",
|
|
25
|
+
"save": f"Saves the {func_name.split('_', 1)[-1]}.",
|
|
26
|
+
"validate": f"Validates the {func_name.split('_', 1)[-1]}.",
|
|
27
|
+
"send": f"Sends the {func_name.split('_', 1)[-1]}.",
|
|
28
|
+
"receive": f"Receives the {func_name.split('_', 1)[-1]}.",
|
|
29
|
+
"compute": f"Computes the {func_name.split('_', 1)[-1]}.",
|
|
30
|
+
"generate": f"Generates the {func_name.split('_', 1)[-1]}.",
|
|
31
|
+
"convert": f"Converts the {func_name.split('_', 1)[-1]}.",
|
|
32
|
+
"transform": f"Transforms the {func_name.split('_', 1)[-1]}.",
|
|
33
|
+
"merge": f"Merges the {func_name.split('_', 1)[-1]}.",
|
|
34
|
+
"split": f"Splits the {func_name.split('_', 1)[-1]}.",
|
|
35
|
+
"extract": f"Extracts the {func_name.split('_', 1)[-1]}.",
|
|
36
|
+
"compare": f"Compares the {func_name.split('_', 1)[-1]}.",
|
|
37
|
+
"sort": f"Sorts the {func_name.split('_', 1)[-1]}.",
|
|
38
|
+
"filter": f"Filters the {func_name.split('_', 1)[-1]}.",
|
|
39
|
+
"aggregate": f"Aggregates the {func_name.split('_', 1)[-1]}.",
|
|
40
|
+
"map": f"Maps the {func_name.split('_', 1)[-1]}.",
|
|
41
|
+
"reduce": f"Reduces the {func_name.split('_', 1)[-1]}.",
|
|
42
|
+
"clean": f"Cleans the {func_name.split('_', 1)[-1]}.",
|
|
43
|
+
"normalize": f"Normalizes the {func_name.split('_', 1)[-1]}.",
|
|
44
|
+
"format": f"Formats the {func_name.split('_', 1)[-1]}.",
|
|
45
|
+
"parse": f"Parses the {func_name.split('_', 1)[-1]}.",
|
|
46
|
+
"serialize": f"Serializes the {func_name.split('_', 1)[-1]}.",
|
|
47
|
+
"deserialize": f"Deserializes the {func_name.split('_', 1)[-1]}.",
|
|
48
|
+
"encrypt": f"Encrypts the {func_name.split('_', 1)[-1]}.",
|
|
49
|
+
"decrypt": f"Decrypts the {func_name.split('_', 1)[-1]}.",
|
|
50
|
+
"compress": f"Compresses the {func_name.split('_', 1)[-1]}.",
|
|
51
|
+
"decompress": f"Decompresses the {func_name.split('_', 1)[-1]}.",
|
|
52
|
+
"archive": f"Archives the {func_name.split('_', 1)[-1]}.",
|
|
53
|
+
"unarchive": f"Unarchives the {func_name.split('_', 1)[-1]}.",
|
|
54
|
+
"backup": f"Backs up the {func_name.split('_', 1)[-1]}.",
|
|
55
|
+
"restore": f"Restores the {func_name.split('_', 1)[-1]}.",
|
|
56
|
+
"log": f"Logs the {func_name.split('_', 1)[-1]}.",
|
|
57
|
+
"monitor": f"Monitors the {func_name.split('_', 1)[-1]}.",
|
|
58
|
+
"track": f"Tracks the {func_name.split('_', 1)[-1]}.",
|
|
59
|
+
"report": f"Reports the {func_name.split('_', 1)[-1]}.",
|
|
60
|
+
"notify": f"Notifies about the {func_name.split('_', 1)[-1]}.",
|
|
61
|
+
"alert": f"Alerts about the {func_name.split('_', 1)[-1]}.",
|
|
62
|
+
"schedule": f"Schedules the {func_name.split('_', 1)[-1]}.",
|
|
63
|
+
"execute": f"Executes the {func_name.split('_', 1)[-1]}.",
|
|
64
|
+
"run": f"Runs the {func_name.split('_', 1)[-1]}.",
|
|
65
|
+
"test": f"Tests the {func_name.split('_', 1)[-1]}.",
|
|
66
|
+
"debug": f"Debugs the {func_name.split('_', 1)[-1]}.",
|
|
67
|
+
"build": f"Builds the {func_name.split('_', 1)[-1]}.",
|
|
68
|
+
"deploy": f"Deploys the {func_name.split('_', 1)[-1]}.",
|
|
69
|
+
"install": f"Installs the {func_name.split('_', 1)[-1]}.",
|
|
70
|
+
"uninstall": f"Uninstalls the {func_name.split('_', 1)[-1]}.",
|
|
71
|
+
"upgrade": f"Upgrades the {func_name.split('_', 1)[-1]}.",
|
|
72
|
+
"downgrade": f"Downgrades the {func_name.split('_', 1)[-1]}.",
|
|
73
|
+
"configure": f"Configures the {func_name.split('_', 1)[-1]}.",
|
|
74
|
+
"initialize": f"Initializes the {func_name.split('_', 1)[-1]}.",
|
|
75
|
+
"shutdown": f"Shuts down the {func_name.split('_', 1)[-1]}.",
|
|
76
|
+
"restart": f"Restarts the {func_name.split('_', 1)[-1]}.",
|
|
77
|
+
"start": f"Starts the {func_name.split('_', 1)[-1]}.",
|
|
78
|
+
"stop": f"Stops the {func_name.split('_', 1)[-1]}.",
|
|
79
|
+
"pause": f"Pauses the {func_name.split('_', 1)[-1]}.",
|
|
80
|
+
"resume": f"Resumes the {func_name.split('_', 1)[-1]}.",
|
|
81
|
+
"lock": f"Locks the {func_name.split('_', 1)[-1]}.",
|
|
82
|
+
"unlock": f"Unlocks the {func_name.split('_', 1)[-1]}.",
|
|
83
|
+
"authenticate": f"Authenticates the {func_name.split('_', 1)[-1]}.",
|
|
84
|
+
"authorize": f"Authorizes the {func_name.split('_', 1)[-1]}.",
|
|
85
|
+
"log_in": f"Logs in the {func_name.split('_', 1)[-1]}.",
|
|
86
|
+
"log_out": f"Logs out the {func_name.split('_', 1)[-1]}.",
|
|
87
|
+
"register": f"Registers the {func_name.split('_', 1)[-1]}.",
|
|
88
|
+
"deregister": f"Deregisters the {func_name.split('_', 1)[-1]}.",
|
|
89
|
+
"subscribe": f"Subscribes to the {func_name.split('_', 1)[-1]}.",
|
|
90
|
+
"unsubscribe": f"Unsubscribes from the {func_name.split('_', 1)[-1]}.",
|
|
91
|
+
"publish": f"Publishes the {func_name.split('_', 1)[-1]}.",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
action = func_name.split('_')[0] # First part is the action (verb)
|
|
95
|
+
return verb_templates.get(action, f"Executes the function {func_name}.")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type inference module for PyCodeCommenter.
|
|
3
|
+
|
|
4
|
+
This module provides sophisticated type inference for Python code by analyzing
|
|
5
|
+
AST nodes. It supports modern Python features including PEP 604 union types (|),
|
|
6
|
+
PEP 585 generics (list[int]), and complex type annotations.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
TypeAnalyzer: Analyzes Python AST nodes to infer types for variables,
|
|
10
|
+
arguments, and return values
|
|
11
|
+
"""
|
|
12
|
+
import ast
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Dict, Optional, Set, Union
|
|
15
|
+
|
|
16
|
+
# Configure logging
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class TypeAnalyzer:
|
|
20
|
+
"""
|
|
21
|
+
Analyzes Python AST nodes to infer types for variables, arguments, and return values.
|
|
22
|
+
Supports modern Python features like PEP 604 (|) and PEP 585 (generics).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, local_types: Optional[Dict[str, str]] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the analyzer with optional pre-known local types.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
local_types (Optional[Dict[str, str]]): Dictionary mapping variable names to their inferred types.
|
|
31
|
+
"""
|
|
32
|
+
self.local_types = local_types or {}
|
|
33
|
+
|
|
34
|
+
def infer_type(self, node: Any) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Infers the type of an AST node (Argument, Name, Constant, etc.).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
node (Any): The AST node to analyze.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: The inferred type as a string (e.g., 'int', 'List[str]', 'any').
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# Handle Argument nodes (with annotations)
|
|
46
|
+
if isinstance(node, ast.arg):
|
|
47
|
+
if node.annotation:
|
|
48
|
+
return self.get_annotation_type(node.annotation)
|
|
49
|
+
return self.local_types.get(node.arg, "any")
|
|
50
|
+
|
|
51
|
+
# Handle Value nodes (Expressions)
|
|
52
|
+
return self.infer_expr_type(node)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Error in infer_type: {e}")
|
|
55
|
+
return "any"
|
|
56
|
+
|
|
57
|
+
def infer_expr_type(self, expr: Any) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Infers type from an expression node.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
expr (Any): The expression node to analyze.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: The inferred type.
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(expr, ast.Constant):
|
|
68
|
+
return type(expr.value).__name__
|
|
69
|
+
|
|
70
|
+
if isinstance(expr, (ast.List, ast.ListComp)):
|
|
71
|
+
return "list"
|
|
72
|
+
|
|
73
|
+
if isinstance(expr, (ast.Dict, ast.DictComp)):
|
|
74
|
+
return "dict"
|
|
75
|
+
|
|
76
|
+
if isinstance(expr, (ast.Set, ast.SetComp)):
|
|
77
|
+
return "set"
|
|
78
|
+
|
|
79
|
+
if isinstance(expr, (ast.Tuple)):
|
|
80
|
+
return "tuple"
|
|
81
|
+
|
|
82
|
+
if isinstance(expr, ast.Name):
|
|
83
|
+
if expr.id in {'True', 'False'}:
|
|
84
|
+
return "bool"
|
|
85
|
+
if expr.id == 'None':
|
|
86
|
+
return "NoneType"
|
|
87
|
+
return self.local_types.get(expr.id, "any")
|
|
88
|
+
|
|
89
|
+
if isinstance(expr, ast.BinOp):
|
|
90
|
+
return self._infer_binop_type(expr)
|
|
91
|
+
|
|
92
|
+
if isinstance(expr, ast.Call):
|
|
93
|
+
return self._infer_call_type(expr)
|
|
94
|
+
|
|
95
|
+
if isinstance(expr, ast.Attribute):
|
|
96
|
+
return self._infer_attribute_type(expr)
|
|
97
|
+
|
|
98
|
+
return "any"
|
|
99
|
+
|
|
100
|
+
def get_annotation_type(self, annotation: Any) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Translates an AST annotation node into a human-readable type string.
|
|
103
|
+
Supports PEP 604 (|) and PEP 585 (list[int]).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
annotation (Any): The annotation node (ast.Name, ast.Subscript, etc.).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
str: The type string.
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(annotation, ast.Name):
|
|
112
|
+
return annotation.id
|
|
113
|
+
|
|
114
|
+
if isinstance(annotation, ast.Constant) and annotation.value is None:
|
|
115
|
+
return "None"
|
|
116
|
+
|
|
117
|
+
if isinstance(annotation, ast.Attribute):
|
|
118
|
+
value_id = self.get_annotation_type(annotation.value)
|
|
119
|
+
return f"{value_id}.{annotation.attr}"
|
|
120
|
+
|
|
121
|
+
if isinstance(annotation, ast.Subscript):
|
|
122
|
+
base_type = self.get_annotation_type(annotation.value)
|
|
123
|
+
# Python 3.9+ uses Slice for index in some cases, but 3.10+ simplified it
|
|
124
|
+
index = annotation.slice
|
|
125
|
+
if isinstance(index, ast.Index): # Older Python
|
|
126
|
+
index = index.value
|
|
127
|
+
|
|
128
|
+
# Handle Tuple/List of types inside subscript
|
|
129
|
+
if isinstance(index, ast.Tuple):
|
|
130
|
+
inner_types = ", ".join(self.get_annotation_type(elt) for elt in index.elts)
|
|
131
|
+
return f"{base_type}[{inner_types}]"
|
|
132
|
+
|
|
133
|
+
inner_type = self.get_annotation_type(index)
|
|
134
|
+
return f"{base_type}[{inner_type}]"
|
|
135
|
+
|
|
136
|
+
if isinstance(annotation, ast.BinOp):
|
|
137
|
+
if isinstance(annotation.op, ast.BitOr): # PEP 604: int | str
|
|
138
|
+
left = self.get_annotation_type(annotation.left)
|
|
139
|
+
right = self.get_annotation_type(annotation.right)
|
|
140
|
+
return f"Union[{left}, {right}]"
|
|
141
|
+
|
|
142
|
+
return "any"
|
|
143
|
+
|
|
144
|
+
def _infer_binop_type(self, node: ast.BinOp) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Infers the result of a binary operation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
node (ast.BinOp): The binary operation node.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The inferred type.
|
|
153
|
+
"""
|
|
154
|
+
left = self.infer_expr_type(node.left)
|
|
155
|
+
right = self.infer_expr_type(node.right)
|
|
156
|
+
|
|
157
|
+
if left == "float" or right == "float":
|
|
158
|
+
return "float"
|
|
159
|
+
|
|
160
|
+
if isinstance(node.op, ast.Div):
|
|
161
|
+
return "float"
|
|
162
|
+
|
|
163
|
+
if left != "any":
|
|
164
|
+
return left
|
|
165
|
+
return right
|
|
166
|
+
|
|
167
|
+
def _infer_call_type(self, node: ast.Call) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Infers return type from a function call (basic heuristic).
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
node (ast.Call): The call node to analyze.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: The inferred return type.
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(node.func, ast.Name):
|
|
178
|
+
name = node.func.id
|
|
179
|
+
if name in {'int', 'float', 'str', 'list', 'dict', 'set', 'bool'}:
|
|
180
|
+
return name
|
|
181
|
+
if name == 'len':
|
|
182
|
+
return "int"
|
|
183
|
+
|
|
184
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
|
185
|
+
if node.func.value.id == 'math':
|
|
186
|
+
return "float"
|
|
187
|
+
|
|
188
|
+
return "any"
|
|
189
|
+
|
|
190
|
+
def _infer_attribute_type(self, node: ast.Attribute) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Infers type of an attribute access.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
node (ast.Attribute): The attribute node.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str: The inferred type.
|
|
199
|
+
"""
|
|
200
|
+
if isinstance(node.value, ast.Name):
|
|
201
|
+
if node.value.id == 'math' and node.attr in {'pi', 'e', 'tau'}:
|
|
202
|
+
return "float"
|
|
203
|
+
return "any"
|