pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__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 +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +80 -19
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +281 -81
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -62
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +331 -77
- pdd/fix_error_loop.py +209 -60
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +319 -272
- pdd/fix_verification_main.py +57 -17
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +48 -9
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/increase_tests.py +7 -0
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1278 -110
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +98 -101
- pdd/prompts/change_LLM.prompt +1 -3
- pdd/prompts/detect_change_LLM.prompt +562 -3
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -2
- pdd/prompts/insert_includes_LLM.prompt +1181 -6
- pdd/prompts/split_LLM.prompt +1 -62
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/prompts/xml_convertor_LLM.prompt +3246 -7
- pdd/pytest_output.py +188 -21
- pdd/python_env_detector.py +151 -0
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +56 -7
- pdd/sync_determine_operation.py +918 -186
- pdd/sync_main.py +82 -32
- pdd/sync_orchestration.py +1456 -453
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.42.dist-info/RECORD +0 -115
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/fix_verification_main.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import click
|
|
5
5
|
import logging
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Optional, Tuple, List, Dict, Any
|
|
7
8
|
|
|
8
9
|
# Use Rich for pretty printing to the console
|
|
@@ -17,6 +18,7 @@ from .fix_verification_errors import fix_verification_errors
|
|
|
17
18
|
from .fix_verification_errors_loop import fix_verification_errors_loop
|
|
18
19
|
# Import DEFAULT_STRENGTH from the main package
|
|
19
20
|
from . import DEFAULT_STRENGTH, DEFAULT_TIME
|
|
21
|
+
from .python_env_detector import detect_host_python_executable
|
|
20
22
|
|
|
21
23
|
# Default values from the README
|
|
22
24
|
DEFAULT_TEMPERATURE = 0.0
|
|
@@ -48,7 +50,7 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
48
50
|
# A more robust solution might use the 'language' from construct_paths
|
|
49
51
|
interpreter = []
|
|
50
52
|
if program_path.endswith(".py"):
|
|
51
|
-
interpreter = [
|
|
53
|
+
interpreter = [detect_host_python_executable()] # Use environment-aware Python executable
|
|
52
54
|
elif program_path.endswith(".js"):
|
|
53
55
|
interpreter = ["node"]
|
|
54
56
|
elif program_path.endswith(".sh"):
|
|
@@ -57,13 +59,21 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
57
59
|
|
|
58
60
|
command = interpreter + [program_path] + args
|
|
59
61
|
rich_print(f"[dim]Running command:[/dim] {' '.join(command)}")
|
|
62
|
+
rich_print(f"[dim]Working directory:[/dim] {os.path.dirname(program_path) if program_path else 'None'}")
|
|
63
|
+
rich_print(f"[dim]Environment PYTHONPATH:[/dim] {os.environ.get('PYTHONPATH', 'Not set')}")
|
|
60
64
|
|
|
65
|
+
# Create a copy of environment with PYTHONUNBUFFERED set
|
|
66
|
+
env = os.environ.copy()
|
|
67
|
+
env['PYTHONUNBUFFERED'] = '1' # Force unbuffered output
|
|
68
|
+
|
|
61
69
|
process = subprocess.run(
|
|
62
70
|
command,
|
|
63
71
|
capture_output=True,
|
|
64
72
|
text=True,
|
|
65
73
|
check=False, # Don't raise exception on non-zero exit code
|
|
66
|
-
timeout=60 # Add a timeout to prevent hangs
|
|
74
|
+
timeout=60, # Add a timeout to prevent hangs
|
|
75
|
+
env=env, # Pass modified environment variables
|
|
76
|
+
cwd=os.path.dirname(program_path) if program_path else None # Set working directory
|
|
67
77
|
)
|
|
68
78
|
|
|
69
79
|
success = process.returncode == 0
|
|
@@ -71,11 +81,17 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
71
81
|
stderr = process.stderr
|
|
72
82
|
|
|
73
83
|
if not success:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
rich_print(f"[yellow]Warning:[/yellow] Program '{os.path.basename(program_path)}' exited with code {process.returncode}.")
|
|
85
|
+
|
|
86
|
+
# Check for syntax errors specifically
|
|
87
|
+
if "SyntaxError" in stderr:
|
|
88
|
+
rich_print("[bold red]Syntax Error Detected:[/bold red]")
|
|
89
|
+
rich_print(Panel(stderr, border_style="red", title="Python Syntax Error"))
|
|
90
|
+
# Return with special indicator for syntax errors
|
|
91
|
+
return False, stdout, f"SYNTAX_ERROR: {stderr}"
|
|
92
|
+
elif stderr:
|
|
93
|
+
rich_print("[yellow]Stderr:[/yellow]")
|
|
94
|
+
rich_print(Panel(stderr, border_style="yellow"))
|
|
79
95
|
|
|
80
96
|
return success, stdout, stderr
|
|
81
97
|
|
|
@@ -101,6 +117,9 @@ def fix_verification_main(
|
|
|
101
117
|
verification_program: Optional[str], # Only used if loop=True
|
|
102
118
|
max_attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
103
119
|
budget: float = DEFAULT_BUDGET,
|
|
120
|
+
agentic_fallback: bool = True,
|
|
121
|
+
strength: Optional[float] = None,
|
|
122
|
+
temperature: Optional[float] = None,
|
|
104
123
|
) -> Tuple[bool, str, str, int, float, str]:
|
|
105
124
|
"""
|
|
106
125
|
CLI wrapper for the 'verify' command. Verifies code correctness by running
|
|
@@ -129,9 +148,9 @@ def fix_verification_main(
|
|
|
129
148
|
- total_cost (float): Total cost incurred.
|
|
130
149
|
- model_name (str): Name of the LLM used.
|
|
131
150
|
"""
|
|
132
|
-
# Extract global options from context
|
|
133
|
-
strength: float = ctx.obj.get('strength', DEFAULT_STRENGTH)
|
|
134
|
-
temperature: float = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
|
|
151
|
+
# Extract global options from context (prefer passed parameters over ctx.obj)
|
|
152
|
+
strength: float = strength if strength is not None else ctx.obj.get('strength', DEFAULT_STRENGTH)
|
|
153
|
+
temperature: float = temperature if temperature is not None else ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
|
|
135
154
|
force: bool = ctx.obj.get('force', False)
|
|
136
155
|
quiet: bool = ctx.obj.get('quiet', False)
|
|
137
156
|
verbose: bool = ctx.obj.get('verbose', False)
|
|
@@ -189,6 +208,8 @@ def fix_verification_main(
|
|
|
189
208
|
quiet=quiet,
|
|
190
209
|
command="verify",
|
|
191
210
|
command_options=command_options,
|
|
211
|
+
context_override=ctx.obj.get('context'),
|
|
212
|
+
confirm_callback=ctx.obj.get('confirm_callback')
|
|
192
213
|
)
|
|
193
214
|
output_code_path = output_file_paths.get("output_code")
|
|
194
215
|
output_results_path = output_file_paths.get("output_results")
|
|
@@ -197,6 +218,9 @@ def fix_verification_main(
|
|
|
197
218
|
if verbose:
|
|
198
219
|
rich_print("[dim]Resolved output paths via construct_paths.[/dim]")
|
|
199
220
|
|
|
221
|
+
except click.Abort:
|
|
222
|
+
# User cancelled - re-raise to stop the sync loop
|
|
223
|
+
raise
|
|
200
224
|
except Exception as e:
|
|
201
225
|
# If the helper does not understand the "verify" command fall back.
|
|
202
226
|
if "invalid command" in str(e).lower():
|
|
@@ -215,7 +239,8 @@ def fix_verification_main(
|
|
|
215
239
|
input_strings["program_file"] = f.read()
|
|
216
240
|
except FileNotFoundError as fe:
|
|
217
241
|
rich_print(f"[bold red]Error:[/bold red] {fe}")
|
|
218
|
-
sys.exit(1)
|
|
242
|
+
# Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
|
|
243
|
+
return False, "", "", 0, 0.0, f"FileNotFoundError: {fe}"
|
|
219
244
|
|
|
220
245
|
# Pick or build output paths
|
|
221
246
|
if output_code_path is None:
|
|
@@ -237,7 +262,8 @@ def fix_verification_main(
|
|
|
237
262
|
if verbose:
|
|
238
263
|
import traceback
|
|
239
264
|
rich_print(Panel(traceback.format_exc(), title="Traceback", border_style="red"))
|
|
240
|
-
sys.exit(1)
|
|
265
|
+
# Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
|
|
266
|
+
return False, "", "", 0, 0.0, f"Error: {e}"
|
|
241
267
|
|
|
242
268
|
# --- Core Logic ---
|
|
243
269
|
success: bool = False
|
|
@@ -258,6 +284,7 @@ def fix_verification_main(
|
|
|
258
284
|
program_file=program_file, # Changed to pass the program_file path
|
|
259
285
|
code_file=code_file, # Changed to pass the code_file path
|
|
260
286
|
prompt=input_strings["prompt_file"], # Correctly passing prompt content
|
|
287
|
+
prompt_file=prompt_file,
|
|
261
288
|
verification_program=verification_program, # Path to the verifier program
|
|
262
289
|
strength=strength,
|
|
263
290
|
temperature=temperature,
|
|
@@ -268,7 +295,8 @@ def fix_verification_main(
|
|
|
268
295
|
# output_code_path should not be passed here
|
|
269
296
|
# output_program_path should not be passed here
|
|
270
297
|
verbose=verbose,
|
|
271
|
-
program_args=[] # Pass an empty list for program_args
|
|
298
|
+
program_args=[], # Pass an empty list for program_args
|
|
299
|
+
agentic_fallback=agentic_fallback,
|
|
272
300
|
)
|
|
273
301
|
success = loop_results.get('success', False)
|
|
274
302
|
final_program = loop_results.get('final_program', "") # Use .get for safety
|
|
@@ -354,7 +382,13 @@ def fix_verification_main(
|
|
|
354
382
|
results_log_content += f"Model Used: {model_name}\n"
|
|
355
383
|
results_log_content += f"Total Cost: ${total_cost:.6f}\n"
|
|
356
384
|
results_log_content += "\n--- LLM Explanation ---\n"
|
|
357
|
-
|
|
385
|
+
# The original code here was:
|
|
386
|
+
# results_log_content += "\n".join(fix_results.get('explanation', ['N/A']))
|
|
387
|
+
# This was incorrect because fix_results['explanation'] is a single string.
|
|
388
|
+
# The list constructor would then iterate through it character-by-character,
|
|
389
|
+
# causing the single-character-per-line output.
|
|
390
|
+
# The fix is to just append the string directly, using a default value if it is None.
|
|
391
|
+
results_log_content += fix_results.get('explanation') or 'N/A'
|
|
358
392
|
results_log_content += "\n\n--- Program Output Used for Verification ---\n"
|
|
359
393
|
results_log_content += program_output
|
|
360
394
|
|
|
@@ -386,7 +420,9 @@ def fix_verification_main(
|
|
|
386
420
|
try:
|
|
387
421
|
if verbose:
|
|
388
422
|
rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write code to: {output_code_path!r}")
|
|
389
|
-
|
|
423
|
+
output_code_path_obj = Path(output_code_path)
|
|
424
|
+
output_code_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
with open(output_code_path_obj, "w") as f:
|
|
390
426
|
f.write(final_code)
|
|
391
427
|
saved_code_path = output_code_path
|
|
392
428
|
if not quiet:
|
|
@@ -406,7 +442,9 @@ def fix_verification_main(
|
|
|
406
442
|
try:
|
|
407
443
|
if verbose:
|
|
408
444
|
rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write program to: {output_program_path!r}")
|
|
409
|
-
|
|
445
|
+
output_program_path_obj = Path(output_program_path)
|
|
446
|
+
output_program_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
447
|
+
with open(output_program_path_obj, "w") as f:
|
|
410
448
|
f.write(final_program)
|
|
411
449
|
saved_program_path = output_program_path
|
|
412
450
|
if not quiet:
|
|
@@ -416,7 +454,9 @@ def fix_verification_main(
|
|
|
416
454
|
|
|
417
455
|
if not loop and output_results_path:
|
|
418
456
|
try:
|
|
419
|
-
|
|
457
|
+
output_results_path_obj = Path(output_results_path)
|
|
458
|
+
output_results_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
with open(output_results_path_obj, "w") as f:
|
|
420
460
|
f.write(results_log_content)
|
|
421
461
|
saved_results_path = output_results_path
|
|
422
462
|
if not quiet:
|
pdd/generate_output_paths.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Dict, List, Optional
|
|
3
|
+
from typing import Dict, List, Literal, Optional
|
|
4
|
+
|
|
5
|
+
# Type alias for path resolution mode
|
|
6
|
+
PathResolutionMode = Literal["config_base", "cwd"]
|
|
4
7
|
|
|
5
8
|
# Configure logging
|
|
6
|
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
7
9
|
logger = logging.getLogger(__name__)
|
|
8
10
|
|
|
9
11
|
# --- Configuration Data ---
|
|
10
12
|
|
|
13
|
+
# Default directory names
|
|
14
|
+
EXAMPLES_DIR = "examples"
|
|
15
|
+
|
|
11
16
|
# Define the expected output keys for each command
|
|
12
17
|
# Use underscores for keys as requested
|
|
13
18
|
COMMAND_OUTPUT_KEYS: Dict[str, List[str]] = {
|
|
@@ -178,14 +183,19 @@ def generate_output_paths(
|
|
|
178
183
|
basename: str,
|
|
179
184
|
language: str,
|
|
180
185
|
file_extension: str,
|
|
181
|
-
context_config: Optional[Dict[str, str]] = None
|
|
186
|
+
context_config: Optional[Dict[str, str]] = None,
|
|
187
|
+
input_file_dir: Optional[str] = None,
|
|
188
|
+
input_file_dirs: Optional[Dict[str, str]] = None,
|
|
189
|
+
config_base_dir: Optional[str] = None,
|
|
190
|
+
path_resolution_mode: PathResolutionMode = "config_base",
|
|
182
191
|
) -> Dict[str, str]:
|
|
183
192
|
"""
|
|
184
193
|
Generates the full, absolute output paths for a given PDD command.
|
|
185
194
|
|
|
186
|
-
It prioritizes user-specified paths (--output options), then context
|
|
187
|
-
configuration from .pddrc, then environment variables, and finally
|
|
188
|
-
falls back to default naming conventions in the
|
|
195
|
+
It prioritizes user-specified paths (--output options), then context
|
|
196
|
+
configuration from .pddrc, then environment variables, and finally
|
|
197
|
+
falls back to default naming conventions in the input file's directory
|
|
198
|
+
(or current working directory if input_file_dir is not provided).
|
|
189
199
|
|
|
190
200
|
Args:
|
|
191
201
|
command: The PDD command being executed (e.g., 'generate', 'fix').
|
|
@@ -199,6 +209,23 @@ def generate_output_paths(
|
|
|
199
209
|
used when default patterns require it.
|
|
200
210
|
context_config: Optional dictionary with context-specific paths from .pddrc
|
|
201
211
|
configuration (e.g., {'generate_output_path': 'src/'}).
|
|
212
|
+
input_file_dir: Optional path to the input file's directory. When provided,
|
|
213
|
+
default output files will be placed in this directory instead
|
|
214
|
+
of the current working directory.
|
|
215
|
+
input_file_dirs: Optional dictionary mapping output keys to specific input
|
|
216
|
+
file directories. When provided, each output will use its
|
|
217
|
+
corresponding input file directory (e.g., {'output_code': 'src/main/java'}).
|
|
218
|
+
config_base_dir: Optional base directory to resolve relative `.pddrc` and
|
|
219
|
+
environment variable output paths. When set, relative
|
|
220
|
+
config paths resolve under this directory (typically the
|
|
221
|
+
directory containing `.pddrc`) instead of the input file
|
|
222
|
+
directory.
|
|
223
|
+
path_resolution_mode: Controls how relative paths from `.pddrc` and
|
|
224
|
+
environment variables are resolved. "config_base"
|
|
225
|
+
(default) resolves relative to config_base_dir,
|
|
226
|
+
"cwd" resolves relative to the current working
|
|
227
|
+
directory. Use "cwd" for sync command to ensure
|
|
228
|
+
output files are created where the user is.
|
|
202
229
|
|
|
203
230
|
Returns:
|
|
204
231
|
A dictionary where keys are the standardized output identifiers
|
|
@@ -209,9 +236,14 @@ def generate_output_paths(
|
|
|
209
236
|
logger.debug(f"Generating output paths for command: {command}")
|
|
210
237
|
logger.debug(f"User output locations: {output_locations}")
|
|
211
238
|
logger.debug(f"Context config: {context_config}")
|
|
239
|
+
logger.debug(f"Input file dirs: {input_file_dirs}")
|
|
240
|
+
logger.debug(f"Config base dir: {config_base_dir}")
|
|
241
|
+
logger.debug(f"Path resolution mode: {path_resolution_mode}")
|
|
212
242
|
logger.debug(f"Basename: {basename}, Language: {language}, Extension: {file_extension}")
|
|
213
243
|
|
|
214
244
|
context_config = context_config or {}
|
|
245
|
+
input_file_dirs = input_file_dirs or {}
|
|
246
|
+
config_base_dir_abs = os.path.abspath(config_base_dir) if config_base_dir else None
|
|
215
247
|
result_paths: Dict[str, str] = {}
|
|
216
248
|
|
|
217
249
|
if not basename:
|
|
@@ -278,6 +310,20 @@ def generate_output_paths(
|
|
|
278
310
|
# 2. Check Context Configuration Path (.pddrc)
|
|
279
311
|
elif context_path:
|
|
280
312
|
source = "context"
|
|
313
|
+
|
|
314
|
+
# Resolve relative `.pddrc` paths based on path_resolution_mode.
|
|
315
|
+
# "cwd" mode: resolve relative to current working directory (for sync)
|
|
316
|
+
# "config_base" mode: resolve relative to config_base_dir (for fix, etc.)
|
|
317
|
+
# Fall back to the input file directory for backwards compatibility.
|
|
318
|
+
if not os.path.isabs(context_path):
|
|
319
|
+
if path_resolution_mode == "cwd":
|
|
320
|
+
context_path = os.path.join(os.getcwd(), context_path)
|
|
321
|
+
elif config_base_dir_abs:
|
|
322
|
+
context_path = os.path.join(config_base_dir_abs, context_path)
|
|
323
|
+
elif input_file_dir:
|
|
324
|
+
context_path = os.path.join(input_file_dir, context_path)
|
|
325
|
+
logger.debug(f"Resolved relative context path to: {context_path}")
|
|
326
|
+
|
|
281
327
|
# Check if the context path is a directory
|
|
282
328
|
is_dir = context_path.endswith(os.path.sep) or context_path.endswith('/')
|
|
283
329
|
if not is_dir:
|
|
@@ -297,6 +343,18 @@ def generate_output_paths(
|
|
|
297
343
|
# 3. Check Environment Variable Path
|
|
298
344
|
elif env_path:
|
|
299
345
|
source = "environment"
|
|
346
|
+
|
|
347
|
+
# Resolve relative env paths based on path_resolution_mode.
|
|
348
|
+
# Same logic as .pddrc paths for consistency.
|
|
349
|
+
if not os.path.isabs(env_path):
|
|
350
|
+
if path_resolution_mode == "cwd":
|
|
351
|
+
env_path = os.path.join(os.getcwd(), env_path)
|
|
352
|
+
elif config_base_dir_abs:
|
|
353
|
+
env_path = os.path.join(config_base_dir_abs, env_path)
|
|
354
|
+
elif input_file_dir:
|
|
355
|
+
env_path = os.path.join(input_file_dir, env_path)
|
|
356
|
+
logger.debug(f"Resolved relative env path to: {env_path}")
|
|
357
|
+
|
|
300
358
|
# Check if the environment variable points to a directory
|
|
301
359
|
is_dir = env_path.endswith(os.path.sep)
|
|
302
360
|
if not is_dir:
|
|
@@ -313,11 +371,33 @@ def generate_output_paths(
|
|
|
313
371
|
logger.debug(f"Env path '{env_path}' identified as a specific file path.")
|
|
314
372
|
final_path = env_path # Assume it's a full path or filename
|
|
315
373
|
|
|
316
|
-
# 4. Use Default Naming Convention
|
|
374
|
+
# 4. Use Default Naming Convention
|
|
317
375
|
else:
|
|
318
376
|
source = "default"
|
|
319
|
-
|
|
320
|
-
|
|
377
|
+
# For example command, default to examples/ directory if no .pddrc config
|
|
378
|
+
if command == "example":
|
|
379
|
+
examples_dir = EXAMPLES_DIR # Fallback constant
|
|
380
|
+
# Create examples directory if it doesn't exist
|
|
381
|
+
try:
|
|
382
|
+
os.makedirs(examples_dir, exist_ok=True)
|
|
383
|
+
logger.debug(f"Created examples directory: {examples_dir}")
|
|
384
|
+
except OSError as e:
|
|
385
|
+
logger.warning(f"Could not create examples directory: {e}")
|
|
386
|
+
final_path = os.path.join(examples_dir, default_filename)
|
|
387
|
+
logger.debug(f"Using default filename '{default_filename}' in examples directory.")
|
|
388
|
+
else:
|
|
389
|
+
# First check if there's a specific directory for this output key
|
|
390
|
+
specific_dir = input_file_dirs.get(output_key)
|
|
391
|
+
if specific_dir:
|
|
392
|
+
final_path = os.path.join(specific_dir, default_filename)
|
|
393
|
+
logger.debug(f"Using default filename '{default_filename}' in specific input file directory: {specific_dir}")
|
|
394
|
+
# Otherwise use the general input file directory if provided
|
|
395
|
+
elif input_file_dir:
|
|
396
|
+
final_path = os.path.join(input_file_dir, default_filename)
|
|
397
|
+
logger.debug(f"Using default filename '{default_filename}' in input file directory: {input_file_dir}")
|
|
398
|
+
else:
|
|
399
|
+
final_path = default_filename # Relative to CWD initially
|
|
400
|
+
logger.debug(f"Using default filename '{default_filename}' in current directory.")
|
|
321
401
|
|
|
322
402
|
# Resolve to absolute path
|
|
323
403
|
if final_path:
|
|
@@ -340,6 +420,9 @@ def generate_output_paths(
|
|
|
340
420
|
|
|
341
421
|
# --- Example Usage (for testing) ---
|
|
342
422
|
if __name__ == '__main__':
|
|
423
|
+
# Configure logging for standalone execution
|
|
424
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
425
|
+
|
|
343
426
|
# Mock inputs
|
|
344
427
|
mock_basename = "my_module"
|
|
345
428
|
mock_language = "python"
|
|
@@ -568,4 +651,4 @@ if __name__ == '__main__':
|
|
|
568
651
|
# 'output_code': '/path/to/cwd/another_module_verify_verified.py',
|
|
569
652
|
# 'output_program': f'/path/to/cwd/{env_verify_prog_path}'
|
|
570
653
|
# }
|
|
571
|
-
del os.environ['PDD_VERIFY_PROGRAM_OUTPUT_PATH'] # Clean up
|
|
654
|
+
del os.environ['PDD_VERIFY_PROGRAM_OUTPUT_PATH'] # Clean up
|
pdd/generate_test.py
CHANGED
|
@@ -15,11 +15,14 @@ console = Console()
|
|
|
15
15
|
def generate_test(
|
|
16
16
|
prompt: str,
|
|
17
17
|
code: str,
|
|
18
|
-
strength: float=DEFAULT_STRENGTH,
|
|
19
|
-
temperature: float=0.0,
|
|
18
|
+
strength: float = DEFAULT_STRENGTH,
|
|
19
|
+
temperature: float = 0.0,
|
|
20
20
|
time: float = DEFAULT_TIME,
|
|
21
21
|
language: str = "python",
|
|
22
|
-
verbose: bool = False
|
|
22
|
+
verbose: bool = False,
|
|
23
|
+
source_file_path: Optional[str] = None,
|
|
24
|
+
test_file_path: Optional[str] = None,
|
|
25
|
+
module_name: Optional[str] = None
|
|
23
26
|
) -> Tuple[str, float, str]:
|
|
24
27
|
"""
|
|
25
28
|
Generate a unit test from a code file using LLM.
|
|
@@ -32,6 +35,9 @@ def generate_test(
|
|
|
32
35
|
language (str): The programming language for the unit test.
|
|
33
36
|
time (float, optional): Time budget for LLM calls. Defaults to DEFAULT_TIME.
|
|
34
37
|
verbose (bool): Whether to print detailed information.
|
|
38
|
+
source_file_path (Optional[str]): Absolute or relative path to the code under test.
|
|
39
|
+
test_file_path (Optional[str]): Destination path for the generated test file.
|
|
40
|
+
module_name (Optional[str]): Module name (without extension) for proper imports.
|
|
35
41
|
|
|
36
42
|
Returns:
|
|
37
43
|
Tuple[str, float, str]: (unit_test, total_cost, model_name)
|
|
@@ -53,7 +59,10 @@ def generate_test(
|
|
|
53
59
|
input_json = {
|
|
54
60
|
"prompt_that_generated_code": processed_prompt,
|
|
55
61
|
"code": code,
|
|
56
|
-
"language": language
|
|
62
|
+
"language": language,
|
|
63
|
+
"source_file_path": source_file_path or "",
|
|
64
|
+
"test_file_path": test_file_path or "",
|
|
65
|
+
"module_name": module_name or ""
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
if verbose:
|
|
@@ -98,6 +107,7 @@ def generate_test(
|
|
|
98
107
|
strength=strength,
|
|
99
108
|
temperature=temperature,
|
|
100
109
|
time=time,
|
|
110
|
+
language=language,
|
|
101
111
|
verbose=verbose
|
|
102
112
|
)
|
|
103
113
|
total_cost += check_cost
|
|
@@ -112,6 +122,7 @@ def generate_test(
|
|
|
112
122
|
strength=strength,
|
|
113
123
|
temperature=temperature,
|
|
114
124
|
time=time,
|
|
125
|
+
language=language,
|
|
115
126
|
verbose=verbose
|
|
116
127
|
)
|
|
117
128
|
total_cost += continue_cost
|
|
@@ -181,4 +192,4 @@ def _validate_inputs(
|
|
|
181
192
|
if not isinstance(temperature, float):
|
|
182
193
|
raise ValueError("Temperature must be a float")
|
|
183
194
|
if not language or not isinstance(language, str):
|
|
184
|
-
raise ValueError("Language must be a non-empty string")
|
|
195
|
+
raise ValueError("Language must be a non-empty string")
|
pdd/get_jwt_token.py
CHANGED
|
@@ -2,7 +2,20 @@ import asyncio
|
|
|
2
2
|
import time
|
|
3
3
|
from typing import Dict, Optional, Tuple
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
# Cross-platform keyring import with fallback for WSL compatibility
|
|
6
|
+
try:
|
|
7
|
+
import keyring
|
|
8
|
+
KEYRING_AVAILABLE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
try:
|
|
11
|
+
import keyrings.alt.file
|
|
12
|
+
keyring = keyrings.alt.file.PlaintextKeyring()
|
|
13
|
+
KEYRING_AVAILABLE = True
|
|
14
|
+
print("Warning: Using alternative keyring (PlaintextKeyring) - tokens stored in plaintext")
|
|
15
|
+
except ImportError:
|
|
16
|
+
keyring = None
|
|
17
|
+
KEYRING_AVAILABLE = False
|
|
18
|
+
print("Warning: No keyring available - token storage disabled")
|
|
6
19
|
import requests
|
|
7
20
|
|
|
8
21
|
# Custom exception classes for better error handling
|
|
@@ -128,20 +141,39 @@ class FirebaseAuthenticator:
|
|
|
128
141
|
|
|
129
142
|
def _store_refresh_token(self, refresh_token: str):
|
|
130
143
|
"""Stores the Firebase refresh token in the system keyring."""
|
|
131
|
-
keyring
|
|
144
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
145
|
+
print("Warning: No keyring available, refresh token not stored")
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
keyring.set_password(self.keyring_service_name, self.keyring_user_name, refresh_token)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"Warning: Failed to store refresh token in keyring: {e}")
|
|
132
151
|
|
|
133
152
|
def _get_stored_refresh_token(self) -> Optional[str]:
|
|
134
153
|
"""Retrieves the Firebase refresh token from the system keyring."""
|
|
135
|
-
|
|
154
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
155
|
+
return None
|
|
156
|
+
try:
|
|
157
|
+
return keyring.get_password(self.keyring_service_name, self.keyring_user_name)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"Warning: Failed to retrieve refresh token from keyring: {e}")
|
|
160
|
+
return None
|
|
136
161
|
|
|
137
162
|
def _delete_stored_refresh_token(self):
|
|
138
163
|
"""Deletes the stored Firebase refresh token from the keyring."""
|
|
164
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
165
|
+
print("No keyring available. Token deletion skipped.")
|
|
166
|
+
return
|
|
139
167
|
try:
|
|
140
168
|
keyring.delete_password(self.keyring_service_name, self.keyring_user_name)
|
|
141
|
-
except
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
except Exception as e:
|
|
170
|
+
# Handle both keyring.errors and generic exceptions for cross-platform compatibility
|
|
171
|
+
if "NoKeyringError" in str(type(e)) or "no keyring" in str(e).lower():
|
|
172
|
+
print("No keyring found. Token deletion skipped.")
|
|
173
|
+
elif "PasswordDeleteError" in str(type(e)) or "delete" in str(e).lower():
|
|
174
|
+
print("Failed to delete token from keyring.")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Warning: Error deleting token from keyring: {e}")
|
|
145
177
|
|
|
146
178
|
async def _refresh_firebase_token(self, refresh_token: str) -> str:
|
|
147
179
|
"""
|
|
@@ -216,7 +248,14 @@ class FirebaseAuthenticator:
|
|
|
216
248
|
except requests.exceptions.ConnectionError as e:
|
|
217
249
|
raise NetworkError(f"Failed to connect to Firebase: {e}")
|
|
218
250
|
except requests.exceptions.RequestException as e:
|
|
219
|
-
|
|
251
|
+
# Capture more detail to help diagnose provider configuration or audience mismatches
|
|
252
|
+
extra = ""
|
|
253
|
+
if getattr(e, "response", None) is not None:
|
|
254
|
+
try:
|
|
255
|
+
extra = f" | response: {e.response.text}"
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
raise TokenError(f"Error exchanging GitHub token for Firebase token: {e}{extra}")
|
|
220
259
|
|
|
221
260
|
def verify_firebase_token(self, id_token: str) -> bool:
|
|
222
261
|
"""
|
|
@@ -287,4 +326,4 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
|
|
|
287
326
|
# Store refresh token
|
|
288
327
|
firebase_auth._store_refresh_token(refresh_token)
|
|
289
328
|
|
|
290
|
-
return id_token
|
|
329
|
+
return id_token
|
pdd/get_run_command.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Module to retrieve run commands for programming languages."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import csv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_run_command(extension: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Retrieves the run command for a given file extension.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
extension: The file extension (e.g., ".py", ".js").
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The run command template with {file} placeholder (e.g., "python {file}"),
|
|
16
|
+
or an empty string if not found or not executable.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If the PDD_PATH environment variable is not set.
|
|
20
|
+
"""
|
|
21
|
+
# Step 1: Load environment variable PDD_PATH
|
|
22
|
+
pdd_path = os.environ.get('PDD_PATH')
|
|
23
|
+
if not pdd_path:
|
|
24
|
+
raise ValueError("PDD_PATH environment variable is not set")
|
|
25
|
+
|
|
26
|
+
# Step 2: Ensure the extension starts with a dot and convert to lowercase
|
|
27
|
+
if not extension.startswith('.'):
|
|
28
|
+
extension = '.' + extension
|
|
29
|
+
extension = extension.lower()
|
|
30
|
+
|
|
31
|
+
# Step 3: Look up the run command
|
|
32
|
+
csv_path = os.path.join(pdd_path, 'data', 'language_format.csv')
|
|
33
|
+
try:
|
|
34
|
+
with open(csv_path, 'r') as csvfile:
|
|
35
|
+
reader = csv.DictReader(csvfile)
|
|
36
|
+
for row in reader:
|
|
37
|
+
if row['extension'].lower() == extension:
|
|
38
|
+
run_command = row.get('run_command', '').strip()
|
|
39
|
+
return run_command if run_command else ''
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
print(f"CSV file not found at {csv_path}")
|
|
42
|
+
except csv.Error as e:
|
|
43
|
+
print(f"Error reading CSV file: {e}")
|
|
44
|
+
except KeyError:
|
|
45
|
+
# run_command column doesn't exist
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
return ''
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_run_command_for_file(file_path: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Retrieves the run command for a given file, with the {file} placeholder replaced.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_path: The path to the file to run.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The complete run command (e.g., "python /path/to/script.py"),
|
|
60
|
+
or an empty string if no run command is available for this file type.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If the PDD_PATH environment variable is not set.
|
|
64
|
+
"""
|
|
65
|
+
_, extension = os.path.splitext(file_path)
|
|
66
|
+
if not extension:
|
|
67
|
+
return ''
|
|
68
|
+
|
|
69
|
+
run_command_template = get_run_command(extension)
|
|
70
|
+
if not run_command_template:
|
|
71
|
+
return ''
|
|
72
|
+
|
|
73
|
+
return run_command_template.replace('{file}', file_path)
|
pdd/get_test_command.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# pdd/get_test_command.py
|
|
2
|
+
"""Get language-appropriate test commands.
|
|
3
|
+
|
|
4
|
+
This module provides functions to resolve the appropriate test command
|
|
5
|
+
for a given test file based on:
|
|
6
|
+
1. CSV run_test_command (if non-empty)
|
|
7
|
+
2. Smart detection via default_verify_cmd_for()
|
|
8
|
+
3. None (triggers agentic fallback)
|
|
9
|
+
"""
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import csv
|
|
13
|
+
|
|
14
|
+
from .agentic_langtest import default_verify_cmd_for
|
|
15
|
+
from .get_language import get_language
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_language_format() -> dict:
|
|
19
|
+
"""Load language_format.csv into a dict keyed by extension."""
|
|
20
|
+
csv_path = Path(__file__).parent.parent / "data" / "language_format.csv"
|
|
21
|
+
result = {}
|
|
22
|
+
with open(csv_path, 'r') as f:
|
|
23
|
+
reader = csv.DictReader(f)
|
|
24
|
+
for row in reader:
|
|
25
|
+
ext = row.get('extension', '')
|
|
26
|
+
if ext:
|
|
27
|
+
result[ext] = row
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_test_command_for_file(test_file: str, language: Optional[str] = None) -> Optional[str]:
|
|
32
|
+
"""
|
|
33
|
+
Get the appropriate test command for a test file.
|
|
34
|
+
|
|
35
|
+
Resolution order:
|
|
36
|
+
1. CSV run_test_command (if non-empty)
|
|
37
|
+
2. Smart detection via default_verify_cmd_for()
|
|
38
|
+
3. None (triggers agentic fallback)
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
test_file: Path to the test file
|
|
42
|
+
language: Optional language override
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Test command string with {file} placeholder replaced, or None
|
|
46
|
+
"""
|
|
47
|
+
test_path = Path(test_file)
|
|
48
|
+
ext = test_path.suffix
|
|
49
|
+
|
|
50
|
+
resolved_language = language
|
|
51
|
+
if resolved_language is None:
|
|
52
|
+
resolved_language = get_language(ext)
|
|
53
|
+
|
|
54
|
+
# 1. Check CSV for run_test_command
|
|
55
|
+
lang_formats = _load_language_format()
|
|
56
|
+
if ext in lang_formats:
|
|
57
|
+
csv_cmd = lang_formats[ext].get('run_test_command', '').strip()
|
|
58
|
+
if csv_cmd:
|
|
59
|
+
return csv_cmd.replace('{file}', str(test_file))
|
|
60
|
+
|
|
61
|
+
# 2. Smart detection
|
|
62
|
+
if resolved_language:
|
|
63
|
+
smart_cmd = default_verify_cmd_for(resolved_language.lower(), str(test_file))
|
|
64
|
+
if smart_cmd:
|
|
65
|
+
return smart_cmd
|
|
66
|
+
|
|
67
|
+
# 3. No command available
|
|
68
|
+
return None
|