pdd-cli 0.0.45__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 +73 -21
- 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 +258 -82
- 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 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +330 -76
- pdd/fix_error_loop.py +207 -61
- 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 +306 -272
- pdd/fix_verification_main.py +28 -9
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +9 -2
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1269 -103
- 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 +100 -905
- pdd/prompts/detect_change_LLM.prompt +122 -20
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- 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 +4 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +228 -108
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- 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 +49 -6
- pdd/sync_determine_operation.py +543 -98
- pdd/sync_main.py +81 -31
- pdd/sync_orchestration.py +1334 -751
- 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.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/construct_paths.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Dict, Tuple, Any, Optional, List
|
|
7
|
+
from typing import Dict, Tuple, Any, Optional, List, Callable
|
|
8
8
|
import fnmatch
|
|
9
9
|
import logging
|
|
10
10
|
|
|
@@ -56,6 +56,23 @@ def _load_pddrc_config(pddrc_path: Path) -> Dict[str, Any]:
|
|
|
56
56
|
except Exception as e:
|
|
57
57
|
raise ValueError(f"Error loading .pddrc: {e}")
|
|
58
58
|
|
|
59
|
+
def list_available_contexts(start_path: Optional[Path] = None) -> list[str]:
|
|
60
|
+
"""Return sorted context names from the nearest .pddrc.
|
|
61
|
+
|
|
62
|
+
- Searches upward from `start_path` (or CWD) for a `.pddrc` file.
|
|
63
|
+
- If found, loads and validates it, then returns sorted context names.
|
|
64
|
+
- If no `.pddrc` exists, returns ["default"].
|
|
65
|
+
- Propagates ValueError for malformed `.pddrc` to allow callers to render
|
|
66
|
+
helpful errors.
|
|
67
|
+
"""
|
|
68
|
+
pddrc = _find_pddrc_file(start_path)
|
|
69
|
+
if not pddrc:
|
|
70
|
+
return ["default"]
|
|
71
|
+
config = _load_pddrc_config(pddrc)
|
|
72
|
+
contexts = config.get("contexts", {})
|
|
73
|
+
names = sorted(contexts.keys()) if isinstance(contexts, dict) else []
|
|
74
|
+
return names or ["default"]
|
|
75
|
+
|
|
59
76
|
def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
|
|
60
77
|
"""Detect the appropriate context based on current directory path."""
|
|
61
78
|
if context_override:
|
|
@@ -108,8 +125,9 @@ def _resolve_config_hierarchy(
|
|
|
108
125
|
# Configuration keys to resolve
|
|
109
126
|
config_keys = {
|
|
110
127
|
'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
|
|
111
|
-
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
128
|
+
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
112
129
|
'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
|
|
130
|
+
'prompts_dir': 'PDD_PROMPTS_DIR',
|
|
113
131
|
'default_language': 'PDD_DEFAULT_LANGUAGE',
|
|
114
132
|
'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
|
|
115
133
|
'strength': None,
|
|
@@ -176,52 +194,41 @@ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
|
|
|
176
194
|
for p in input_files.values():
|
|
177
195
|
if p.suffix == ".prompt":
|
|
178
196
|
return p
|
|
197
|
+
|
|
198
|
+
# Final fallback: Return the first file path available (e.g. for pdd update <code_file>)
|
|
199
|
+
if input_files:
|
|
200
|
+
return next(iter(input_files.values()))
|
|
201
|
+
|
|
179
202
|
return None
|
|
180
203
|
|
|
181
204
|
|
|
182
205
|
# New helper function to check if a language is known
|
|
183
206
|
def _is_known_language(language_name: str) -> bool:
|
|
184
|
-
"""
|
|
207
|
+
"""Return True if the language is recognized.
|
|
208
|
+
|
|
209
|
+
Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set
|
|
210
|
+
so basename/language inference does not fail when PDD_PATH is unset.
|
|
211
|
+
"""
|
|
212
|
+
language_name_lower = (language_name or "").lower()
|
|
213
|
+
if not language_name_lower:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
builtin_languages = {
|
|
217
|
+
'python', 'javascript', 'typescript', 'java', 'cpp', 'c', 'go', 'ruby', 'rust',
|
|
218
|
+
'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
|
|
219
|
+
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
|
|
220
|
+
# Common data and config formats for architecture prompts and configs
|
|
221
|
+
'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
|
|
222
|
+
}
|
|
223
|
+
|
|
185
224
|
pdd_path_str = os.getenv('PDD_PATH')
|
|
186
225
|
if not pdd_path_str:
|
|
187
|
-
|
|
188
|
-
# Or, for an internal helper, we might decide to log and return False,
|
|
189
|
-
# but raising an error for missing config is generally safer.
|
|
190
|
-
# However, _determine_language (the caller) already raises ValueError
|
|
191
|
-
# if language cannot be found, so this path might not be strictly necessary
|
|
192
|
-
# if we assume PDD_PATH is validated earlier or by other get_extension/get_language calls.
|
|
193
|
-
# For robustness here, let's keep a check but perhaps make it less severe if called internally.
|
|
194
|
-
# For now, align with how get_extension might handle it.
|
|
195
|
-
# console.print("[error]PDD_PATH environment variable is not set. Cannot validate language.", style="error")
|
|
196
|
-
# return False # Or raise error
|
|
197
|
-
# Given this is internal and other functions (get_extension) already depend on PDD_PATH,
|
|
198
|
-
# we can assume if those ran, PDD_PATH is set. If not, they'd fail first.
|
|
199
|
-
# So, we can simplify or rely on that pre-condition.
|
|
200
|
-
# Let's assume PDD_PATH will be set if other language functions are working.
|
|
201
|
-
# If it's critical, an explicit check and raise ValueError is better.
|
|
202
|
-
# For now, let's proceed assuming PDD_PATH is available if this point is reached.
|
|
203
|
-
pass # Assuming PDD_PATH is checked by get_extension/get_language if they are called
|
|
204
|
-
|
|
205
|
-
# If PDD_PATH is not set, this will likely fail earlier if get_extension/get_language are used.
|
|
206
|
-
# If we want this helper to be fully independent, it needs robust PDD_PATH handling.
|
|
207
|
-
# Let's assume for now, PDD_PATH is available if this point is reached through normal flow.
|
|
208
|
-
|
|
209
|
-
# Re-evaluate: PDD_PATH is critical for this function. Let's keep the check.
|
|
210
|
-
if not pdd_path_str:
|
|
211
|
-
# This helper might be called before get_extension in some logic paths
|
|
212
|
-
# if _determine_language prioritizes suffix checking first.
|
|
213
|
-
# So, it needs its own PDD_PATH check.
|
|
214
|
-
# Raise ValueError to be consistent with get_extension's behavior.
|
|
215
|
-
raise ValueError("PDD_PATH environment variable is not set. Cannot validate language.")
|
|
226
|
+
return language_name_lower in builtin_languages
|
|
216
227
|
|
|
217
228
|
csv_file_path = Path(pdd_path_str) / 'data' / 'language_format.csv'
|
|
218
|
-
|
|
219
229
|
if not csv_file_path.is_file():
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
language_name_lower = language_name.lower()
|
|
224
|
-
|
|
230
|
+
return language_name_lower in builtin_languages
|
|
231
|
+
|
|
225
232
|
try:
|
|
226
233
|
with open(csv_file_path, mode='r', encoding='utf-8', newline='') as csvfile:
|
|
227
234
|
reader = csv.DictReader(csvfile)
|
|
@@ -229,10 +236,10 @@ def _is_known_language(language_name: str) -> bool:
|
|
|
229
236
|
if row.get('language', '').lower() == language_name_lower:
|
|
230
237
|
return True
|
|
231
238
|
except csv.Error as e:
|
|
232
|
-
# Log and return False or raise a custom error
|
|
233
239
|
console.print(f"[error]CSV Error reading {csv_file_path}: {e}", style="error")
|
|
234
|
-
return
|
|
235
|
-
|
|
240
|
+
return language_name_lower in builtin_languages
|
|
241
|
+
|
|
242
|
+
return language_name_lower in builtin_languages
|
|
236
243
|
|
|
237
244
|
|
|
238
245
|
def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
|
|
@@ -264,6 +271,24 @@ def _extract_basename(
|
|
|
264
271
|
"""
|
|
265
272
|
Deduce the project basename according to the rules explained in *Step A*.
|
|
266
273
|
"""
|
|
274
|
+
# Handle 'fix' command specifically to create a unique basename per test file
|
|
275
|
+
if command == "fix":
|
|
276
|
+
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
277
|
+
if not prompt_path:
|
|
278
|
+
raise ValueError("Could not determine prompt file for 'fix' command.")
|
|
279
|
+
|
|
280
|
+
prompt_basename = _strip_language_suffix(prompt_path)
|
|
281
|
+
|
|
282
|
+
unit_test_path = input_file_paths.get("unit_test_file")
|
|
283
|
+
if not unit_test_path:
|
|
284
|
+
# Fallback to just the prompt basename if no unit test file is provided
|
|
285
|
+
# This might happen in some edge cases, but 'fix' command structure requires it
|
|
286
|
+
return prompt_basename
|
|
287
|
+
|
|
288
|
+
# Use the stem of the unit test file to make the basename unique
|
|
289
|
+
test_basename = Path(unit_test_path).stem
|
|
290
|
+
return f"{prompt_basename}_{test_basename}"
|
|
291
|
+
|
|
267
292
|
# Handle conflicts first due to its unique structure
|
|
268
293
|
if command == "conflicts":
|
|
269
294
|
key1 = "prompt1"
|
|
@@ -331,14 +356,48 @@ def _determine_language(
|
|
|
331
356
|
ext = path_obj.suffix
|
|
332
357
|
# Prioritize non-prompt code files
|
|
333
358
|
if ext and ext != ".prompt":
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
359
|
+
try:
|
|
360
|
+
language = get_language(ext)
|
|
361
|
+
if language:
|
|
362
|
+
return language.lower()
|
|
363
|
+
except ValueError:
|
|
364
|
+
# Fallback: load language CSV file directly when PDD_PATH is not set
|
|
365
|
+
try:
|
|
366
|
+
import csv
|
|
367
|
+
import os
|
|
368
|
+
# Try to find the CSV file relative to this script
|
|
369
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
370
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
371
|
+
if os.path.exists(csv_path):
|
|
372
|
+
with open(csv_path, 'r') as csvfile:
|
|
373
|
+
reader = csv.DictReader(csvfile)
|
|
374
|
+
for row in reader:
|
|
375
|
+
if row['extension'].lower() == ext.lower():
|
|
376
|
+
return row['language'].lower()
|
|
377
|
+
except (FileNotFoundError, csv.Error):
|
|
378
|
+
pass
|
|
337
379
|
# Handle files without extension like Makefile
|
|
338
380
|
elif not ext and path_obj.is_file(): # Check it's actually a file
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
381
|
+
try:
|
|
382
|
+
language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
|
|
383
|
+
if language:
|
|
384
|
+
return language.lower()
|
|
385
|
+
except ValueError:
|
|
386
|
+
# Fallback: load language CSV file directly for files without extension
|
|
387
|
+
try:
|
|
388
|
+
import csv
|
|
389
|
+
import os
|
|
390
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
391
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
392
|
+
if os.path.exists(csv_path):
|
|
393
|
+
with open(csv_path, 'r') as csvfile:
|
|
394
|
+
reader = csv.DictReader(csvfile)
|
|
395
|
+
for row in reader:
|
|
396
|
+
# Check if the filename matches (for files without extension)
|
|
397
|
+
if not row['extension'] and path_obj.name.lower() == row['language'].lower():
|
|
398
|
+
return row['language'].lower()
|
|
399
|
+
except (FileNotFoundError, csv.Error):
|
|
400
|
+
pass
|
|
342
401
|
|
|
343
402
|
# 3 – parse from prompt filename suffix
|
|
344
403
|
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
@@ -354,7 +413,7 @@ def _determine_language(
|
|
|
354
413
|
|
|
355
414
|
# 4 - Special handling for detect command - default to prompt for LLM prompts
|
|
356
415
|
if command == "detect" and "change_file" in input_file_paths:
|
|
357
|
-
return "prompt"
|
|
416
|
+
return "prompt"
|
|
358
417
|
|
|
359
418
|
# 5 - If no language determined, raise error
|
|
360
419
|
raise ValueError("Could not determine language from input files or options.")
|
|
@@ -374,6 +433,8 @@ def construct_paths(
|
|
|
374
433
|
command_options: Optional[Dict[str, Any]], # Allow None
|
|
375
434
|
create_error_file: bool = True, # Added parameter to control error file creation
|
|
376
435
|
context_override: Optional[str] = None, # Added parameter for context override
|
|
436
|
+
confirm_callback: Optional[Callable[[str, str], bool]] = None, # Callback for interactive confirmation
|
|
437
|
+
path_resolution_mode: Optional[str] = None, # "cwd" or "config_base" - if None, use command default
|
|
377
438
|
) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
|
|
378
439
|
"""
|
|
379
440
|
High‑level orchestrator that loads inputs, determines basename/language,
|
|
@@ -390,6 +451,7 @@ def construct_paths(
|
|
|
390
451
|
|
|
391
452
|
# ------------- Load .pddrc configuration -----------------
|
|
392
453
|
pddrc_config = {}
|
|
454
|
+
pddrc_path: Optional[Path] = None
|
|
393
455
|
context = None
|
|
394
456
|
context_config = {}
|
|
395
457
|
original_context_config = {} # Keep track of original context config for sync discovery
|
|
@@ -450,7 +512,10 @@ def construct_paths(
|
|
|
450
512
|
language="python", # Dummy language
|
|
451
513
|
file_extension=".py", # Dummy extension
|
|
452
514
|
context_config=context_config,
|
|
515
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
516
|
+
path_resolution_mode="cwd", # Sync resolves paths relative to CWD
|
|
453
517
|
)
|
|
518
|
+
|
|
454
519
|
# Infer base directories from a sample output path
|
|
455
520
|
gen_path = Path(output_paths_str.get("generate_output_path", "src"))
|
|
456
521
|
|
|
@@ -467,9 +532,9 @@ def construct_paths(
|
|
|
467
532
|
# Fall back to context-aware logic
|
|
468
533
|
# Use original_context_config to avoid checking augmented config with env vars
|
|
469
534
|
if original_context_config and any(key.endswith('_output_path') for key in original_context_config):
|
|
470
|
-
# For configured contexts,
|
|
471
|
-
#
|
|
472
|
-
resolved_config["prompts_dir"] = "prompts"
|
|
535
|
+
# For configured contexts, use prompts_dir from config if provided,
|
|
536
|
+
# otherwise default to "prompts" at the same level as output dirs
|
|
537
|
+
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
|
|
473
538
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
474
539
|
else:
|
|
475
540
|
# For default contexts, maintain relative relationship
|
|
@@ -478,7 +543,14 @@ def construct_paths(
|
|
|
478
543
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
479
544
|
|
|
480
545
|
resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
|
|
481
|
-
|
|
546
|
+
# example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
547
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
548
|
+
example_path_str = output_paths_str.get("example_output_path", "examples")
|
|
549
|
+
example_path = Path(example_path_str)
|
|
550
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
551
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
552
|
+
else:
|
|
553
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
482
554
|
|
|
483
555
|
except Exception as e:
|
|
484
556
|
console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
|
|
@@ -497,11 +569,15 @@ def construct_paths(
|
|
|
497
569
|
for key, path_str in input_file_paths.items():
|
|
498
570
|
try:
|
|
499
571
|
path = Path(path_str).expanduser()
|
|
500
|
-
# Resolve non-error files strictly first
|
|
572
|
+
# Resolve non-error files strictly first, but be more lenient for sync command
|
|
501
573
|
if key != "error_file":
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
574
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
575
|
+
if command == "sync":
|
|
576
|
+
input_paths[key] = path.resolve()
|
|
577
|
+
else:
|
|
578
|
+
# Let FileNotFoundError propagate naturally if path doesn't exist
|
|
579
|
+
resolved_path = path.resolve(strict=True)
|
|
580
|
+
input_paths[key] = resolved_path
|
|
505
581
|
else:
|
|
506
582
|
# Resolve error file non-strictly, existence checked later
|
|
507
583
|
input_paths[key] = path.resolve()
|
|
@@ -531,9 +607,14 @@ def construct_paths(
|
|
|
531
607
|
|
|
532
608
|
# Check existence again, especially for error_file which might have been created
|
|
533
609
|
if not path.exists():
|
|
534
|
-
#
|
|
535
|
-
|
|
536
|
-
|
|
610
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
611
|
+
if command == "sync":
|
|
612
|
+
# Skip reading content for non-existent files in sync mode
|
|
613
|
+
continue
|
|
614
|
+
else:
|
|
615
|
+
# This case should ideally be caught by resolve(strict=True) earlier for non-error files
|
|
616
|
+
# Raise standard FileNotFoundError
|
|
617
|
+
raise FileNotFoundError(f"{path}")
|
|
537
618
|
|
|
538
619
|
if path.is_file(): # Read only if it's a file
|
|
539
620
|
try:
|
|
@@ -598,7 +679,25 @@ def construct_paths(
|
|
|
598
679
|
style="warning"
|
|
599
680
|
)
|
|
600
681
|
|
|
601
|
-
|
|
682
|
+
|
|
683
|
+
# Try to get extension from CSV; fallback to built-in mapping if PDD_PATH/CSV unavailable
|
|
684
|
+
try:
|
|
685
|
+
file_extension = get_extension(language) # Pass determined language
|
|
686
|
+
if not file_extension and (language or '').lower() != 'prompt':
|
|
687
|
+
raise ValueError('empty extension')
|
|
688
|
+
except Exception:
|
|
689
|
+
builtin_ext_map = {
|
|
690
|
+
'python': '.py', 'javascript': '.js', 'typescript': '.ts', 'java': '.java',
|
|
691
|
+
'cpp': '.cpp', 'c': '.c', 'go': '.go', 'ruby': '.rb', 'rust': '.rs',
|
|
692
|
+
'kotlin': '.kt', 'swift': '.swift', 'csharp': '.cs', 'php': '.php',
|
|
693
|
+
'scala': '.scala', 'r': '.r', 'lua': '.lua', 'perl': '.pl', 'bash': '.sh',
|
|
694
|
+
'shell': '.sh', 'powershell': '.ps1', 'sql': '.sql', 'html': '.html', 'css': '.css',
|
|
695
|
+
'prompt': '.prompt', 'makefile': '',
|
|
696
|
+
# Common data/config formats
|
|
697
|
+
'json': '.json', 'jsonl': '.jsonl', 'yaml': '.yaml', 'yml': '.yml', 'toml': '.toml', 'ini': '.ini'
|
|
698
|
+
}
|
|
699
|
+
file_extension = builtin_ext_map.get(language.lower(), f".{language.lower()}" if language else '')
|
|
700
|
+
|
|
602
701
|
|
|
603
702
|
|
|
604
703
|
# ------------- Step 3b: build output paths ---------------
|
|
@@ -608,10 +707,52 @@ def construct_paths(
|
|
|
608
707
|
if k.startswith("output") and v is not None # Ensure value is not None
|
|
609
708
|
}
|
|
610
709
|
|
|
710
|
+
# Determine input file directory for default output path generation
|
|
711
|
+
# Only apply for commands that generate/update files based on specific input files
|
|
712
|
+
# Commands like sync, generate, test, example have their own directory management
|
|
713
|
+
commands_using_input_dir = {'fix', 'crash', 'verify', 'split', 'change', 'update'}
|
|
714
|
+
input_file_dir: Optional[str] = None
|
|
715
|
+
input_file_dirs: Dict[str, Optional[str]] = {}
|
|
716
|
+
if input_paths and command in commands_using_input_dir:
|
|
717
|
+
try:
|
|
718
|
+
# For fix/crash/verify commands, use specific file directories for each output
|
|
719
|
+
if command in {'fix', 'crash', 'verify'}:
|
|
720
|
+
# Map output keys to their corresponding input file keys
|
|
721
|
+
input_key_map = {
|
|
722
|
+
'fix': {'output_code': 'code_file', 'output_test': 'unit_test_file', 'output_results': 'code_file'},
|
|
723
|
+
'crash': {'output': 'code_file', 'output_program': 'program_file'},
|
|
724
|
+
'verify': {'output_code': 'code_file', 'output_program': 'verification_program', 'output_results': 'code_file'},
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for output_key, input_key in input_key_map.get(command, {}).items():
|
|
728
|
+
if input_key in input_paths:
|
|
729
|
+
input_file_dirs[output_key] = str(input_paths[input_key].parent)
|
|
730
|
+
|
|
731
|
+
# Set default input_file_dir to code_file directory as fallback
|
|
732
|
+
if 'code_file' in input_paths:
|
|
733
|
+
input_file_dir = str(input_paths['code_file'].parent)
|
|
734
|
+
else:
|
|
735
|
+
first_input_path = next(iter(input_paths.values()))
|
|
736
|
+
input_file_dir = str(first_input_path.parent)
|
|
737
|
+
else:
|
|
738
|
+
# For other commands, use first input path
|
|
739
|
+
first_input_path = next(iter(input_paths.values()))
|
|
740
|
+
input_file_dir = str(first_input_path.parent)
|
|
741
|
+
except (StopIteration, AttributeError):
|
|
742
|
+
# If no input paths or path doesn't have parent, use None (falls back to CWD)
|
|
743
|
+
pass
|
|
744
|
+
|
|
611
745
|
try:
|
|
612
746
|
# generate_output_paths might return Dict[str, str] or Dict[str, Path]
|
|
613
747
|
# Let's assume it returns Dict[str, str] based on verification error,
|
|
614
748
|
# and convert them to Path objects here.
|
|
749
|
+
# Determine path resolution mode:
|
|
750
|
+
# - If explicitly provided, use it
|
|
751
|
+
# - Otherwise: sync uses "cwd", other commands use "config_base"
|
|
752
|
+
effective_path_resolution_mode = path_resolution_mode
|
|
753
|
+
if effective_path_resolution_mode is None:
|
|
754
|
+
effective_path_resolution_mode = "cwd" if command == "sync" else "config_base"
|
|
755
|
+
|
|
615
756
|
output_paths_str: Dict[str, str] = generate_output_paths(
|
|
616
757
|
command=command,
|
|
617
758
|
output_locations=output_location_opts,
|
|
@@ -619,7 +760,12 @@ def construct_paths(
|
|
|
619
760
|
language=language,
|
|
620
761
|
file_extension=file_extension,
|
|
621
762
|
context_config=context_config,
|
|
763
|
+
input_file_dir=input_file_dir,
|
|
764
|
+
input_file_dirs=input_file_dirs,
|
|
765
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
766
|
+
path_resolution_mode=effective_path_resolution_mode,
|
|
622
767
|
)
|
|
768
|
+
|
|
623
769
|
# Convert to Path objects for internal use
|
|
624
770
|
output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
|
|
625
771
|
|
|
@@ -628,36 +774,59 @@ def construct_paths(
|
|
|
628
774
|
raise # Re-raise the ValueError
|
|
629
775
|
|
|
630
776
|
# ------------- Step 4: overwrite confirmation ------------
|
|
631
|
-
#
|
|
777
|
+
# Initialize existing_files before the conditional to avoid UnboundLocalError
|
|
632
778
|
existing_files: Dict[str, Path] = {}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
779
|
+
|
|
780
|
+
if command in ["test", "bug"] and not force:
|
|
781
|
+
# For test/bug commands without --force, create numbered files instead of overwriting
|
|
782
|
+
for key, path in output_paths_resolved.items():
|
|
783
|
+
if path.is_file():
|
|
784
|
+
base, ext = os.path.splitext(path)
|
|
785
|
+
i = 1
|
|
786
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
787
|
+
while new_path.exists():
|
|
788
|
+
i += 1
|
|
789
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
790
|
+
output_paths_resolved[key] = new_path
|
|
791
|
+
else:
|
|
792
|
+
# Check if any output *file* exists (operate on Path objects)
|
|
793
|
+
for k, p_obj in output_paths_resolved.items():
|
|
794
|
+
if p_obj.is_file():
|
|
795
|
+
existing_files[k] = p_obj # Store the Path object
|
|
637
796
|
|
|
638
797
|
if existing_files and not force:
|
|
798
|
+
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
639
799
|
if not quiet:
|
|
640
800
|
# Use the Path objects stored in existing_files for resolve()
|
|
641
801
|
# Print without Rich tags for easier testing
|
|
642
|
-
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
643
802
|
console.print(
|
|
644
803
|
f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
|
|
645
804
|
style="warning"
|
|
646
805
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
click.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
806
|
+
|
|
807
|
+
# Use confirm_callback if provided (for TUI environments), otherwise use click.confirm
|
|
808
|
+
if confirm_callback is not None:
|
|
809
|
+
# Use the provided callback for confirmation (e.g., from Textual TUI)
|
|
810
|
+
confirm_message = f"The following files will be overwritten:\n{paths_list}\n\nOverwrite existing files?"
|
|
811
|
+
if not confirm_callback(confirm_message, "Overwrite Confirmation"):
|
|
812
|
+
raise click.Abort()
|
|
813
|
+
else:
|
|
814
|
+
# Use click.confirm for CLI interaction
|
|
815
|
+
try:
|
|
816
|
+
if not click.confirm(
|
|
817
|
+
click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
|
|
818
|
+
):
|
|
819
|
+
click.secho("Operation cancelled.", fg="red", err=True)
|
|
820
|
+
raise click.Abort()
|
|
821
|
+
except click.Abort:
|
|
822
|
+
raise # Let Abort propagate to be handled by PDDCLI.invoke()
|
|
823
|
+
except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
|
|
824
|
+
if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
|
|
825
|
+
# Non-interactive environment, default to not overwriting
|
|
826
|
+
click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
|
|
827
|
+
else:
|
|
828
|
+
click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
|
|
829
|
+
raise click.Abort()
|
|
661
830
|
|
|
662
831
|
|
|
663
832
|
# ------------- Final reporting ---------------------------
|
|
@@ -685,7 +854,14 @@ def construct_paths(
|
|
|
685
854
|
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
|
|
686
855
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
687
856
|
resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
|
|
688
|
-
|
|
857
|
+
# example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
858
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
859
|
+
example_path_str = resolved_config.get("example_output_path", "examples")
|
|
860
|
+
example_path = Path(example_path_str)
|
|
861
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
862
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
863
|
+
else:
|
|
864
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
689
865
|
|
|
690
866
|
|
|
691
|
-
return resolved_config, input_strings, output_file_paths_str_return, language
|
|
867
|
+
return resolved_config, input_strings, output_file_paths_str_return, language
|
pdd/context_generator.py
CHANGED
|
@@ -16,6 +16,9 @@ def context_generator(
|
|
|
16
16
|
temperature: float = 0,
|
|
17
17
|
time: Optional[float] = DEFAULT_TIME,
|
|
18
18
|
verbose: bool = False,
|
|
19
|
+
source_file_path: str = None,
|
|
20
|
+
example_file_path: str = None,
|
|
21
|
+
module_name: str = None,
|
|
19
22
|
) -> tuple:
|
|
20
23
|
"""
|
|
21
24
|
Generates a concise example on how to use a given code module properly.
|
|
@@ -79,7 +82,10 @@ def context_generator(
|
|
|
79
82
|
input_json={
|
|
80
83
|
"code_module": code_module,
|
|
81
84
|
"processed_prompt": processed_prompt,
|
|
82
|
-
"language": language
|
|
85
|
+
"language": language,
|
|
86
|
+
"source_file_path": source_file_path or "",
|
|
87
|
+
"example_file_path": example_file_path or "",
|
|
88
|
+
"module_name": module_name or ""
|
|
83
89
|
},
|
|
84
90
|
strength=strength,
|
|
85
91
|
temperature=temperature,
|
|
@@ -95,6 +101,7 @@ def context_generator(
|
|
|
95
101
|
strength=0.5,
|
|
96
102
|
temperature=temperature,
|
|
97
103
|
time=time,
|
|
104
|
+
language=language,
|
|
98
105
|
verbose=verbose
|
|
99
106
|
)
|
|
100
107
|
except Exception as e:
|
|
@@ -112,6 +119,7 @@ def context_generator(
|
|
|
112
119
|
strength=strength,
|
|
113
120
|
temperature=temperature,
|
|
114
121
|
time=time,
|
|
122
|
+
language=language,
|
|
115
123
|
verbose=verbose
|
|
116
124
|
)
|
|
117
125
|
total_cost = llm_response['cost'] + unfinished_cost + continue_cost
|
|
@@ -149,4 +157,4 @@ if __name__ == "__main__":
|
|
|
149
157
|
print("[bold green]Generated Example Code:[/bold green]")
|
|
150
158
|
print(example_code)
|
|
151
159
|
print(f"[bold blue]Total Cost: ${total_cost:.6f}[/bold blue]")
|
|
152
|
-
print(f"[bold blue]Model Name: {model_name}[/bold blue]")
|
|
160
|
+
print(f"[bold blue]Model Name: {model_name}[/bold blue]")
|