pdd-cli 0.0.24__py3-none-any.whl → 0.0.26__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.
Potentially problematic release.
This version of pdd-cli might be problematic. Click here for more details.
- pdd/__init__.py +14 -1
- pdd/bug_main.py +5 -1
- pdd/bug_to_unit_test.py +16 -5
- pdd/change.py +2 -1
- pdd/change_main.py +407 -189
- pdd/cli.py +853 -301
- pdd/code_generator.py +2 -1
- pdd/conflicts_in_prompts.py +2 -1
- pdd/construct_paths.py +377 -222
- pdd/context_generator.py +2 -1
- pdd/continue_generation.py +5 -2
- pdd/crash_main.py +55 -20
- pdd/data/llm_model.csv +18 -17
- pdd/detect_change.py +2 -1
- pdd/fix_code_loop.py +465 -160
- pdd/fix_code_module_errors.py +7 -4
- pdd/fix_error_loop.py +9 -9
- pdd/fix_errors_from_unit_tests.py +207 -365
- pdd/fix_main.py +32 -4
- pdd/fix_verification_errors.py +148 -77
- pdd/fix_verification_errors_loop.py +842 -768
- pdd/fix_verification_main.py +412 -0
- pdd/generate_output_paths.py +427 -189
- pdd/generate_test.py +3 -2
- pdd/increase_tests.py +2 -2
- pdd/llm_invoke.py +1167 -343
- pdd/preprocess.py +3 -3
- pdd/process_csv_change.py +466 -154
- pdd/prompts/bug_to_unit_test_LLM.prompt +11 -11
- pdd/prompts/extract_prompt_update_LLM.prompt +11 -5
- pdd/prompts/extract_unit_code_fix_LLM.prompt +2 -2
- pdd/prompts/find_verification_errors_LLM.prompt +11 -9
- pdd/prompts/fix_code_module_errors_LLM.prompt +29 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +5 -5
- pdd/prompts/fix_verification_errors_LLM.prompt +8 -1
- pdd/prompts/generate_test_LLM.prompt +9 -3
- pdd/prompts/trim_results_start_LLM.prompt +1 -1
- pdd/prompts/update_prompt_LLM.prompt +3 -3
- pdd/split.py +6 -5
- pdd/split_main.py +13 -4
- pdd/trace_main.py +7 -0
- pdd/update_model_costs.py +446 -0
- pdd/xml_tagger.py +2 -1
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/METADATA +8 -16
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/RECORD +49 -47
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/WHEEL +1 -1
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/top_level.txt +0 -0
pdd/code_generator.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Tuple
|
|
2
2
|
from rich.console import Console
|
|
3
|
+
from . import EXTRACTION_STRENGTH
|
|
3
4
|
from .preprocess import preprocess
|
|
4
5
|
from .llm_invoke import llm_invoke
|
|
5
6
|
from .unfinished_prompt import unfinished_prompt
|
|
@@ -104,7 +105,7 @@ def code_generator(
|
|
|
104
105
|
runnable_code, postprocess_cost, model_name_post = postprocess(
|
|
105
106
|
llm_output=final_output,
|
|
106
107
|
language=language,
|
|
107
|
-
strength=
|
|
108
|
+
strength=EXTRACTION_STRENGTH,
|
|
108
109
|
temperature=0.0,
|
|
109
110
|
verbose=verbose
|
|
110
111
|
)
|
pdd/conflicts_in_prompts.py
CHANGED
|
@@ -4,6 +4,7 @@ from rich import print as rprint
|
|
|
4
4
|
from rich.markdown import Markdown
|
|
5
5
|
from .load_prompt_template import load_prompt_template
|
|
6
6
|
from .llm_invoke import llm_invoke
|
|
7
|
+
from . import EXTRACTION_STRENGTH
|
|
7
8
|
|
|
8
9
|
class ConflictChange(BaseModel):
|
|
9
10
|
prompt_name: str = Field(description="Name of the prompt that needs to be changed")
|
|
@@ -85,7 +86,7 @@ def conflicts_in_prompts(
|
|
|
85
86
|
extract_response = llm_invoke(
|
|
86
87
|
prompt=extract_prompt,
|
|
87
88
|
input_json=extract_input,
|
|
88
|
-
strength=
|
|
89
|
+
strength=EXTRACTION_STRENGTH,
|
|
89
90
|
temperature=temperature,
|
|
90
91
|
output_pydantic=ConflictResponse,
|
|
91
92
|
verbose=verbose
|
pdd/construct_paths.py
CHANGED
|
@@ -1,251 +1,406 @@
|
|
|
1
|
+
# pdd/construct_paths.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
1
5
|
import os
|
|
2
|
-
import csv
|
|
3
6
|
from pathlib import Path
|
|
4
|
-
from typing import Dict, Tuple, Optional
|
|
7
|
+
from typing import Dict, Tuple, Any, Optional # Added Optional
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
from rich.
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.theme import Theme
|
|
8
12
|
|
|
9
|
-
from .generate_output_paths import generate_output_paths
|
|
10
13
|
from .get_extension import get_extension
|
|
11
14
|
from .get_language import get_language
|
|
15
|
+
from .generate_output_paths import generate_output_paths
|
|
16
|
+
|
|
17
|
+
# Assume generate_output_paths raises ValueError on unknown command
|
|
18
|
+
|
|
19
|
+
console = Console(theme=Theme({"info": "cyan", "warning": "yellow", "error": "bold red"}))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _read_file(path: Path) -> str:
|
|
23
|
+
"""Read a text file safely and return its contents."""
|
|
24
|
+
try:
|
|
25
|
+
return path.read_text(encoding="utf-8")
|
|
26
|
+
except Exception as exc: # pragma: no cover
|
|
27
|
+
# Error is raised in the main function after this fails
|
|
28
|
+
console.print(f"[error]Could not read {path}: {exc}", style="error")
|
|
29
|
+
raise
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ensure_error_file(path: Path, quiet: bool) -> None:
|
|
33
|
+
"""Create an empty error log file if it doesn't exist."""
|
|
34
|
+
if not path.exists():
|
|
35
|
+
if not quiet:
|
|
36
|
+
# Use console.print from the main module scope
|
|
37
|
+
# Print without Rich tags for easier testing
|
|
38
|
+
console.print(f"Warning: Error file '{path.resolve()}' does not exist. Creating an empty file.", style="warning")
|
|
39
|
+
try:
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
path.touch()
|
|
42
|
+
except Exception as exc: # pragma: no cover
|
|
43
|
+
console.print(f"[error]Could not create error file {path}: {exc}", style="error")
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
|
|
48
|
+
"""Return the path most likely to be the prompt file, if any."""
|
|
49
|
+
# Prioritize specific keys known to hold the primary prompt
|
|
50
|
+
for key in (
|
|
51
|
+
"prompt_file", # generate, test, fix, crash, trace, verify, auto-deps
|
|
52
|
+
"input_prompt", # split
|
|
53
|
+
"input_prompt_file", # update, change (non-csv), bug
|
|
54
|
+
"prompt1", # conflicts
|
|
55
|
+
# Less common / potentially ambiguous keys last
|
|
56
|
+
"change_prompt_file", # change (specific case handled in _extract_basename)
|
|
57
|
+
):
|
|
58
|
+
if key in input_files:
|
|
59
|
+
return input_files[key]
|
|
60
|
+
|
|
61
|
+
# Fallback: first file with a .prompt extension if no specific key matches
|
|
62
|
+
for p in input_files.values():
|
|
63
|
+
if p.suffix == ".prompt":
|
|
64
|
+
return p
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Remove trailing '_<language>.prompt' or '_<language>' from a filename stem
|
|
71
|
+
if it matches a known language.
|
|
72
|
+
"""
|
|
73
|
+
p = Path(path_like)
|
|
74
|
+
stem = p.stem # removes last extension (e.g. '.prompt', '.py')
|
|
12
75
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
raise ValueError("Environment variable 'PDD_PATH' is not set")
|
|
16
|
-
csv_file_path = os.path.join(pdd_path, 'data', 'language_format.csv')
|
|
76
|
+
if "_" not in stem: # No underscore, nothing to strip
|
|
77
|
+
return stem
|
|
17
78
|
|
|
18
|
-
|
|
19
|
-
|
|
79
|
+
parts = stem.split("_")
|
|
80
|
+
# Avoid splitting single-word stems like "Makefile_" if that's possible
|
|
81
|
+
if len(parts) < 2:
|
|
82
|
+
return stem
|
|
20
83
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
84
|
+
candidate_lang = parts[-1]
|
|
85
|
+
|
|
86
|
+
# Check if the last part is a known language
|
|
87
|
+
if get_extension(candidate_lang) != "": # recognised language
|
|
88
|
+
# If the last part is a language, strip it
|
|
89
|
+
return "_".join(parts[:-1])
|
|
90
|
+
else:
|
|
91
|
+
# Last part is not a language, return original stem
|
|
92
|
+
return stem
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_basename(
|
|
96
|
+
command: str,
|
|
97
|
+
input_file_paths: Dict[str, Path],
|
|
98
|
+
) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Deduce the project basename according to the rules explained in *Step A*.
|
|
101
|
+
"""
|
|
102
|
+
# Handle conflicts first due to its unique structure
|
|
103
|
+
if command == "conflicts":
|
|
104
|
+
key1 = "prompt1"
|
|
105
|
+
key2 = "prompt2"
|
|
106
|
+
# Ensure keys exist before proceeding
|
|
107
|
+
if key1 in input_file_paths and key2 in input_file_paths:
|
|
108
|
+
p1 = Path(input_file_paths[key1])
|
|
109
|
+
p2 = Path(input_file_paths[key2])
|
|
110
|
+
base1 = _strip_language_suffix(p1)
|
|
111
|
+
base2 = _strip_language_suffix(p2)
|
|
112
|
+
# Combine basenames, ensure order for consistency (sorted)
|
|
113
|
+
return "_".join(sorted([base1, base2]))
|
|
114
|
+
# else: Fall through might occur if keys missing, handled by general logic/fallback
|
|
115
|
+
|
|
116
|
+
# Special‑case commands that choose a non‑prompt file for the basename
|
|
117
|
+
elif command == "detect":
|
|
118
|
+
key = "change_file"
|
|
119
|
+
if key in input_file_paths:
|
|
120
|
+
# Basename is from change_file, no language suffix stripping needed usually
|
|
121
|
+
return Path(input_file_paths[key]).stem
|
|
122
|
+
elif command == "change":
|
|
123
|
+
# If change_prompt_file is given, use its stem (no language strip needed per convention)
|
|
124
|
+
if "change_prompt_file" in input_file_paths:
|
|
125
|
+
return Path(input_file_paths["change_prompt_file"]).stem
|
|
126
|
+
# If --csv is used or change_prompt_file is absent, fall through to general logic
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# General case: Use the primary prompt file
|
|
130
|
+
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
131
|
+
if prompt_path:
|
|
132
|
+
return _strip_language_suffix(prompt_path)
|
|
133
|
+
|
|
134
|
+
# Fallback: If no prompt found (e.g., command only takes code files?),
|
|
135
|
+
# use the first input file's stem. This requires input_file_paths not to be empty.
|
|
136
|
+
# This fallback is reached only if input_file_paths is not empty (checked earlier)
|
|
137
|
+
first_path = next(iter(input_file_paths.values()))
|
|
138
|
+
# Should we strip language here too? Let's be consistent.
|
|
139
|
+
return _strip_language_suffix(first_path)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _determine_language(
|
|
143
|
+
command_options: Dict[str, Any], # Keep original type hint
|
|
144
|
+
input_file_paths: Dict[str, Path],
|
|
145
|
+
command: str = "", # New parameter for the command name
|
|
146
|
+
) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Apply the language discovery strategy.
|
|
149
|
+
Priority: Explicit option > Code/Test file extension > Prompt filename suffix.
|
|
150
|
+
For 'detect' command, default to 'prompt' as it typically doesn't need a language.
|
|
151
|
+
"""
|
|
152
|
+
# Diagnostic check for None (should be handled by caller, but for safety)
|
|
153
|
+
command_options = command_options or {}
|
|
154
|
+
# 1 – explicit option
|
|
155
|
+
explicit_lang = command_options.get("language")
|
|
156
|
+
if explicit_lang:
|
|
157
|
+
lang_lower = explicit_lang.lower()
|
|
158
|
+
# Optional: Validate known language? Let's assume valid for now.
|
|
159
|
+
return lang_lower
|
|
160
|
+
|
|
161
|
+
# 2 – infer from extension of any code/test file (excluding .prompt)
|
|
162
|
+
# Iterate through values, ensuring consistent order if needed (e.g., sort keys)
|
|
163
|
+
# For now, rely on dict order (Python 3.7+)
|
|
164
|
+
for key, p in input_file_paths.items():
|
|
165
|
+
path_obj = Path(p)
|
|
166
|
+
ext = path_obj.suffix
|
|
167
|
+
# Prioritize non-prompt code files
|
|
168
|
+
if ext and ext != ".prompt":
|
|
169
|
+
language = get_language(ext)
|
|
170
|
+
if language:
|
|
171
|
+
return language.lower()
|
|
172
|
+
# Handle files without extension like Makefile
|
|
173
|
+
elif not ext and path_obj.is_file(): # Check it's actually a file
|
|
174
|
+
language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
|
|
175
|
+
if language:
|
|
176
|
+
return language.lower()
|
|
177
|
+
|
|
178
|
+
# 3 – parse from prompt filename suffix
|
|
179
|
+
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
180
|
+
if prompt_path and prompt_path.suffix == ".prompt":
|
|
181
|
+
stem = prompt_path.stem
|
|
182
|
+
if "_" in stem:
|
|
183
|
+
parts = stem.split("_")
|
|
184
|
+
if len(parts) >= 2:
|
|
185
|
+
token = parts[-1]
|
|
186
|
+
# Check if the token is a known language
|
|
187
|
+
if get_extension(token) != "":
|
|
188
|
+
return token.lower()
|
|
189
|
+
|
|
190
|
+
# 4 - Special handling for detect command - default to prompt for LLM prompts
|
|
191
|
+
if command == "detect" and "change_file" in input_file_paths:
|
|
192
|
+
return "prompt" # Default to prompt for detect command
|
|
193
|
+
|
|
194
|
+
# 5 - If no language determined, raise error
|
|
195
|
+
raise ValueError("Could not determine language from input files or options.")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _paths_exist(paths: Dict[str, Path]) -> bool: # Value type is Path
|
|
199
|
+
"""Return True if any of the given paths is an existing file."""
|
|
200
|
+
# Check specifically for files, not directories
|
|
201
|
+
return any(p.is_file() for p in paths.values())
|
|
26
202
|
|
|
27
|
-
# We also treat "prompt" as a recognized suffix
|
|
28
|
-
EXTENDED_LANGUAGES = KNOWN_LANGUAGES.union({"prompt"})
|
|
29
203
|
|
|
30
204
|
def construct_paths(
|
|
31
205
|
input_file_paths: Dict[str, str],
|
|
32
206
|
force: bool,
|
|
33
207
|
quiet: bool,
|
|
34
208
|
command: str,
|
|
35
|
-
command_options: Dict[str,
|
|
209
|
+
command_options: Optional[Dict[str, Any]], # Allow None
|
|
210
|
+
create_error_file: bool = True, # Added parameter to control error file creation
|
|
36
211
|
) -> Tuple[Dict[str, str], Dict[str, str], str]:
|
|
37
212
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
213
|
+
High‑level orchestrator that loads inputs, determines basename/language,
|
|
214
|
+
computes output locations, and verifies overwrite rules.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
(input_strings, output_file_paths, language)
|
|
40
219
|
"""
|
|
220
|
+
command_options = command_options or {} # Ensure command_options is a dict
|
|
41
221
|
|
|
42
222
|
if not input_file_paths:
|
|
43
223
|
raise ValueError("No input files provided")
|
|
44
224
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
output_file_paths: Dict[str, str] = {}
|
|
48
|
-
|
|
49
|
-
def extract_basename(filename: str) -> str:
|
|
50
|
-
"""
|
|
51
|
-
Extract the 'basename' from the filename, removing any recognized language
|
|
52
|
-
suffix (e.g., "_python") or a "_prompt" suffix if present.
|
|
53
|
-
"""
|
|
54
|
-
name = Path(filename).stem # e.g. "regression_bash" if "regression_bash.prompt"
|
|
55
|
-
parts = name.split('_')
|
|
56
|
-
last_token = parts[-1].lower()
|
|
57
|
-
if last_token in EXTENDED_LANGUAGES:
|
|
58
|
-
name = '_'.join(parts[:-1])
|
|
59
|
-
return name
|
|
60
|
-
|
|
61
|
-
def determine_language(filename: str,
|
|
62
|
-
cmd_options: Dict[str, str],
|
|
63
|
-
code_file: Optional[str] = None) -> str:
|
|
64
|
-
"""
|
|
65
|
-
Figure out the language:
|
|
66
|
-
1) If command_options['language'] is given, return it.
|
|
67
|
-
2) Check if the file's stem ends with a known language suffix (e.g. "_python").
|
|
68
|
-
3) Otherwise, check the file extension or code_file extension.
|
|
69
|
-
4) If none recognized, raise an error.
|
|
70
|
-
"""
|
|
71
|
-
# 1) If user explicitly gave a language in command_options
|
|
72
|
-
if cmd_options.get('language'):
|
|
73
|
-
return cmd_options['language']
|
|
74
|
-
|
|
75
|
-
# 2) Extract last token from the stem
|
|
76
|
-
name = Path(filename).stem
|
|
77
|
-
parts = name.split('_')
|
|
78
|
-
last_token = parts[-1].lower()
|
|
79
|
-
|
|
80
|
-
# If the last token is a known language (e.g. "python", "java") or "prompt",
|
|
81
|
-
# that is the language. E.g. "my_project_python.prompt" => python
|
|
82
|
-
# "main_gen_prompt.prompt" => prompt
|
|
83
|
-
if last_token in KNOWN_LANGUAGES:
|
|
84
|
-
return last_token
|
|
85
|
-
elif last_token == "prompt":
|
|
86
|
-
return "prompt"
|
|
87
|
-
|
|
88
|
-
# 3) If extension is .prompt, see if code_file helps or if get_language(".prompt") is mocked
|
|
89
|
-
ext = Path(filename).suffix.lower()
|
|
90
|
-
|
|
91
|
-
# If it’s explicitly ".prompt" but there's no recognized suffix,
|
|
92
|
-
# many tests rely on us calling get_language(".prompt") or checking code_file
|
|
93
|
-
if ext == ".prompt":
|
|
94
|
-
# Maybe the test mocks this to return "python", or we can check code_file:
|
|
95
|
-
if code_file:
|
|
96
|
-
code_ext = Path(code_file).suffix.lower()
|
|
97
|
-
code_lang = get_language(code_ext)
|
|
98
|
-
if code_lang:
|
|
99
|
-
return code_lang
|
|
100
|
-
|
|
101
|
-
# Attempt to see if the test or environment forcibly sets a language for ".prompt"
|
|
102
|
-
possibly_mocked = get_language(".prompt")
|
|
103
|
-
if possibly_mocked:
|
|
104
|
-
return possibly_mocked
|
|
105
|
-
|
|
106
|
-
# If not recognized, treat it as an ambiguous prompt
|
|
107
|
-
# The older tests typically don't raise an error here; they rely on mocking
|
|
108
|
-
# or a code_file. However, if there's absolutely no mock or code file, it is
|
|
109
|
-
# "Could not determine...". That's exactly what some tests check for.
|
|
110
|
-
raise ValueError("Could not determine language from command options, filename, or code file extension")
|
|
111
|
-
|
|
112
|
-
# If extension is .unsupported, raise an error
|
|
113
|
-
if ext == ".unsupported":
|
|
114
|
-
raise ValueError("Unsupported file extension for language: .unsupported")
|
|
115
|
-
|
|
116
|
-
# Otherwise, see if extension is recognized
|
|
117
|
-
lang = get_language(ext)
|
|
118
|
-
if lang:
|
|
119
|
-
return lang
|
|
120
|
-
|
|
121
|
-
# If we still cannot figure out the language, try code_file
|
|
122
|
-
if code_file:
|
|
123
|
-
code_ext = Path(code_file).suffix.lower()
|
|
124
|
-
code_lang = get_language(code_ext)
|
|
125
|
-
if code_lang:
|
|
126
|
-
return code_lang
|
|
127
|
-
|
|
128
|
-
# Otherwise, unknown language
|
|
129
|
-
raise ValueError("Could not determine language from command options, filename, or code file extension")
|
|
130
|
-
|
|
131
|
-
# -----------------
|
|
132
|
-
# Step 1: Load input files
|
|
133
|
-
# -----------------
|
|
225
|
+
# ------------- normalise & resolve Paths -----------------
|
|
226
|
+
input_paths: Dict[str, Path] = {}
|
|
134
227
|
for key, path_str in input_file_paths.items():
|
|
135
|
-
|
|
228
|
+
try:
|
|
229
|
+
path = Path(path_str).expanduser()
|
|
230
|
+
# Resolve non-error files strictly first
|
|
231
|
+
if key != "error_file":
|
|
232
|
+
# Let FileNotFoundError propagate naturally if path doesn't exist
|
|
233
|
+
resolved_path = path.resolve(strict=True)
|
|
234
|
+
input_paths[key] = resolved_path
|
|
235
|
+
else:
|
|
236
|
+
# Resolve error file non-strictly, existence checked later
|
|
237
|
+
input_paths[key] = path.resolve()
|
|
238
|
+
except FileNotFoundError as e:
|
|
239
|
+
# Re-raise standard FileNotFoundError, tests will check path within it
|
|
240
|
+
raise e
|
|
241
|
+
except Exception as exc: # Catch other potential path errors like permission issues
|
|
242
|
+
console.print(f"[error]Invalid path provided for {key}: '{path_str}' - {exc}", style="error")
|
|
243
|
+
raise # Re-raise other exceptions
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ------------- Step 1: load input files ------------------
|
|
247
|
+
input_strings: Dict[str, str] = {}
|
|
248
|
+
for key, path in input_paths.items():
|
|
249
|
+
if key == "error_file":
|
|
250
|
+
if create_error_file:
|
|
251
|
+
_ensure_error_file(path, quiet) # Pass quiet flag
|
|
252
|
+
# Ensure path exists before trying to read
|
|
253
|
+
if not path.exists():
|
|
254
|
+
# _ensure_error_file should have created it, but check again
|
|
255
|
+
# If it still doesn't exist, something went wrong
|
|
256
|
+
raise FileNotFoundError(f"Error file '{path}' could not be created or found.")
|
|
257
|
+
else:
|
|
258
|
+
# When create_error_file is False, error out if the file doesn't exist
|
|
259
|
+
if not path.exists():
|
|
260
|
+
raise FileNotFoundError(f"Error file '{path}' does not exist.")
|
|
261
|
+
|
|
262
|
+
# Check existence again, especially for error_file which might have been created
|
|
136
263
|
if not path.exists():
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
264
|
+
# This case should ideally be caught by resolve(strict=True) earlier for non-error files
|
|
265
|
+
# Raise standard FileNotFoundError
|
|
266
|
+
raise FileNotFoundError(f"{path}")
|
|
267
|
+
|
|
268
|
+
if path.is_file(): # Read only if it's a file
|
|
269
|
+
try:
|
|
270
|
+
input_strings[key] = _read_file(path)
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
# Re-raise exceptions during reading
|
|
273
|
+
raise IOError(f"Failed to read input file '{path}' (key='{key}'): {exc}") from exc
|
|
274
|
+
elif path.is_dir():
|
|
275
|
+
# Decide how to handle directories if they are passed unexpectedly
|
|
276
|
+
if not quiet:
|
|
277
|
+
console.print(f"[warning]Warning: Input path '{path}' for key '{key}' is a directory, not reading content.", style="warning")
|
|
278
|
+
# Store the path string or skip? Let's skip for input_strings.
|
|
279
|
+
# input_strings[key] = "" # Or None? Or skip? Skipping seems best.
|
|
280
|
+
# Handle other path types? (symlinks are resolved by resolve())
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ------------- Step 2: basename --------------------------
|
|
284
|
+
try:
|
|
285
|
+
basename = _extract_basename(command, input_paths)
|
|
286
|
+
except ValueError as exc:
|
|
287
|
+
# Check if it's the specific error from the initial check (now done at start)
|
|
288
|
+
# This try/except might not be needed if initial check is robust
|
|
289
|
+
# Let's keep it simple for now and let initial check handle empty inputs
|
|
290
|
+
console.print(f"[error]Unable to extract basename: {exc}", style="error")
|
|
291
|
+
raise ValueError(f"Failed to determine basename: {exc}") from exc
|
|
292
|
+
except Exception as exc: # Catch other exceptions like potential StopIteration
|
|
293
|
+
console.print(f"[error]Unexpected error during basename extraction: {exc}", style="error")
|
|
294
|
+
raise ValueError(f"Failed to determine basename: {exc}") from exc
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ------------- Step 3: language & extension --------------
|
|
298
|
+
try:
|
|
299
|
+
# Pass the potentially updated command_options
|
|
300
|
+
language = _determine_language(command_options, input_paths, command)
|
|
301
|
+
|
|
302
|
+
# Add validation to ensure language is never None
|
|
303
|
+
if language is None:
|
|
304
|
+
# Set a default language based on command, defaulting to 'python' for most commands
|
|
305
|
+
if command == 'bug':
|
|
306
|
+
# The bug command typically defaults to python in bug_main.py
|
|
307
|
+
language = 'python'
|
|
142
308
|
else:
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
"change": "input_prompt_file" if "input_prompt_file" in input_file_paths else "change_prompt_file",
|
|
174
|
-
"detect": "change_file",
|
|
175
|
-
"conflicts": "prompt1",
|
|
309
|
+
# General fallback for other commands
|
|
310
|
+
language = 'python'
|
|
311
|
+
|
|
312
|
+
# Log the issue for debugging
|
|
313
|
+
if not quiet:
|
|
314
|
+
console.print(
|
|
315
|
+
f"[warning]Warning: Could not determine language for '{command}' command. Using default: {language}[/warning]",
|
|
316
|
+
style="warning"
|
|
317
|
+
)
|
|
318
|
+
except ValueError as e:
|
|
319
|
+
console.print(f"[error]{e}", style="error")
|
|
320
|
+
raise # Re-raise the ValueError from _determine_language
|
|
321
|
+
|
|
322
|
+
# Final safety check before calling get_extension
|
|
323
|
+
if not language or not isinstance(language, str):
|
|
324
|
+
language = 'python' # Absolute fallback
|
|
325
|
+
if not quiet:
|
|
326
|
+
console.print(
|
|
327
|
+
f"[warning]Warning: Invalid language value. Using default: {language}[/warning]",
|
|
328
|
+
style="warning"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
file_extension = get_extension(language) # Pass determined language
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ------------- Step 3b: build output paths ---------------
|
|
335
|
+
# Filter user‑provided output_* locations from CLI options
|
|
336
|
+
output_location_opts = {
|
|
337
|
+
k: v for k, v in command_options.items()
|
|
338
|
+
if k.startswith("output") and v is not None # Ensure value is not None
|
|
176
339
|
}
|
|
177
340
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# If not force, confirm overwriting
|
|
229
|
-
if not force:
|
|
230
|
-
for _, out_path_str in output_file_paths.items():
|
|
231
|
-
out_path = Path(out_path_str)
|
|
232
|
-
if out_path.exists():
|
|
233
|
-
if not Confirm.ask(
|
|
234
|
-
f"Output file [bold blue]{out_path}[/bold blue] already exists. Overwrite?",
|
|
235
|
-
default=True
|
|
236
|
-
):
|
|
237
|
-
rich_print("[bold red]Cancelled by user. Exiting.[/bold red]")
|
|
238
|
-
raise SystemExit(1)
|
|
239
|
-
|
|
240
|
-
# -----------------
|
|
241
|
-
# Step 6: Print details if not quiet
|
|
242
|
-
# -----------------
|
|
341
|
+
try:
|
|
342
|
+
# generate_output_paths might return Dict[str, str] or Dict[str, Path]
|
|
343
|
+
# Let's assume it returns Dict[str, str] based on verification error,
|
|
344
|
+
# and convert them to Path objects here.
|
|
345
|
+
output_paths_str: Dict[str, str] = generate_output_paths(
|
|
346
|
+
command=command,
|
|
347
|
+
output_locations=output_location_opts,
|
|
348
|
+
basename=basename,
|
|
349
|
+
language=language,
|
|
350
|
+
file_extension=file_extension,
|
|
351
|
+
)
|
|
352
|
+
# Convert to Path objects for internal use
|
|
353
|
+
output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
|
|
354
|
+
|
|
355
|
+
except ValueError as e: # Catch ValueError if generate_output_paths raises it
|
|
356
|
+
console.print(f"[error]Error generating output paths: {e}", style="error")
|
|
357
|
+
raise # Re-raise the ValueError
|
|
358
|
+
|
|
359
|
+
# ------------- Step 4: overwrite confirmation ------------
|
|
360
|
+
# Check if any output *file* exists (operate on Path objects)
|
|
361
|
+
existing_files: Dict[str, Path] = {}
|
|
362
|
+
for k, p_obj in output_paths_resolved.items():
|
|
363
|
+
# p_obj = Path(p_val) # Conversion now happens earlier
|
|
364
|
+
if p_obj.is_file():
|
|
365
|
+
existing_files[k] = p_obj # Store the Path object
|
|
366
|
+
|
|
367
|
+
if existing_files and not force:
|
|
368
|
+
if not quiet:
|
|
369
|
+
# Use the Path objects stored in existing_files for resolve()
|
|
370
|
+
# Print without Rich tags for easier testing
|
|
371
|
+
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
372
|
+
console.print(
|
|
373
|
+
f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
|
|
374
|
+
style="warning"
|
|
375
|
+
)
|
|
376
|
+
# Use click.confirm for user interaction
|
|
377
|
+
try:
|
|
378
|
+
if not click.confirm(
|
|
379
|
+
click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
|
|
380
|
+
):
|
|
381
|
+
click.secho("Operation cancelled.", fg="red", err=True)
|
|
382
|
+
sys.exit(1) # Exit if user chooses not to overwrite
|
|
383
|
+
except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
|
|
384
|
+
click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ------------- Final reporting ---------------------------
|
|
243
389
|
if not quiet:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
390
|
+
console.print("[info]Input files:[/info]")
|
|
391
|
+
# Print resolved input paths
|
|
392
|
+
for k, p in input_paths.items():
|
|
393
|
+
console.print(f" [info]{k:<15}[/info] {p.resolve()}") # Use resolve() for consistent absolute paths
|
|
394
|
+
console.print("[info]Output files:[/info]")
|
|
395
|
+
# Print resolved output paths (using the Path objects)
|
|
396
|
+
for k, p in output_paths_resolved.items():
|
|
397
|
+
console.print(f" [info]{k:<15}[/info] {p.resolve()}") # Use resolve()
|
|
398
|
+
console.print(f"[info]Detected language:[/info] {language}")
|
|
399
|
+
console.print(f"[info]Basename:[/info] {basename}")
|
|
400
|
+
|
|
401
|
+
# Return output paths as strings, using the original dict from generate_output_paths
|
|
402
|
+
# if it returned strings, or convert the Path dict back.
|
|
403
|
+
# Since we converted to Path, convert back now.
|
|
404
|
+
output_file_paths_str_return = {k: str(v) for k, v in output_paths_resolved.items()}
|
|
405
|
+
|
|
406
|
+
return input_strings, output_file_paths_str_return, language
|