pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/auto_update.py
CHANGED
|
@@ -1,67 +1,111 @@
|
|
|
1
|
-
"""This module provides a function to automatically update the package.
|
|
1
|
+
"""This module provides a function to automatically update the package.
|
|
2
|
+
|
|
3
|
+
It can detect whether the current tool was installed via UV or pip, check
|
|
4
|
+
PyPI for a newer version, prompt the user, and perform an upgrade using the
|
|
5
|
+
appropriate installer with sensible fallbacks.
|
|
6
|
+
"""
|
|
2
7
|
import importlib.metadata
|
|
8
|
+
import os
|
|
3
9
|
import shutil
|
|
4
10
|
import subprocess
|
|
5
11
|
import sys
|
|
6
|
-
from typing import Optional
|
|
12
|
+
from typing import Optional, Tuple, List
|
|
13
|
+
|
|
7
14
|
import requests
|
|
8
15
|
import semver
|
|
9
16
|
|
|
10
17
|
|
|
11
|
-
def detect_installation_method(sys_executable):
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
def detect_installation_method(sys_executable: str) -> str:
|
|
19
|
+
"""Detect whether the package is installed via UV or pip.
|
|
20
|
+
|
|
21
|
+
The detection is based on the path of the Python executable. The path is
|
|
22
|
+
normalized so that both Unix ("/") and Windows ("\\") separators are
|
|
23
|
+
handled uniformly. If typical UV tool installation markers are present in
|
|
24
|
+
the normalized path, "uv" is returned; otherwise "pip" is returned.
|
|
25
|
+
|
|
15
26
|
Args:
|
|
16
|
-
sys_executable
|
|
17
|
-
|
|
27
|
+
sys_executable: Path to the Python executable (e.g. ``sys.executable``).
|
|
28
|
+
|
|
18
29
|
Returns:
|
|
19
|
-
|
|
30
|
+
"uv" if a UV-style installation path is detected, otherwise "pip".
|
|
20
31
|
"""
|
|
32
|
+
# Normalize path separators to support both Unix (/) and Windows (\) paths
|
|
33
|
+
normalized_path = sys_executable.replace("\\", "/")
|
|
34
|
+
|
|
21
35
|
# Check if executable path contains UV paths
|
|
22
|
-
if any(marker in
|
|
36
|
+
if any(marker in normalized_path for marker in ["/uv/tools/", ".local/share/uv/"]):
|
|
23
37
|
return "uv"
|
|
24
38
|
return "pip" # Default to pip for all other cases
|
|
25
39
|
|
|
26
40
|
|
|
27
|
-
def get_upgrade_command(package_name, installation_method):
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
def get_upgrade_command(package_name: str, installation_method: str) -> Tuple[List[str], bool]:
|
|
42
|
+
"""Build the appropriate upgrade command based on the installation method.
|
|
43
|
+
|
|
44
|
+
For UV, this uses ``uv tool install --force``. For pip, this uses
|
|
45
|
+
``python -m pip install --upgrade`` with the current Python executable.
|
|
46
|
+
|
|
31
47
|
Args:
|
|
32
|
-
package_name
|
|
33
|
-
installation_method
|
|
34
|
-
|
|
48
|
+
package_name: Name of the package to upgrade.
|
|
49
|
+
installation_method: Either ``"uv"`` or ``"pip"``.
|
|
50
|
+
|
|
35
51
|
Returns:
|
|
36
|
-
tuple
|
|
37
|
-
|
|
52
|
+
A tuple ``(command_list, shell_mode)`` where ``command_list`` is the
|
|
53
|
+
command and its arguments as a list of strings, and ``shell_mode`` is a
|
|
54
|
+
boolean indicating whether ``subprocess.run`` should be invoked with
|
|
55
|
+
``shell=True``.
|
|
38
56
|
"""
|
|
39
57
|
if installation_method == "uv":
|
|
40
58
|
# For UV commands, we need the full path if available
|
|
41
59
|
uv_path = shutil.which("uv")
|
|
42
60
|
if uv_path:
|
|
43
|
-
return
|
|
61
|
+
return [uv_path, "tool", "install", package_name, "--force"], False
|
|
44
62
|
# If uv isn't in PATH, use shell=True
|
|
45
|
-
return
|
|
63
|
+
return ["uv", "tool", "install", package_name, "--force"], True
|
|
64
|
+
|
|
46
65
|
# Default pip method
|
|
47
|
-
return
|
|
66
|
+
return [sys.executable, "-m", "pip", "install", "--upgrade", package_name], False
|
|
48
67
|
|
|
49
68
|
|
|
50
69
|
def _get_latest_version(package_name: str) -> Optional[str]:
|
|
51
|
-
"""Fetch the latest version of a package from PyPI.
|
|
70
|
+
"""Fetch the latest version of a package from PyPI.
|
|
71
|
+
|
|
72
|
+
This queries the JSON API at ``https://pypi.org/pypi/<package_name>/json``
|
|
73
|
+
with a timeout and extracts the ``info.version`` field.
|
|
74
|
+
|
|
75
|
+
Any exception results in a user-friendly error message and ``None`` being
|
|
76
|
+
returned.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
package_name: The name of the package to query.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The latest version string if it could be fetched, otherwise ``None``.
|
|
83
|
+
"""
|
|
52
84
|
# pylint: disable=broad-except
|
|
53
85
|
try:
|
|
54
86
|
pypi_url = f"https://pypi.org/pypi/{package_name}/json"
|
|
55
87
|
response = requests.get(pypi_url, timeout=10)
|
|
56
88
|
response.raise_for_status()
|
|
57
|
-
return response.json()[
|
|
58
|
-
except Exception as ex:
|
|
89
|
+
return response.json()["info"]["version"]
|
|
90
|
+
except Exception as ex: # noqa: BLE001
|
|
59
91
|
print(f"Failed to fetch latest version from PyPI: {str(ex)}")
|
|
60
92
|
return None
|
|
61
93
|
|
|
62
94
|
|
|
63
|
-
def _upgrade_package(package_name: str, installation_method: str):
|
|
64
|
-
"""Upgrade a package using the specified installation method.
|
|
95
|
+
def _upgrade_package(package_name: str, installation_method: str) -> bool:
|
|
96
|
+
"""Upgrade a package using the specified installation method.
|
|
97
|
+
|
|
98
|
+
This runs the command returned by :func:`get_upgrade_command`, captures
|
|
99
|
+
stdout/stderr, and reports success or failure.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
package_name: Name of the package to upgrade.
|
|
103
|
+
installation_method: Either ``"uv"`` or ``"pip"``.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
``True`` if the upgrade command succeeded (exit code 0), otherwise
|
|
107
|
+
``False``.
|
|
108
|
+
"""
|
|
65
109
|
cmd, use_shell = get_upgrade_command(package_name, installation_method)
|
|
66
110
|
cmd_str = " ".join(cmd)
|
|
67
111
|
print(f"\nDetected installation method: {installation_method}")
|
|
@@ -69,25 +113,38 @@ def _upgrade_package(package_name: str, installation_method: str):
|
|
|
69
113
|
|
|
70
114
|
# pylint: disable=broad-except
|
|
71
115
|
try:
|
|
72
|
-
result = subprocess.run(
|
|
116
|
+
result = subprocess.run( # noqa: PLW1510
|
|
73
117
|
cmd,
|
|
74
118
|
shell=use_shell,
|
|
75
119
|
capture_output=True,
|
|
76
120
|
text=True,
|
|
77
|
-
check=False
|
|
121
|
+
check=False,
|
|
78
122
|
)
|
|
79
123
|
if result.returncode == 0:
|
|
80
124
|
print(f"\nSuccessfully upgraded {package_name}")
|
|
81
125
|
return True
|
|
82
126
|
print(f"\nUpgrade command failed: {result.stderr}")
|
|
83
127
|
return False
|
|
84
|
-
except Exception as ex:
|
|
128
|
+
except Exception as ex: # noqa: BLE001
|
|
85
129
|
print(f"\nError during upgrade: {str(ex)}")
|
|
86
130
|
return False
|
|
87
131
|
|
|
88
132
|
|
|
89
133
|
def _is_new_version_available(current_version: str, latest_version: str) -> bool:
|
|
90
|
-
"""
|
|
134
|
+
"""Determine whether a newer version is available.
|
|
135
|
+
|
|
136
|
+
Semantic versioning is used when possible via :mod:`semver`. If parsing
|
|
137
|
+
fails for either version string, the function falls back to a simple
|
|
138
|
+
inequality comparison of the raw strings.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
current_version: The currently installed version string.
|
|
142
|
+
latest_version: The latest available version string.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
``True`` if ``latest_version`` is considered newer than
|
|
146
|
+
``current_version``, otherwise ``False``.
|
|
147
|
+
"""
|
|
91
148
|
try:
|
|
92
149
|
current_semver = semver.VersionInfo.parse(current_version)
|
|
93
150
|
latest_semver = semver.VersionInfo.parse(latest_version)
|
|
@@ -96,15 +153,34 @@ def _is_new_version_available(current_version: str, latest_version: str) -> bool
|
|
|
96
153
|
return latest_version != current_version
|
|
97
154
|
|
|
98
155
|
|
|
99
|
-
def auto_update(package_name: str = "pdd-cli", latest_version: str = None) -> None:
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
def auto_update(package_name: str = "pdd-cli", latest_version: Optional[str] = None) -> None:
|
|
157
|
+
"""Check PyPI for a newer version of the package and offer to upgrade.
|
|
158
|
+
|
|
159
|
+
This function:
|
|
160
|
+
|
|
161
|
+
* Determines the currently installed version using :mod:`importlib.metadata`.
|
|
162
|
+
* Optionally accepts a pre-fetched ``latest_version`` for testing; if not
|
|
163
|
+
supplied it will query PyPI.
|
|
164
|
+
* Compares versions using semantic versioning with a string comparison
|
|
165
|
+
fallback.
|
|
166
|
+
* Interactively prompts the user for confirmation before upgrading.
|
|
167
|
+
* Performs the upgrade using UV or pip depending on installation method,
|
|
168
|
+
with a pip fallback if a UV upgrade fails.
|
|
169
|
+
|
|
104
170
|
Args:
|
|
105
|
-
|
|
106
|
-
|
|
171
|
+
package_name: Name of the package to check (default: ``"pdd-cli"``).
|
|
172
|
+
latest_version: Optionally, a known latest version to use instead of
|
|
173
|
+
querying PyPI (primarily for testing).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
None. All feedback is provided via ``print`` statements.
|
|
107
177
|
"""
|
|
178
|
+
# Skip update check in CI mode, headless mode, or when stdin is not a TTY
|
|
179
|
+
if (os.environ.get('CI') == '1' or
|
|
180
|
+
os.environ.get('PDD_SKIP_UPDATE_CHECK') == '1' or
|
|
181
|
+
not sys.stdin.isatty()):
|
|
182
|
+
return
|
|
183
|
+
|
|
108
184
|
# pylint: disable=broad-except
|
|
109
185
|
try:
|
|
110
186
|
current_version = importlib.metadata.version(package_name)
|
|
@@ -117,32 +193,34 @@ def auto_update(package_name: str = "pdd-cli", latest_version: str = None) -> No
|
|
|
117
193
|
if not _is_new_version_available(current_version, latest_version):
|
|
118
194
|
return
|
|
119
195
|
|
|
120
|
-
print(
|
|
121
|
-
|
|
122
|
-
|
|
196
|
+
print(
|
|
197
|
+
f"\nNew version of {package_name} available: "
|
|
198
|
+
f"{latest_version} (current: {current_version})",
|
|
199
|
+
)
|
|
200
|
+
|
|
123
201
|
while True:
|
|
124
202
|
response = input("Would you like to upgrade? [y/N]: ").lower().strip()
|
|
125
|
-
if response in [
|
|
203
|
+
if response in ["y", "yes"]:
|
|
126
204
|
installation_method = detect_installation_method(sys.executable)
|
|
127
205
|
if _upgrade_package(package_name, installation_method):
|
|
128
206
|
break
|
|
129
|
-
|
|
207
|
+
|
|
130
208
|
if installation_method == "uv":
|
|
131
209
|
print("\nAttempting fallback to pip...")
|
|
132
210
|
if _upgrade_package(package_name, "pip"):
|
|
133
211
|
break
|
|
134
|
-
|
|
212
|
+
|
|
135
213
|
break
|
|
136
|
-
if response in [
|
|
214
|
+
if response in ["n", "no", ""]:
|
|
137
215
|
print("\nUpgrade cancelled")
|
|
138
216
|
break
|
|
139
217
|
print("Please answer 'y' or 'n'")
|
|
140
218
|
|
|
141
219
|
except importlib.metadata.PackageNotFoundError:
|
|
142
220
|
print(f"Package {package_name} is not installed")
|
|
143
|
-
except Exception as ex:
|
|
221
|
+
except Exception as ex: # noqa: BLE001
|
|
144
222
|
print(f"Error checking for updates: {str(ex)}")
|
|
145
223
|
|
|
146
224
|
|
|
147
225
|
if __name__ == "__main__":
|
|
148
|
-
auto_update()
|
|
226
|
+
auto_update()
|
pdd/bug_main.py
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
from typing import Tuple, Optional
|
|
5
|
+
|
|
4
6
|
import click
|
|
5
|
-
|
|
7
|
+
import requests
|
|
6
8
|
from pathlib import Path
|
|
9
|
+
from rich import print as rprint
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
7
12
|
|
|
8
13
|
from . import DEFAULT_STRENGTH, DEFAULT_TIME
|
|
9
14
|
from .construct_paths import construct_paths
|
|
10
15
|
from .bug_to_unit_test import bug_to_unit_test
|
|
16
|
+
from .core.cloud import CloudConfig
|
|
17
|
+
|
|
18
|
+
# Cloud request timeout
|
|
19
|
+
CLOUD_REQUEST_TIMEOUT = 400 # seconds
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _env_flag_enabled(name: str) -> bool:
|
|
25
|
+
"""Return True when an env var is set to a truthy value."""
|
|
26
|
+
value = os.environ.get(name)
|
|
27
|
+
if value is None:
|
|
28
|
+
return False
|
|
29
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
30
|
+
|
|
11
31
|
|
|
12
32
|
def bug_main(
|
|
13
33
|
ctx: click.Context,
|
|
@@ -32,6 +52,14 @@ def bug_main(
|
|
|
32
52
|
:param language: Optional programming language for the unit test. Defaults to "Python".
|
|
33
53
|
:return: A tuple containing the generated unit test, total cost, and model name used.
|
|
34
54
|
"""
|
|
55
|
+
# Initialize variables
|
|
56
|
+
unit_test = ""
|
|
57
|
+
total_cost = 0.0
|
|
58
|
+
model_name = ""
|
|
59
|
+
|
|
60
|
+
verbose = ctx.obj.get('verbose', False)
|
|
61
|
+
quiet = ctx.obj.get('quiet', False)
|
|
62
|
+
|
|
35
63
|
try:
|
|
36
64
|
# Construct file paths
|
|
37
65
|
input_file_paths = {
|
|
@@ -48,12 +76,13 @@ def bug_main(
|
|
|
48
76
|
resolved_config, input_strings, output_file_paths, detected_language = construct_paths(
|
|
49
77
|
input_file_paths=input_file_paths,
|
|
50
78
|
force=ctx.obj.get('force', False),
|
|
51
|
-
quiet=
|
|
79
|
+
quiet=quiet,
|
|
52
80
|
command="bug",
|
|
53
81
|
command_options=command_options,
|
|
54
|
-
context_override=ctx.obj.get('context')
|
|
82
|
+
context_override=ctx.obj.get('context'),
|
|
83
|
+
confirm_callback=ctx.obj.get('confirm_callback')
|
|
55
84
|
)
|
|
56
|
-
|
|
85
|
+
|
|
57
86
|
# Use the language detected by construct_paths if none was explicitly provided
|
|
58
87
|
if language is None:
|
|
59
88
|
language = detected_language
|
|
@@ -65,21 +94,158 @@ def bug_main(
|
|
|
65
94
|
current_output_content = input_strings["current_output"]
|
|
66
95
|
desired_output_content = input_strings["desired_output"]
|
|
67
96
|
|
|
68
|
-
#
|
|
97
|
+
# Get generation parameters
|
|
69
98
|
strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
|
|
70
99
|
temperature = ctx.obj.get('temperature', 0)
|
|
71
100
|
time_budget = ctx.obj.get('time', DEFAULT_TIME)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
|
|
102
|
+
# Determine cloud vs local execution preference
|
|
103
|
+
is_local_execution_preferred = ctx.obj.get('local', False)
|
|
104
|
+
cloud_only = _env_flag_enabled("PDD_CLOUD_ONLY") or _env_flag_enabled("PDD_NO_LOCAL_FALLBACK")
|
|
105
|
+
current_execution_is_local = is_local_execution_preferred and not cloud_only
|
|
106
|
+
|
|
107
|
+
# Try cloud execution first if not preferring local
|
|
108
|
+
if not current_execution_is_local:
|
|
109
|
+
if verbose:
|
|
110
|
+
console.print(Panel("Attempting cloud bug test generation...", title="[blue]Mode[/blue]", expand=False))
|
|
111
|
+
|
|
112
|
+
jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
|
|
113
|
+
|
|
114
|
+
if not jwt_token:
|
|
115
|
+
if cloud_only:
|
|
116
|
+
console.print("[red]Cloud authentication failed.[/red]")
|
|
117
|
+
raise click.UsageError("Cloud authentication failed")
|
|
118
|
+
console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
|
|
119
|
+
current_execution_is_local = True
|
|
120
|
+
|
|
121
|
+
if jwt_token and not current_execution_is_local:
|
|
122
|
+
# Build cloud payload
|
|
123
|
+
payload = {
|
|
124
|
+
"promptContent": prompt_content,
|
|
125
|
+
"codeContent": code_content,
|
|
126
|
+
"programContent": program_content,
|
|
127
|
+
"currentOutput": current_output_content,
|
|
128
|
+
"desiredOutput": desired_output_content,
|
|
129
|
+
"language": language,
|
|
130
|
+
"strength": strength,
|
|
131
|
+
"temperature": temperature,
|
|
132
|
+
"time": time_budget,
|
|
133
|
+
"verbose": verbose,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
headers = {
|
|
137
|
+
"Authorization": f"Bearer {jwt_token}",
|
|
138
|
+
"Content-Type": "application/json"
|
|
139
|
+
}
|
|
140
|
+
cloud_url = CloudConfig.get_endpoint_url("generateBugTest")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
response = requests.post(
|
|
144
|
+
cloud_url,
|
|
145
|
+
json=payload,
|
|
146
|
+
headers=headers,
|
|
147
|
+
timeout=CLOUD_REQUEST_TIMEOUT
|
|
148
|
+
)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
|
|
151
|
+
response_data = response.json()
|
|
152
|
+
unit_test = response_data.get("generatedTest", "")
|
|
153
|
+
total_cost = float(response_data.get("totalCost", 0.0))
|
|
154
|
+
model_name = response_data.get("modelName", "cloud_model")
|
|
155
|
+
|
|
156
|
+
if not unit_test:
|
|
157
|
+
if cloud_only:
|
|
158
|
+
console.print("[red]Cloud execution returned no test code.[/red]")
|
|
159
|
+
raise click.UsageError("Cloud execution returned no test code")
|
|
160
|
+
console.print("[yellow]Cloud execution returned no test code. Falling back to local.[/yellow]")
|
|
161
|
+
current_execution_is_local = True
|
|
162
|
+
elif verbose:
|
|
163
|
+
console.print(Panel(
|
|
164
|
+
f"Cloud bug test generation successful. Model: {model_name}, Cost: ${total_cost:.6f}",
|
|
165
|
+
title="[green]Cloud Success[/green]",
|
|
166
|
+
expand=False
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
except requests.exceptions.Timeout:
|
|
170
|
+
if cloud_only:
|
|
171
|
+
console.print(f"[red]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s).[/red]")
|
|
172
|
+
raise click.UsageError("Cloud execution timed out")
|
|
173
|
+
console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
|
|
174
|
+
current_execution_is_local = True
|
|
175
|
+
|
|
176
|
+
except requests.exceptions.HTTPError as e:
|
|
177
|
+
status_code = e.response.status_code if e.response else 0
|
|
178
|
+
err_content = e.response.text[:200] if e.response else "No response content"
|
|
179
|
+
|
|
180
|
+
# Non-recoverable errors: do NOT fall back to local
|
|
181
|
+
if status_code == 402: # Insufficient credits
|
|
182
|
+
try:
|
|
183
|
+
error_data = e.response.json()
|
|
184
|
+
current_balance = error_data.get("currentBalance", "unknown")
|
|
185
|
+
estimated_cost = error_data.get("estimatedCost", "unknown")
|
|
186
|
+
console.print(f"[red]Insufficient credits. Current balance: {current_balance}, estimated cost: {estimated_cost}[/red]")
|
|
187
|
+
except Exception:
|
|
188
|
+
console.print(f"[red]Insufficient credits: {err_content}[/red]")
|
|
189
|
+
raise click.UsageError("Insufficient credits for cloud bug test generation")
|
|
190
|
+
elif status_code == 401: # Authentication error
|
|
191
|
+
console.print(f"[red]Authentication failed: {err_content}[/red]")
|
|
192
|
+
raise click.UsageError("Cloud authentication failed")
|
|
193
|
+
elif status_code == 403: # Authorization error (not approved)
|
|
194
|
+
console.print(f"[red]Access denied: {err_content}[/red]")
|
|
195
|
+
raise click.UsageError("Access denied - user not approved")
|
|
196
|
+
elif status_code == 400: # Validation error
|
|
197
|
+
console.print(f"[red]Invalid request: {err_content}[/red]")
|
|
198
|
+
raise click.UsageError(f"Invalid request: {err_content}")
|
|
199
|
+
else:
|
|
200
|
+
# Recoverable errors (5xx, unexpected errors): fall back to local
|
|
201
|
+
if cloud_only:
|
|
202
|
+
console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
|
|
203
|
+
raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
|
|
204
|
+
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
|
|
205
|
+
current_execution_is_local = True
|
|
206
|
+
|
|
207
|
+
except requests.exceptions.RequestException as e:
|
|
208
|
+
if cloud_only:
|
|
209
|
+
console.print(f"[red]Cloud network error: {e}[/red]")
|
|
210
|
+
raise click.UsageError(f"Cloud network error: {e}")
|
|
211
|
+
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
|
|
212
|
+
current_execution_is_local = True
|
|
213
|
+
|
|
214
|
+
except json.JSONDecodeError:
|
|
215
|
+
if cloud_only:
|
|
216
|
+
console.print("[red]Cloud returned invalid JSON.[/red]")
|
|
217
|
+
raise click.UsageError("Cloud returned invalid JSON")
|
|
218
|
+
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
|
|
219
|
+
current_execution_is_local = True
|
|
220
|
+
|
|
221
|
+
# Local execution path
|
|
222
|
+
if current_execution_is_local:
|
|
223
|
+
if verbose:
|
|
224
|
+
console.print(Panel("Performing local bug test generation...", title="[blue]Mode[/blue]", expand=False))
|
|
225
|
+
|
|
226
|
+
unit_test, total_cost, model_name = bug_to_unit_test(
|
|
227
|
+
current_output_content,
|
|
228
|
+
desired_output_content,
|
|
229
|
+
prompt_content,
|
|
230
|
+
code_content,
|
|
231
|
+
program_content,
|
|
232
|
+
strength,
|
|
233
|
+
temperature,
|
|
234
|
+
time_budget,
|
|
235
|
+
language
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if verbose:
|
|
239
|
+
console.print(Panel(
|
|
240
|
+
f"Local bug test generation successful. Model: {model_name}, Cost: ${total_cost:.6f}",
|
|
241
|
+
title="[green]Local Success[/green]",
|
|
242
|
+
expand=False
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
# Validate generated content
|
|
246
|
+
if not unit_test or not unit_test.strip():
|
|
247
|
+
rprint("[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
|
|
248
|
+
return "", 0.0, "Error: Generated unit test content is empty"
|
|
83
249
|
|
|
84
250
|
# Save results if output path is provided
|
|
85
251
|
if output_file_paths.get("output"):
|
|
@@ -88,21 +254,21 @@ def bug_main(
|
|
|
88
254
|
if not output_path or output_path.strip() == '':
|
|
89
255
|
# Use a default output path in the current directory
|
|
90
256
|
output_path = f"test_{Path(code_file).stem}_bug.{language.lower()}"
|
|
91
|
-
if not
|
|
257
|
+
if not quiet:
|
|
92
258
|
rprint(f"[yellow]Warning: Empty output path detected. Using default: {output_path}[/yellow]")
|
|
93
259
|
output_file_paths["output"] = output_path
|
|
94
|
-
|
|
260
|
+
|
|
95
261
|
# Create directory if it doesn't exist
|
|
96
262
|
dir_path = os.path.dirname(output_path)
|
|
97
263
|
if dir_path: # Only create directory if there's a directory part in the path
|
|
98
264
|
os.makedirs(dir_path, exist_ok=True)
|
|
99
|
-
|
|
265
|
+
|
|
100
266
|
# Write the file
|
|
101
|
-
with open(output_path, 'w') as f:
|
|
267
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
102
268
|
f.write(unit_test)
|
|
103
269
|
|
|
104
270
|
# Provide user feedback
|
|
105
|
-
if not
|
|
271
|
+
if not quiet:
|
|
106
272
|
rprint("[bold green]Unit test generated successfully.[/bold green]")
|
|
107
273
|
rprint(f"[bold]Model used:[/bold] {model_name}")
|
|
108
274
|
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
@@ -115,7 +281,13 @@ def bug_main(
|
|
|
115
281
|
|
|
116
282
|
return unit_test, total_cost, model_name
|
|
117
283
|
|
|
284
|
+
except click.Abort:
|
|
285
|
+
# User cancelled - re-raise to stop the sync loop
|
|
286
|
+
raise
|
|
287
|
+
except click.UsageError:
|
|
288
|
+
# Re-raise usage errors to be handled by CLI
|
|
289
|
+
raise
|
|
118
290
|
except Exception as e:
|
|
119
|
-
if not
|
|
291
|
+
if not quiet:
|
|
120
292
|
rprint(f"[bold red]Error:[/bold red] {str(e)}")
|
|
121
|
-
|
|
293
|
+
return "", 0.0, f"Error: {e}"
|