universal-mcp 0.1.15rc5__py3-none-any.whl → 0.1.16__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.
- universal_mcp/analytics.py +7 -1
- universal_mcp/applications/README.md +122 -0
- universal_mcp/applications/__init__.py +51 -56
- universal_mcp/applications/application.py +255 -82
- universal_mcp/cli.py +27 -43
- universal_mcp/config.py +16 -48
- universal_mcp/exceptions.py +8 -0
- universal_mcp/integrations/__init__.py +1 -3
- universal_mcp/integrations/integration.py +18 -2
- universal_mcp/logger.py +31 -29
- universal_mcp/servers/server.py +6 -18
- universal_mcp/stores/store.py +2 -12
- universal_mcp/tools/__init__.py +12 -1
- universal_mcp/tools/adapters.py +11 -0
- universal_mcp/tools/func_metadata.py +11 -15
- universal_mcp/tools/manager.py +163 -117
- universal_mcp/tools/tools.py +6 -13
- universal_mcp/utils/agentr.py +2 -6
- universal_mcp/utils/common.py +33 -0
- universal_mcp/utils/docstring_parser.py +4 -13
- universal_mcp/utils/installation.py +67 -184
- universal_mcp/utils/openapi/__inti__.py +0 -0
- universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +2 -4
- universal_mcp/utils/{docgen.py → openapi/docgen.py} +17 -54
- universal_mcp/utils/openapi/openapi.py +882 -0
- universal_mcp/utils/openapi/preprocessor.py +1093 -0
- universal_mcp/utils/{readme.py → openapi/readme.py} +21 -37
- universal_mcp-0.1.16.dist-info/METADATA +282 -0
- universal_mcp-0.1.16.dist-info/RECORD +44 -0
- universal_mcp-0.1.16.dist-info/licenses/LICENSE +21 -0
- universal_mcp/utils/openapi.py +0 -646
- universal_mcp-0.1.15rc5.dist-info/METADATA +0 -245
- universal_mcp-0.1.15rc5.dist-info/RECORD +0 -39
- /universal_mcp/{templates → utils/templates}/README.md.j2 +0 -0
- /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
- {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/entry_points.txt +0 -0
@@ -7,19 +7,30 @@ from loguru import logger
|
|
7
7
|
from rich import print
|
8
8
|
|
9
9
|
|
10
|
-
def get_uvx_path() ->
|
10
|
+
def get_uvx_path() -> Path:
|
11
11
|
"""Get the full path to the uv executable."""
|
12
12
|
uvx_path = shutil.which("uvx")
|
13
|
-
if
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
13
|
+
if uvx_path:
|
14
|
+
return Path(uvx_path)
|
15
|
+
|
16
|
+
# Check if
|
17
|
+
common_uv_paths = [
|
18
|
+
Path.home() / ".local/bin/uvx", # Linux/macOS
|
19
|
+
Path.home() / ".cargo/bin/uvx", # Linux/macOS
|
20
|
+
Path.home() / "AppData/Local/Programs/uvx/uvx.exe", # Windows
|
21
|
+
Path.home() / "AppData/Roaming/uvx/uvx.exe", # Windows
|
22
|
+
]
|
23
|
+
for path in common_uv_paths:
|
24
|
+
logger.info(f"Checking {path}")
|
25
|
+
if path.exists():
|
26
|
+
return path
|
27
|
+
logger.error(
|
28
|
+
"uvx executable not found in PATH, falling back to 'uvx'. Please ensure uvx is installed and in your PATH"
|
29
|
+
)
|
30
|
+
return None # Fall back to just "uvx" if not found
|
31
|
+
|
32
|
+
|
33
|
+
def _create_file_if_not_exists(path: Path) -> None:
|
23
34
|
"""Create a file if it doesn't exist"""
|
24
35
|
if not path.exists():
|
25
36
|
print(f"[yellow]Creating config file at {path}[/yellow]")
|
@@ -27,6 +38,34 @@ def create_file_if_not_exists(path: Path) -> None:
|
|
27
38
|
json.dump({}, f)
|
28
39
|
|
29
40
|
|
41
|
+
def _generate_mcp_config(api_key: str) -> None:
|
42
|
+
uvx_path = get_uvx_path()
|
43
|
+
if not uvx_path:
|
44
|
+
raise ValueError("uvx executable not found in PATH")
|
45
|
+
return {
|
46
|
+
"command": str(uvx_path),
|
47
|
+
"args": ["universal_mcp@latest", "run"],
|
48
|
+
"env": {
|
49
|
+
"AGENTR_API_KEY": api_key,
|
50
|
+
"PATH": str(uvx_path.parent),
|
51
|
+
},
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
def _install_config(config_path: Path, mcp_config: dict):
|
56
|
+
_create_file_if_not_exists(config_path)
|
57
|
+
try:
|
58
|
+
config = json.loads(config_path.read_text())
|
59
|
+
except json.JSONDecodeError:
|
60
|
+
print("[yellow]Config file was empty or invalid, creating new configuration[/yellow]")
|
61
|
+
config = {}
|
62
|
+
if "mcpServers" not in config:
|
63
|
+
config["mcpServers"] = {}
|
64
|
+
config["mcpServers"]["universal_mcp"] = mcp_config
|
65
|
+
with open(config_path, "w") as f:
|
66
|
+
json.dump(config, f, indent=4)
|
67
|
+
|
68
|
+
|
30
69
|
def get_supported_apps() -> list[str]:
|
31
70
|
"""Get list of supported apps"""
|
32
71
|
return ["claude", "cursor", "cline", "continue", "goose", "windsurf", "zed"]
|
@@ -37,35 +76,17 @@ def install_claude(api_key: str) -> None:
|
|
37
76
|
print("[bold blue]Installing Claude configuration...[/bold blue]")
|
38
77
|
# Determine platform-specific config path
|
39
78
|
if sys.platform == "darwin": # macOS
|
40
|
-
config_path = (
|
41
|
-
Path.home()
|
42
|
-
/ "Library/Application Support/Claude/claude_desktop_config.json"
|
43
|
-
)
|
79
|
+
config_path = Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
|
44
80
|
elif sys.platform == "win32": # Windows
|
45
81
|
config_path = Path.home() / "AppData/Roaming/Claude/claude_desktop_config.json"
|
46
82
|
else:
|
47
83
|
raise ValueError(
|
48
84
|
"Unsupported platform. Only macOS and Windows are currently supported.",
|
49
85
|
)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
config = json.loads(config_path.read_text())
|
55
|
-
except json.JSONDecodeError:
|
56
|
-
print(
|
57
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
58
|
-
)
|
59
|
-
config = {}
|
60
|
-
if "mcpServers" not in config:
|
61
|
-
config["mcpServers"] = {}
|
62
|
-
config["mcpServers"]["universal_mcp"] = {
|
63
|
-
"command": get_uvx_path(),
|
64
|
-
"args": ["universal_mcp@latest", "run"],
|
65
|
-
"env": {"AGENTR_API_KEY": api_key},
|
66
|
-
}
|
67
|
-
with open(config_path, "w") as f:
|
68
|
-
json.dump(config, f, indent=4)
|
86
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
87
|
+
logger.info(f"Installing Claude configuration at {config_path}")
|
88
|
+
logger.info(f"Config: {mcp_config}")
|
89
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
69
90
|
print("[green]✓[/green] Claude configuration installed successfully")
|
70
91
|
|
71
92
|
|
@@ -74,28 +95,8 @@ def install_cursor(api_key: str) -> None:
|
|
74
95
|
print("[bold blue]Installing Cursor configuration...[/bold blue]")
|
75
96
|
# Set up Cursor config path
|
76
97
|
config_path = Path.home() / ".cursor/mcp.json"
|
77
|
-
|
78
|
-
|
79
|
-
create_file_if_not_exists(config_path)
|
80
|
-
|
81
|
-
try:
|
82
|
-
config = json.loads(config_path.read_text())
|
83
|
-
except json.JSONDecodeError:
|
84
|
-
print(
|
85
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
86
|
-
)
|
87
|
-
config = {}
|
88
|
-
|
89
|
-
if "mcpServers" not in config:
|
90
|
-
config["mcpServers"] = {}
|
91
|
-
config["mcpServers"]["universal_mcp"] = {
|
92
|
-
"command": get_uvx_path(),
|
93
|
-
"args": ["universal_mcp@latest", "run"],
|
94
|
-
"env": {"AGENTR_API_KEY": api_key},
|
95
|
-
}
|
96
|
-
|
97
|
-
with open(config_path, "w") as f:
|
98
|
-
json.dump(config, f, indent=4)
|
98
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
99
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
99
100
|
print("[green]✓[/green] Cursor configuration installed successfully")
|
100
101
|
|
101
102
|
|
@@ -104,28 +105,8 @@ def install_cline(api_key: str) -> None:
|
|
104
105
|
print("[bold blue]Installing Cline configuration...[/bold blue]")
|
105
106
|
# Set up Cline config path
|
106
107
|
config_path = Path.home() / ".config/cline/mcp.json"
|
107
|
-
|
108
|
-
|
109
|
-
create_file_if_not_exists(config_path)
|
110
|
-
|
111
|
-
try:
|
112
|
-
config = json.loads(config_path.read_text())
|
113
|
-
except json.JSONDecodeError:
|
114
|
-
print(
|
115
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
116
|
-
)
|
117
|
-
config = {}
|
118
|
-
|
119
|
-
if "mcpServers" not in config:
|
120
|
-
config["mcpServers"] = {}
|
121
|
-
config["mcpServers"]["universal_mcp"] = {
|
122
|
-
"command": get_uvx_path(),
|
123
|
-
"args": ["universal_mcp@latest", "run"],
|
124
|
-
"env": {"AGENTR_API_KEY": api_key},
|
125
|
-
}
|
126
|
-
|
127
|
-
with open(config_path, "w") as f:
|
128
|
-
json.dump(config, f, indent=4)
|
108
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
109
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
129
110
|
print("[green]✓[/green] Cline configuration installed successfully")
|
130
111
|
|
131
112
|
|
@@ -140,28 +121,8 @@ def install_continue(api_key: str) -> None:
|
|
140
121
|
config_path = Path.home() / "AppData/Roaming/Continue/mcp.json"
|
141
122
|
else: # Linux and others
|
142
123
|
config_path = Path.home() / ".config/continue/mcp.json"
|
143
|
-
|
144
|
-
|
145
|
-
create_file_if_not_exists(config_path)
|
146
|
-
|
147
|
-
try:
|
148
|
-
config = json.loads(config_path.read_text())
|
149
|
-
except json.JSONDecodeError:
|
150
|
-
print(
|
151
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
152
|
-
)
|
153
|
-
config = {}
|
154
|
-
|
155
|
-
if "mcpServers" not in config:
|
156
|
-
config["mcpServers"] = {}
|
157
|
-
config["mcpServers"]["universal_mcp"] = {
|
158
|
-
"command": get_uvx_path(),
|
159
|
-
"args": ["universal_mcp@latest", "run"],
|
160
|
-
"env": {"AGENTR_API_KEY": api_key},
|
161
|
-
}
|
162
|
-
|
163
|
-
with open(config_path, "w") as f:
|
164
|
-
json.dump(config, f, indent=4)
|
124
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
125
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
165
126
|
print("[green]✓[/green] Continue configuration installed successfully")
|
166
127
|
|
167
128
|
|
@@ -177,27 +138,8 @@ def install_goose(api_key: str) -> None:
|
|
177
138
|
else: # Linux and others
|
178
139
|
config_path = Path.home() / ".config/goose/mcp-config.json"
|
179
140
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
try:
|
184
|
-
config = json.loads(config_path.read_text())
|
185
|
-
except json.JSONDecodeError:
|
186
|
-
print(
|
187
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
188
|
-
)
|
189
|
-
config = {}
|
190
|
-
|
191
|
-
if "mcpServers" not in config:
|
192
|
-
config["mcpServers"] = {}
|
193
|
-
config["mcpServers"]["universal_mcp"] = {
|
194
|
-
"command": get_uvx_path(),
|
195
|
-
"args": ["universal_mcp@latest", "run"],
|
196
|
-
"env": {"AGENTR_API_KEY": api_key},
|
197
|
-
}
|
198
|
-
|
199
|
-
with open(config_path, "w") as f:
|
200
|
-
json.dump(config, f, indent=4)
|
141
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
142
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
201
143
|
print("[green]✓[/green] Goose configuration installed successfully")
|
202
144
|
|
203
145
|
|
@@ -212,28 +154,8 @@ def install_windsurf(api_key: str) -> None:
|
|
212
154
|
config_path = Path.home() / "AppData/Roaming/Windsurf/mcp.json"
|
213
155
|
else: # Linux and others
|
214
156
|
config_path = Path.home() / ".config/windsurf/mcp.json"
|
215
|
-
|
216
|
-
|
217
|
-
create_file_if_not_exists(config_path)
|
218
|
-
|
219
|
-
try:
|
220
|
-
config = json.loads(config_path.read_text())
|
221
|
-
except json.JSONDecodeError:
|
222
|
-
print(
|
223
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
224
|
-
)
|
225
|
-
config = {}
|
226
|
-
|
227
|
-
if "mcpServers" not in config:
|
228
|
-
config["mcpServers"] = {}
|
229
|
-
config["mcpServers"]["universal_mcp"] = {
|
230
|
-
"command": get_uvx_path(),
|
231
|
-
"args": ["universal_mcp@latest", "run"],
|
232
|
-
"env": {"AGENTR_API_KEY": api_key},
|
233
|
-
}
|
234
|
-
|
235
|
-
with open(config_path, "w") as f:
|
236
|
-
json.dump(config, f, indent=4)
|
157
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
158
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
237
159
|
print("[green]✓[/green] Windsurf configuration installed successfully")
|
238
160
|
|
239
161
|
|
@@ -244,47 +166,8 @@ def install_zed(api_key: str) -> None:
|
|
244
166
|
# Set up Zed config path
|
245
167
|
config_dir = Path.home() / ".config/zed"
|
246
168
|
config_path = config_dir / "mcp_servers.json"
|
247
|
-
|
248
|
-
|
249
|
-
create_file_if_not_exists(config_path)
|
250
|
-
|
251
|
-
try:
|
252
|
-
config = json.loads(config_path.read_text())
|
253
|
-
except json.JSONDecodeError:
|
254
|
-
print(
|
255
|
-
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
256
|
-
)
|
257
|
-
config = {}
|
258
|
-
|
259
|
-
if not isinstance(config, list):
|
260
|
-
config = []
|
261
|
-
|
262
|
-
# Check if universal_mcp is already in the config
|
263
|
-
existing_config = False
|
264
|
-
for server in config:
|
265
|
-
if server.get("name") == "universal_mcp":
|
266
|
-
existing_config = True
|
267
|
-
server.update(
|
268
|
-
{
|
269
|
-
"command": get_uvx_path(),
|
270
|
-
"args": ["universal_mcp@latest", "run"],
|
271
|
-
"env": {"AGENTR_API_KEY": api_key},
|
272
|
-
}
|
273
|
-
)
|
274
|
-
break
|
275
|
-
|
276
|
-
if not existing_config:
|
277
|
-
config.append(
|
278
|
-
{
|
279
|
-
"name": "universal_mcp",
|
280
|
-
"command": get_uvx_path(),
|
281
|
-
"args": ["universal_mcp@latest", "run"],
|
282
|
-
"env": {"AGENTR_API_KEY": api_key},
|
283
|
-
}
|
284
|
-
)
|
285
|
-
|
286
|
-
with open(config_path, "w") as f:
|
287
|
-
json.dump(config, f, indent=4)
|
169
|
+
mcp_config = _generate_mcp_config(api_key=api_key)
|
170
|
+
_install_config(config_path=config_path, mcp_config=mcp_config)
|
288
171
|
print("[green]✓[/green] Zed configuration installed successfully")
|
289
172
|
|
290
173
|
|
File without changes
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
6
6
|
|
7
7
|
from loguru import logger
|
8
8
|
|
9
|
-
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
9
|
+
from universal_mcp.utils.openapi.openapi import generate_api_client, load_schema
|
10
10
|
|
11
11
|
|
12
12
|
def echo(message: str, err: bool = False) -> None:
|
@@ -109,9 +109,7 @@ def generate_api_from_schema(
|
|
109
109
|
f.write(code)
|
110
110
|
|
111
111
|
if not test_correct_output(gen_file):
|
112
|
-
logger.error(
|
113
|
-
"Generated code validation failed for '%s'. Aborting generation.", gen_file
|
114
|
-
)
|
112
|
+
logger.error("Generated code validation failed for '%s'. Aborting generation.", gen_file)
|
115
113
|
logger.info("Next steps:")
|
116
114
|
logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
|
117
115
|
logger.info(
|
@@ -19,12 +19,8 @@ from pydantic import BaseModel, Field
|
|
19
19
|
class DocstringOutput(BaseModel):
|
20
20
|
"""Structure for the generated docstring output."""
|
21
21
|
|
22
|
-
summary: str = Field(
|
23
|
-
|
24
|
-
)
|
25
|
-
args: dict[str, str] = Field(
|
26
|
-
description="Dictionary mapping parameter names to their descriptions"
|
27
|
-
)
|
22
|
+
summary: str = Field(description="A clear, concise summary of what the function does")
|
23
|
+
args: dict[str, str] = Field(description="Dictionary mapping parameter names to their descriptions")
|
28
24
|
returns: str = Field(description="Description of what the function returns")
|
29
25
|
raises: dict[str, str] = Field(
|
30
26
|
default_factory=dict,
|
@@ -44,9 +40,7 @@ class FunctionExtractor(ast.NodeVisitor):
|
|
44
40
|
|
45
41
|
def __init__(self, source_code: str):
|
46
42
|
self.source_lines = source_code.splitlines(keepends=True)
|
47
|
-
self.functions: list[
|
48
|
-
tuple[str, str]
|
49
|
-
] = [] # Store tuples of (function_name, function_source)
|
43
|
+
self.functions: list[tuple[str, str]] = [] # Store tuples of (function_name, function_source)
|
50
44
|
|
51
45
|
def _get_source_segment(self, node: ast.AST) -> str | None:
|
52
46
|
"""Safely extracts the source segment for a node using ast.get_source_segment."""
|
@@ -110,9 +104,7 @@ def extract_functions_from_script(file_path: str) -> list[tuple[str, str]]:
|
|
110
104
|
try:
|
111
105
|
tree = ast.parse(source_code, filename=file_path)
|
112
106
|
except SyntaxError as e:
|
113
|
-
print(
|
114
|
-
f"Error: Invalid Python syntax in {file_path} at line {e.lineno}, offset {e.offset}: {e.msg}"
|
115
|
-
)
|
107
|
+
print(f"Error: Invalid Python syntax in {file_path} at line {e.lineno}, offset {e.offset}: {e.msg}")
|
116
108
|
raise
|
117
109
|
except Exception as e:
|
118
110
|
print(f"Error parsing {file_path} into AST: {e}")
|
@@ -175,9 +167,7 @@ def extract_json_from_text(text):
|
|
175
167
|
raise ValueError("Could not extract valid JSON from the response") from e
|
176
168
|
|
177
169
|
|
178
|
-
def generate_docstring(
|
179
|
-
function_code: str, model: str = "perplexity/sonar"
|
180
|
-
) -> DocstringOutput:
|
170
|
+
def generate_docstring(function_code: str, model: str = "perplexity/sonar") -> DocstringOutput:
|
181
171
|
"""
|
182
172
|
Generate a docstring for a Python function using litellm with structured output.
|
183
173
|
|
@@ -239,9 +229,7 @@ def generate_docstring(
|
|
239
229
|
parsed_data = extract_json_from_text(response_text)
|
240
230
|
except ValueError as e:
|
241
231
|
print(f"JSON extraction failed: {e}")
|
242
|
-
print(
|
243
|
-
f"Raw response: {response_text[:100]}..."
|
244
|
-
) # Log first 100 chars for debugging
|
232
|
+
print(f"Raw response: {response_text[:100]}...") # Log first 100 chars for debugging
|
245
233
|
# Return a default structure if extraction fails
|
246
234
|
return DocstringOutput(
|
247
235
|
summary="Failed to extract docstring information",
|
@@ -291,20 +279,14 @@ def format_docstring(docstring: DocstringOutput) -> str:
|
|
291
279
|
if summary:
|
292
280
|
parts.append(summary)
|
293
281
|
|
294
|
-
filtered_args = {
|
295
|
-
name: desc
|
296
|
-
for name, desc in docstring.args.items()
|
297
|
-
if name not in ("self", "cls")
|
298
|
-
}
|
282
|
+
filtered_args = {name: desc for name, desc in docstring.args.items() if name not in ("self", "cls")}
|
299
283
|
args_lines = []
|
300
284
|
if filtered_args:
|
301
285
|
args_lines.append("Args:")
|
302
286
|
for arg_name, arg_desc in filtered_args.items():
|
303
287
|
arg_desc_cleaned = arg_desc.strip()
|
304
288
|
args_lines.append(f" {arg_name}: {arg_desc_cleaned}")
|
305
|
-
elif docstring.args.get(
|
306
|
-
"None"
|
307
|
-
): # Include the 'None' placeholder if it was generated
|
289
|
+
elif docstring.args.get("None"): # Include the 'None' placeholder if it was generated
|
308
290
|
args_lines.append("Args:")
|
309
291
|
none_desc_cleaned = docstring.args["None"].strip()
|
310
292
|
args_lines.append(f" None: {none_desc_cleaned}")
|
@@ -321,12 +303,8 @@ def format_docstring(docstring: DocstringOutput) -> str:
|
|
321
303
|
raises_lines.append("Raises:")
|
322
304
|
for exception_type, exception_desc in docstring.raises.items():
|
323
305
|
exception_desc_cleaned = exception_desc.strip()
|
324
|
-
if (
|
325
|
-
exception_type.strip()
|
326
|
-
): # Ensure type and desc are not empty
|
327
|
-
raises_lines.append(
|
328
|
-
f" {exception_type.strip()}: {exception_desc_cleaned}"
|
329
|
-
)
|
306
|
+
if exception_type.strip() and exception_desc_cleaned: # Ensure type and desc are not empty
|
307
|
+
raises_lines.append(f" {exception_type.strip()}: {exception_desc_cleaned}")
|
330
308
|
if raises_lines:
|
331
309
|
parts.append("\n".join(raises_lines))
|
332
310
|
|
@@ -363,9 +341,7 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
363
341
|
lines = function_code.splitlines(keepends=True)
|
364
342
|
|
365
343
|
tree = ast.parse(function_code)
|
366
|
-
if not tree.body or not isinstance(
|
367
|
-
tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
|
368
|
-
):
|
344
|
+
if not tree.body or not isinstance(tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef):
|
369
345
|
print(
|
370
346
|
"Warning: Could not parse function definition from code snippet. Returning original code.",
|
371
347
|
file=sys.stderr,
|
@@ -399,15 +375,11 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
399
375
|
if func_node.lineno - 1 < len(lines): # Ensure def line exists
|
400
376
|
def_line = lines[func_node.lineno - 1]
|
401
377
|
def_line_indent = def_line[: len(def_line) - len(def_line.lstrip())]
|
402
|
-
body_indent =
|
403
|
-
def_line_indent + " "
|
404
|
-
) # Standard 4 spaces relative indent
|
378
|
+
body_indent = def_line_indent + " " # Standard 4 spaces relative indent
|
405
379
|
|
406
380
|
# Format the new docstring lines with the calculated indentation
|
407
381
|
new_docstring_lines_formatted = [f'{body_indent}"""\n']
|
408
|
-
new_docstring_lines_formatted.extend(
|
409
|
-
[f"{body_indent}{line}\n" for line in docstring.splitlines()]
|
410
|
-
)
|
382
|
+
new_docstring_lines_formatted.extend([f"{body_indent}{line}\n" for line in docstring.splitlines()])
|
411
383
|
new_docstring_lines_formatted.append(f'{body_indent}"""\n')
|
412
384
|
|
413
385
|
output_lines = []
|
@@ -425,10 +397,7 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
425
397
|
dummy_tree = ast.parse(dummy_code)
|
426
398
|
dummy_body_statements = (
|
427
399
|
dummy_tree.body[0].body
|
428
|
-
if dummy_tree.body
|
429
|
-
and isinstance(
|
430
|
-
dummy_tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
|
431
|
-
)
|
400
|
+
if dummy_tree.body and isinstance(dummy_tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef)
|
432
401
|
else []
|
433
402
|
)
|
434
403
|
cleaned_body_parts = []
|
@@ -455,13 +424,9 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
455
424
|
|
456
425
|
if not is_just_string_stmt:
|
457
426
|
stmt_start_idx = stmt_node.lineno - 1
|
458
|
-
stmt_end_idx =
|
459
|
-
stmt_node.end_lineno - 1
|
460
|
-
) # Inclusive end line index
|
427
|
+
stmt_end_idx = stmt_node.end_lineno - 1 # Inclusive end line index
|
461
428
|
|
462
|
-
cleaned_body_parts.extend(
|
463
|
-
lines[stmt_start_idx : stmt_end_idx + 1]
|
464
|
-
)
|
429
|
+
cleaned_body_parts.extend(lines[stmt_start_idx : stmt_end_idx + 1])
|
465
430
|
|
466
431
|
if func_node.body:
|
467
432
|
last_stmt_end_idx = func_node.body[-1].end_lineno - 1
|
@@ -545,9 +510,7 @@ def process_file(file_path: str, model: str = "perplexity/sonar") -> int:
|
|
545
510
|
formatted_docstring = format_docstring(docstring_output)
|
546
511
|
|
547
512
|
# Insert docstring into function
|
548
|
-
updated_function = insert_docstring_into_function(
|
549
|
-
function_code, formatted_docstring
|
550
|
-
)
|
513
|
+
updated_function = insert_docstring_into_function(function_code, formatted_docstring)
|
551
514
|
|
552
515
|
# Replace the function in the file content
|
553
516
|
if updated_function != function_code:
|