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.
Files changed (119) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +80 -19
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +281 -81
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
@@ -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 = [sys.executable] # Use the current Python 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
- rich_print(f"[yellow]Warning:[/yellow] Program '{os.path.basename(program_path)}' exited with code {process.returncode}.")
75
- if stderr:
76
- rich_print("[yellow]Stderr:[/yellow]")
77
- rich_print(Panel(stderr, border_style="yellow"))
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
- results_log_content += "\n".join(fix_results.get('explanation', ['N/A']))
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
- with open(output_code_path, "w") as f:
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
- with open(output_program_path, "w") as f:
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
- with open(output_results_path, "w") as f:
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:
@@ -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 current working directory.
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 in CWD
374
+ # 4. Use Default Naming Convention
317
375
  else:
318
376
  source = "default"
319
- logger.debug(f"Using default filename '{default_filename}' in current directory.")
320
- final_path = default_filename # Relative to CWD initially
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 keyring
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.set_password(self.keyring_service_name, self.keyring_user_name, refresh_token)
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
- return keyring.get_password(self.keyring_service_name, self.keyring_user_name)
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 keyring.errors.NoKeyringError:
142
- print("No keyring found. Token deletion skipped.")
143
- except keyring.errors.PasswordDeleteError:
144
- print("Failed to delete token from keyring.")
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
- raise TokenError(f"Error exchanging GitHub token for Firebase token: {e}")
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)
@@ -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