Skip to content

Commit a30ddb2

Browse files
authored
Merge pull request #275 from beknobloch/prompt_path_18
feat(config): resolve prompt_path via env and .pddrc (implements #18)
2 parents 84f95ab + e804ed2 commit a30ddb2

File tree

3 files changed

+163
-30
lines changed

3 files changed

+163
-30
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,7 @@ PDD automatically detects the appropriate context based on:
25592559
3. **Fallback**: Uses `default` context if no path matches
25602560
25612561
**Available Context Settings**:
2562+
- `prompts_dir`: Directory where prompt files are located (default: "prompts")
25622563
- `generate_output_path`: Where generated code files are saved
25632564
- `test_output_path`: Where test files are saved
25642565
- `example_output_path`: Where example files are saved
@@ -2603,6 +2604,7 @@ PDD uses several environment variables to customize its behavior:
26032604
26042605
**Note**: When using `.pddrc` configuration, context-specific settings take precedence over these global environment variables.
26052606
2607+
- **`PDD_PROMPTS_DIR`**: Default directory where prompt files are located (default: "prompts").
26062608
- **`PDD_GENERATE_OUTPUT_PATH`**: Default path for the `generate` command.
26072609
- **`PDD_EXAMPLE_OUTPUT_PATH`**: Default path for the `example` command.
26082610
- **`PDD_TEST_OUTPUT_PATH`**: Default path for the unit test file.

pdd/construct_paths.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -889,31 +889,33 @@ def construct_paths(
889889
# Infer base directories from a sample output path
890890
gen_path = Path(output_paths_str.get("generate_output_path", "src"))
891891

892-
# First, check current working directory for prompt files matching the basename pattern
893-
current_dir = Path.cwd()
894-
prompt_pattern = f"{basename}_*.prompt"
895-
if list(current_dir.glob(prompt_pattern)):
896-
# Found prompt files in current working directory
897-
resolved_config["prompts_dir"] = str(current_dir)
898-
resolved_config["code_dir"] = str(current_dir)
899-
if not quiet:
900-
console.print(f"[info]Found prompt files in current directory:[/info] {current_dir}")
901-
else:
902-
# Fall back to context-aware logic
903-
# Use original_context_config to avoid checking augmented config with env vars
904-
if original_context_config and (
905-
'prompts_dir' in original_context_config or
906-
any(key.endswith('_output_path') for key in original_context_config)
907-
):
908-
# For configured contexts, use prompts_dir from config if provided,
909-
# otherwise default to "prompts" at the same level as output dirs
910-
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
911-
resolved_config["code_dir"] = str(gen_path.parent)
892+
# Only infer prompts_dir if it wasn't provided via CLI/.pddrc/env
893+
if not resolved_config.get("prompts_dir"):
894+
# First, check current working directory for prompt files matching the basename pattern
895+
current_dir = Path.cwd()
896+
prompt_pattern = f"{basename}_*.prompt"
897+
if list(current_dir.glob(prompt_pattern)):
898+
# Found prompt files in current working directory
899+
resolved_config["prompts_dir"] = str(current_dir)
900+
resolved_config["code_dir"] = str(current_dir)
901+
if not quiet:
902+
console.print(f"[info]Found prompt files in current directory:[/info] {current_dir}")
912903
else:
913-
# For default contexts, maintain relative relationship
914-
# e.g., if code goes to "pi.py", prompts should be at "prompts/" (siblings)
915-
resolved_config["prompts_dir"] = str(gen_path.parent / "prompts")
916-
resolved_config["code_dir"] = str(gen_path.parent)
904+
# Fall back to context-aware logic
905+
# Use original_context_config to avoid checking augmented config with env vars
906+
if original_context_config and (
907+
'prompts_dir' in original_context_config or
908+
any(key.endswith('_output_path') for key in original_context_config)
909+
):
910+
# For configured contexts, use prompts_dir from config if provided,
911+
# otherwise default to "prompts" at the same level as output dirs
912+
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
913+
resolved_config["code_dir"] = str(gen_path.parent)
914+
else:
915+
# For default contexts, maintain relative relationship
916+
# e.g., if code goes to "pi.py", prompts should be at "prompts/" (siblings)
917+
resolved_config["prompts_dir"] = str(gen_path.parent / "prompts")
918+
resolved_config["code_dir"] = str(gen_path.parent)
917919

918920
resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
919921

@@ -1247,9 +1249,10 @@ def construct_paths(
12471249

12481250
# Add resolved paths to the config that gets returned
12491251
resolved_config.update(output_file_paths_str_return)
1250-
# Also add inferred directory paths
1252+
# Only infer prompts_dir if it wasn't provided via CLI/.pddrc/env.
12511253
gen_path = Path(resolved_config.get("generate_output_path", "src"))
1252-
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
1254+
if not resolved_config.get("prompts_dir"):
1255+
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
12531256
resolved_config["code_dir"] = str(gen_path.parent)
12541257
resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
12551258

tests/test_construct_paths.py

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import pytest
44
import click
55
from pathlib import Path
6-
from unittest import mock
7-
from unittest.mock import patch, MagicMock, ANY
8-
import sys
6+
from unittest.mock import patch, ANY
97
import os
108

119
# Mock generate_output_paths before importing construct_paths if it's needed globally
@@ -18,6 +16,7 @@
1816
def resolve_path(relative_path_str, base_dir):
1917
return str(Path(base_dir) / relative_path_str)
2018

19+
2120
def test_construct_paths_load_input_files(tmpdir):
2221
"""
2322
Test that construct_paths properly loads input files into input_strings,
@@ -2906,4 +2905,133 @@ def test_construct_paths_sync_basename_paths_pattern_context(self, tmp_path, mon
29062905

29072906
assert resolved_config["_matched_context"] == "frontend-components"
29082907
assert resolved_config["prompts_dir"] == "prompts"
2909-
assert Path(resolved_config["code_dir"]).as_posix().endswith("frontend/src/components")
2908+
assert Path(resolved_config["code_dir"]).as_posix().endswith("frontend/src/components")
2909+
2910+
2911+
def test_resolve_config_hierarchy_env_prompts_dir(monkeypatch):
2912+
"""PDD_PROMPTS_DIR environment variable should be respected."""
2913+
monkeypatch.setenv("PDD_PROMPTS_DIR", "/tmp/custom_prompts")
2914+
2915+
resolved = _resolve_config_hierarchy(
2916+
cli_options={},
2917+
context_config={},
2918+
env_vars={
2919+
"PDD_PROMPTS_DIR": os.environ.get("PDD_PROMPTS_DIR"),
2920+
},
2921+
)
2922+
2923+
assert "prompts_dir" in resolved
2924+
assert resolved["prompts_dir"] == "/tmp/custom_prompts"
2925+
2926+
2927+
def test_resolve_config_hierarchy_pddrc_prompts_dir(monkeypatch):
2928+
"""The .pddrc key `prompts_dir` should be respected."""
2929+
monkeypatch.delenv("PDD_PROMPTS_DIR", raising=False)
2930+
2931+
context_config = {
2932+
"prompts_dir": "my_prompts",
2933+
}
2934+
2935+
resolved = _resolve_config_hierarchy(
2936+
cli_options={},
2937+
context_config=context_config,
2938+
env_vars={},
2939+
)
2940+
2941+
assert "prompts_dir" in resolved
2942+
assert resolved["prompts_dir"] == "my_prompts"
2943+
2944+
2945+
def test_resolve_config_hierarchy_cli_prompts_dir_wins(monkeypatch):
2946+
"""CLI prompts_dir should take precedence over .pddrc and env vars."""
2947+
monkeypatch.setenv("PDD_PROMPTS_DIR", "/tmp/env_prompts")
2948+
2949+
resolved = _resolve_config_hierarchy(
2950+
cli_options={
2951+
"prompts_dir": "cli_prompts",
2952+
},
2953+
context_config={
2954+
"prompts_dir": "pddrc_prompts",
2955+
},
2956+
env_vars={
2957+
"PDD_PROMPTS_DIR": os.environ.get("PDD_PROMPTS_DIR"),
2958+
},
2959+
)
2960+
2961+
assert "prompts_dir" in resolved
2962+
assert resolved["prompts_dir"] == "cli_prompts"
2963+
2964+
2965+
def test_construct_paths_regular_mode_respects_env_prompts_dir(tmp_path, monkeypatch):
2966+
"""
2967+
Integration test: PDD_PROMPTS_DIR should be respected in regular mode (e.g., pdd generate).
2968+
2969+
This verifies the environment variable works through the full construct_paths flow,
2970+
not just in _resolve_config_hierarchy isolation.
2971+
"""
2972+
monkeypatch.chdir(tmp_path)
2973+
monkeypatch.setenv("PDD_PROMPTS_DIR", "/custom/prompts")
2974+
2975+
# Create minimal test files
2976+
prompts_dir = tmp_path / "custom_prompts_location"
2977+
prompts_dir.mkdir()
2978+
prompt_file = prompts_dir / "test_python.prompt"
2979+
prompt_file.write_text("% Test prompt", encoding="utf-8")
2980+
2981+
input_file_paths = {"prompt_file": str(prompt_file)}
2982+
command_options = {"output": "test.py"}
2983+
2984+
resolved_config, _, output_paths, _ = construct_paths(
2985+
input_file_paths=input_file_paths,
2986+
force=True,
2987+
quiet=True,
2988+
command="generate",
2989+
command_options=command_options,
2990+
)
2991+
2992+
# The environment variable should be in resolved_config
2993+
assert "prompts_dir" in resolved_config
2994+
assert resolved_config["prompts_dir"] == "/custom/prompts", \
2995+
f"Expected prompts_dir='/custom/prompts' from PDD_PROMPTS_DIR, got '{resolved_config['prompts_dir']}'"
2996+
2997+
2998+
def test_construct_paths_sync_mode_respects_env_prompts_dir(tmp_path, monkeypatch):
2999+
"""
3000+
Integration test: PDD_PROMPTS_DIR should be respected in sync discovery mode.
3001+
3002+
Verifies the fix for the bug where sync mode would unconditionally overwrite
3003+
prompts_dir (lines 794, 807, 812) even when PDD_PROMPTS_DIR was set.
3004+
"""
3005+
monkeypatch.chdir(tmp_path)
3006+
monkeypatch.setenv("PDD_PROMPTS_DIR", "/custom/sync/prompts")
3007+
3008+
# Create minimal structure for sync mode
3009+
(tmp_path / "src").mkdir()
3010+
(tmp_path / "tests").mkdir()
3011+
(tmp_path / "context").mkdir()
3012+
3013+
command_options = {"basename": "calculator"}
3014+
3015+
# Mock generate_output_paths to return predictable paths
3016+
mock_output_paths = {
3017+
"generate_output_path": str(tmp_path / "src" / "calculator.py"),
3018+
"test_output_path": str(tmp_path / "tests" / "test_calculator.py"),
3019+
"example_output_path": str(tmp_path / "context" / "calculator_example.py"),
3020+
}
3021+
3022+
with patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths), \
3023+
patch('pdd.construct_paths._get_context_config', return_value={}):
3024+
3025+
resolved_config, _, _, _ = construct_paths(
3026+
input_file_paths={},
3027+
force=True,
3028+
quiet=True,
3029+
command="sync",
3030+
command_options=command_options,
3031+
)
3032+
3033+
# The environment variable should take precedence over sync discovery inference
3034+
assert "prompts_dir" in resolved_config
3035+
assert resolved_config["prompts_dir"] == "/custom/sync/prompts", \
3036+
f"Expected prompts_dir='/custom/sync/prompts' from PDD_PROMPTS_DIR in sync mode, got '{resolved_config['prompts_dir']}'"
3037+

0 commit comments

Comments
 (0)