pdd-cli 0.0.45__py3-none-any.whl → 0.0.47__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """PDD - Prompt Driven Development"""
2
2
 
3
- __version__ = "0.0.45"
3
+ __version__ = "0.0.47"
4
4
 
5
5
  # Strength parameter used for LLM extraction across the codebase
6
6
  # Used in postprocessing, XML tagging, code generation, and other extraction
pdd/cli.py CHANGED
@@ -34,6 +34,7 @@ from .fix_main import fix_main
34
34
  from .fix_verification_main import fix_verification_main
35
35
  from .install_completion import install_completion, get_local_pdd_path
36
36
  from .preprocess_main import preprocess_main
37
+ from .pytest_output import run_pytest_and_capture_output
37
38
  from .split_main import split_main
38
39
  from .sync_main import sync_main
39
40
  from .trace_main import trace_main
@@ -1176,6 +1177,47 @@ def sync(
1176
1177
  return None
1177
1178
 
1178
1179
 
1180
+ @cli.command("pytest-output")
1181
+ @click.argument("test_file", type=click.Path(exists=True, dir_okay=False))
1182
+ @click.option(
1183
+ "--json-only",
1184
+ is_flag=True,
1185
+ default=False,
1186
+ help="Output only JSON to stdout for programmatic use.",
1187
+ )
1188
+ @click.pass_context
1189
+ # No @track_cost since this is a utility command
1190
+ def pytest_output_cmd(ctx: click.Context, test_file: str, json_only: bool) -> None:
1191
+ """Run pytest on a test file and capture structured output.
1192
+
1193
+ This is a utility command used internally by PDD for capturing pytest results
1194
+ in a structured format. It can also be used directly for debugging test issues.
1195
+
1196
+ Examples:
1197
+ pdd pytest-output tests/test_example.py
1198
+ pdd pytest-output tests/test_example.py --json-only
1199
+ """
1200
+ command_name = "pytest-output"
1201
+ quiet_mode = ctx.obj.get("quiet", False)
1202
+
1203
+ try:
1204
+ import json
1205
+ pytest_output = run_pytest_and_capture_output(test_file)
1206
+
1207
+ if json_only:
1208
+ # Print only valid JSON to stdout for programmatic use
1209
+ print(json.dumps(pytest_output))
1210
+ else:
1211
+ # Pretty print the output for interactive use
1212
+ if not quiet_mode:
1213
+ console.print(f"Running pytest on: [blue]{test_file}[/blue]")
1214
+ from rich.pretty import pprint
1215
+ pprint(pytest_output, console=console)
1216
+
1217
+ except Exception as e:
1218
+ handle_error(e, command_name, quiet_mode)
1219
+
1220
+
1179
1221
  @cli.command("install_completion")
1180
1222
  @click.pass_context
1181
1223
  # No @track_cost
pdd/cmd_test_main.py CHANGED
@@ -3,6 +3,7 @@ Main entry point for the 'test' command.
3
3
  """
4
4
  from __future__ import annotations
5
5
  import click
6
+ from pathlib import Path
6
7
  # pylint: disable=redefined-builtin
7
8
  from rich import print
8
9
 
@@ -165,6 +166,10 @@ def cmd_test_main(
165
166
  return "", 0.0, ""
166
167
 
167
168
  try:
169
+ # Ensure parent directory exists
170
+ output_path = Path(output_file)
171
+ output_path.parent.mkdir(parents=True, exist_ok=True)
172
+
168
173
  with open(output_file, "w", encoding="utf-8") as file_handle:
169
174
  file_handle.write(unit_test)
170
175
  print(f"[bold green]Unit tests saved to:[/bold green] {output_file}")
pdd/construct_paths.py CHANGED
@@ -497,11 +497,15 @@ def construct_paths(
497
497
  for key, path_str in input_file_paths.items():
498
498
  try:
499
499
  path = Path(path_str).expanduser()
500
- # Resolve non-error files strictly first
500
+ # Resolve non-error files strictly first, but be more lenient for sync command
501
501
  if key != "error_file":
502
- # Let FileNotFoundError propagate naturally if path doesn't exist
503
- resolved_path = path.resolve(strict=True)
504
- input_paths[key] = resolved_path
502
+ # For sync command, be more tolerant of non-existent files since we're just determining paths
503
+ if command == "sync":
504
+ input_paths[key] = path.resolve()
505
+ else:
506
+ # Let FileNotFoundError propagate naturally if path doesn't exist
507
+ resolved_path = path.resolve(strict=True)
508
+ input_paths[key] = resolved_path
505
509
  else:
506
510
  # Resolve error file non-strictly, existence checked later
507
511
  input_paths[key] = path.resolve()
@@ -531,9 +535,14 @@ def construct_paths(
531
535
 
532
536
  # Check existence again, especially for error_file which might have been created
533
537
  if not path.exists():
534
- # This case should ideally be caught by resolve(strict=True) earlier for non-error files
535
- # Raise standard FileNotFoundError
536
- raise FileNotFoundError(f"{path}")
538
+ # For sync command, be more tolerant of non-existent files since we're just determining paths
539
+ if command == "sync":
540
+ # Skip reading content for non-existent files in sync mode
541
+ continue
542
+ else:
543
+ # This case should ideally be caught by resolve(strict=True) earlier for non-error files
544
+ # Raise standard FileNotFoundError
545
+ raise FileNotFoundError(f"{path}")
537
546
 
538
547
  if path.is_file(): # Read only if it's a file
539
548
  try:
pdd/fix_error_loop.py CHANGED
@@ -26,15 +26,46 @@ def run_pytest_on_file(test_file: str) -> tuple[int, int, int, str]:
26
26
  Returns a tuple: (failures, errors, warnings, logs)
27
27
  """
28
28
  try:
29
- # Include "--json-only" to ensure only valid JSON is printed.
30
- # Use environment-aware Python executable for pytest execution
31
- python_executable = detect_host_python_executable()
32
- cmd = [python_executable, "-m", "pdd.pytest_output", "--json-only", test_file]
29
+ # Try using the pdd pytest-output command first (works with uv tool installs)
30
+ cmd = ["pdd", "pytest-output", "--json-only", test_file]
33
31
  result = subprocess.run(cmd, capture_output=True, text=True)
34
32
 
33
+ # If pdd command failed, try fallback approaches
34
+ if result.returncode != 0 and ("command not found" in result.stderr.lower() or "not found" in result.stderr.lower()):
35
+ # Fallback 1: Try direct function call (fastest for development)
36
+ try:
37
+ from .pytest_output import run_pytest_and_capture_output
38
+ pytest_output = run_pytest_and_capture_output(test_file)
39
+ result_stdout = json.dumps(pytest_output)
40
+ result = type('MockResult', (), {'stdout': result_stdout, 'stderr': '', 'returncode': 0})()
41
+ except ImportError:
42
+ # Fallback 2: Try python -m approach for development installs where pdd isn't in PATH
43
+ python_executable = detect_host_python_executable()
44
+ cmd = [python_executable, "-m", "pdd.pytest_output", "--json-only", test_file]
45
+ result = subprocess.run(cmd, capture_output=True, text=True)
46
+
35
47
  # Parse the JSON output from stdout
36
48
  try:
37
- output = json.loads(result.stdout)
49
+ # Extract just the JSON part from stdout (handles CLI contamination)
50
+ stdout_clean = result.stdout
51
+ json_start = stdout_clean.find('{')
52
+ if json_start == -1:
53
+ raise json.JSONDecodeError("No JSON found in output", stdout_clean, 0)
54
+
55
+ # Find the end of the JSON object by counting braces
56
+ brace_count = 0
57
+ json_end = json_start
58
+ for i, char in enumerate(stdout_clean[json_start:], json_start):
59
+ if char == '{':
60
+ brace_count += 1
61
+ elif char == '}':
62
+ brace_count -= 1
63
+ if brace_count == 0:
64
+ json_end = i + 1
65
+ break
66
+
67
+ json_str = stdout_clean[json_start:json_end]
68
+ output = json.loads(json_str)
38
69
  test_results = output.get('test_results', [{}])[0]
39
70
 
40
71
  # Check pytest's return code first
pdd/llm_invoke.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  import pandas as pd
6
6
  import litellm
7
7
  import logging # ADDED FOR DETAILED LOGGING
8
+ import importlib.resources
8
9
 
9
10
  # --- Configure Standard Python Logging ---
10
11
  logger = logging.getLogger("pdd.llm_invoke")
@@ -190,12 +191,20 @@ ENV_PATH = PROJECT_ROOT / ".env"
190
191
  user_pdd_dir = Path.home() / ".pdd"
191
192
  user_model_csv_path = user_pdd_dir / "llm_model.csv"
192
193
 
194
+ # Check in order: user-specific, project-specific, package default
193
195
  if user_model_csv_path.is_file():
194
196
  LLM_MODEL_CSV_PATH = user_model_csv_path
195
197
  logger.info(f"Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
196
198
  else:
197
- LLM_MODEL_CSV_PATH = PROJECT_ROOT / "data" / "llm_model.csv"
198
- logger.info(f"Using project LLM model CSV: {LLM_MODEL_CSV_PATH}")
199
+ # Check project-specific location (.pdd directory)
200
+ project_model_csv_path = PROJECT_ROOT / ".pdd" / "llm_model.csv"
201
+ if project_model_csv_path.is_file():
202
+ LLM_MODEL_CSV_PATH = project_model_csv_path
203
+ logger.info(f"Using project-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
204
+ else:
205
+ # Neither exists, we'll use a marker path that _load_model_data will handle
206
+ LLM_MODEL_CSV_PATH = None
207
+ logger.info("No local LLM model CSV found, will use package default")
199
208
  # ---------------------------------
200
209
 
201
210
  # Load environment variables from .env file
@@ -356,12 +365,41 @@ litellm.success_callback = [_litellm_success_callback]
356
365
 
357
366
  # --- Helper Functions ---
358
367
 
359
- def _load_model_data(csv_path: Path) -> pd.DataFrame:
360
- """Loads and preprocesses the LLM model data from CSV."""
361
- if not csv_path.exists():
362
- raise FileNotFoundError(f"LLM model CSV not found at {csv_path}")
368
+ def _load_model_data(csv_path: Optional[Path]) -> pd.DataFrame:
369
+ """Loads and preprocesses the LLM model data from CSV.
370
+
371
+ Args:
372
+ csv_path: Path to CSV file, or None to use package default
373
+
374
+ Returns:
375
+ DataFrame with model configuration data
376
+ """
377
+ # If csv_path is provided, try to load from it
378
+ if csv_path is not None:
379
+ if not csv_path.exists():
380
+ logger.warning(f"Specified LLM model CSV not found at {csv_path}, trying package default")
381
+ csv_path = None
382
+ else:
383
+ try:
384
+ df = pd.read_csv(csv_path)
385
+ logger.debug(f"Loaded model data from {csv_path}")
386
+ # Continue with the rest of the function...
387
+ except Exception as e:
388
+ logger.warning(f"Failed to load CSV from {csv_path}: {e}, trying package default")
389
+ csv_path = None
390
+
391
+ # If csv_path is None or loading failed, use package default
392
+ if csv_path is None:
393
+ try:
394
+ # Use importlib.resources to load the packaged CSV
395
+ csv_data = importlib.resources.files('pdd').joinpath('data/llm_model.csv').read_text()
396
+ import io
397
+ df = pd.read_csv(io.StringIO(csv_data))
398
+ logger.info("Loaded model data from package default")
399
+ except Exception as e:
400
+ raise FileNotFoundError(f"Failed to load default LLM model CSV from package: {e}")
401
+
363
402
  try:
364
- df = pd.read_csv(csv_path)
365
403
  # Basic validation and type conversion
366
404
  required_cols = ['provider', 'model', 'input', 'output', 'coding_arena_elo', 'api_key', 'structured_output', 'reasoning_type']
367
405
  for col in required_cols: