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.
@@ -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"