Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions pdd/construct_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,56 +533,57 @@ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:


# New helper function to check if a language is known
def _is_known_language(language_name: str) -> bool:
"""Return True if the language is recognized.
def _get_known_languages() -> set:
"""Return the set of known language names (lowercase).

Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set
so basename/language inference does not fail when PDD_PATH is unset.
Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set.
"""
language_name_lower = (language_name or "").lower()
if not language_name_lower:
return False

builtin_languages = {
'python', 'javascript', 'typescript', 'typescriptreact', 'javascriptreact',
'java', 'cpp', 'c', 'go', 'ruby', 'rust',
'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
# Additional languages from language_format.csv
'haskell', 'dart', 'elixir', 'clojure', 'julia', 'erlang', 'fortran',
'nim', 'ocaml', 'groovy', 'coffeescript', 'fish', 'zsh',
'prisma', 'lean', 'agda',
# Frontend / templating
'lisp', 'scheme', 'ada',
'svelte', 'vue', 'scss', 'sass', 'less',
'jinja', 'handlebars', 'pug', 'ejs', 'twig',
# Modern / systems languages
'zig', 'mojo', 'solidity',
# Config / query / infra
'graphql', 'protobuf', 'terraform', 'hcl', 'nix',
'glsl', 'wgsl', 'starlark', 'dockerfile',
# Common data and config formats for architecture prompts and configs
'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
}

pdd_path_str = os.getenv('PDD_PATH')
if not pdd_path_str:
return language_name_lower in builtin_languages
return builtin_languages

csv_file_path = Path(pdd_path_str) / 'data' / 'language_format.csv'
if not csv_file_path.is_file():
return language_name_lower in builtin_languages
return builtin_languages

try:
with open(csv_file_path, mode='r', encoding='utf-8', newline='') as csvfile:
reader = csv.DictReader(csvfile)
csv_languages = set()
for row in reader:
if row.get('language', '').lower() == language_name_lower:
return True
lang = row.get('language', '').strip().lower()
if lang:
csv_languages.add(lang)
return (csv_languages | builtin_languages) if csv_languages else builtin_languages
except csv.Error as e:
console.print(f"[error]CSV Error reading {csv_file_path}: {e}", style="error")
return language_name_lower in builtin_languages
return builtin_languages


def _is_known_language(language_name: str) -> bool:
"""Return True if the language is recognized."""
language_name_lower = (language_name or "").lower()
if not language_name_lower:
return False
return language_name_lower in _get_known_languages()

return language_name_lower in builtin_languages


def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
Expand Down Expand Up @@ -758,7 +759,12 @@ def _determine_language(
if command == "detect" and "change_file" in input_file_paths:
return "prompt"

# 5 - If no language determined, raise error
# 5 - Fallback to default_language from .pddrc
default_lang = command_options.get("default_language")
if default_lang:
return default_lang.lower()

# 6 - If no language determined, raise error
raise ValueError("Could not determine language from input files or options.")


Expand Down
109 changes: 100 additions & 9 deletions tests/test_construct_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ def dynamic_get_extension(lang_candidate):
with pytest.raises(ValueError) as excinfo:
with patch('pdd.construct_paths.get_extension', side_effect=dynamic_get_extension), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str):
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
construct_paths(
input_file_paths, force, quiet, command, command_options
)
Expand Down Expand Up @@ -386,7 +387,8 @@ def mock_get_extension_func_case3(lang):
def dynamic_get_ext_case4(lang): return "" # Always return ""
with patch('pdd.construct_paths.get_extension', side_effect=dynamic_get_ext_case4), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str):
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
with pytest.raises(ValueError) as excinfo:
construct_paths(input_file_paths_4, True, True, 'generate', command_options_4)
assert "Could not determine language" in str(excinfo.value)
Expand Down Expand Up @@ -695,7 +697,8 @@ def test_construct_paths_unsupported_extension_error(tmpdir):
def dynamic_get_ext_unsupported(lang): return "" # Always return ""
with patch('pdd.construct_paths.get_extension', side_effect=dynamic_get_ext_unsupported), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths'): # Mock to prevent its errors
patch('pdd.construct_paths.generate_output_paths'), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
with pytest.raises(ValueError) as excinfo:
construct_paths(
input_file_paths, force, quiet, command, command_options
Expand Down Expand Up @@ -1752,14 +1755,15 @@ def test_construct_paths_change_command_language_detection(tmpdir):
# Test with a different command without language indicators
with patch('pdd.construct_paths.get_extension', side_effect=lambda lang: '.py' if lang == 'python' else ''), \
patch('pdd.construct_paths.get_language', side_effect=lambda ext: 'python' if ext == '.py' else ''), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str):

patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):

# The "generate" command should raise ValueError with no language indicators
with pytest.raises(ValueError) as excinfo:
_, input_strings, output_file_paths, language = construct_paths(
input_file_paths_no_lang, force, quiet, "generate", command_options
)

# The error should be about not being able to determine language
assert "Could not determine language" in str(excinfo.value)

Expand Down Expand Up @@ -1821,17 +1825,79 @@ def test_construct_paths_detect_command_language_detection(tmpdir):
# Test with a different command without language indicators
with patch('pdd.construct_paths.get_extension', side_effect=lambda lang: '.prompt' if lang == 'prompt' else '.py' if lang == 'python' else ''), \
patch('pdd.construct_paths.get_language', side_effect=lambda ext: 'python' if ext == '.py' else ''), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str):

patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths_dict_str), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):

# The "generate" command should raise ValueError with no language indicators
with pytest.raises(ValueError) as excinfo:
_, input_strings, output_file_paths, language = construct_paths(
input_file_paths_no_lang, force, quiet, "generate", command_options
)

# The error should be about not being able to determine language
assert "Could not determine language" in str(excinfo.value)


def test_construct_paths_default_language_fallback(tmpdir):
"""
Test that _determine_language falls back to default_language from .pddrc
when no other language indicator is available (Issue #451).
"""
tmp_path = Path(str(tmpdir))
prompt_file = tmp_path / 'test.prompt'
prompt_file.write_text('write a hello function')

mock_output_paths = {'output': str(tmp_path / 'output.py')}

# Case 1: default_language in command_options should be used as fallback
input_file_paths = {'prompt_file': str(prompt_file)}
command_options = {'default_language': 'python'}
with patch('pdd.construct_paths.get_extension', return_value='.py'), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
_, _, _, language = construct_paths(
input_file_paths, True, True, 'generate', command_options
)
assert language == 'python'

# Case 2: explicit --language flag overrides default_language
command_options_2 = {'language': 'typescript', 'default_language': 'python'}
with patch('pdd.construct_paths.get_extension', return_value='.ts'), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
_, _, _, language = construct_paths(
input_file_paths, True, True, 'generate', command_options_2
)
assert language == 'typescript'

# Case 3: prompt filename suffix overrides default_language
prompt_file_js = tmp_path / 'test_javascript.prompt'
prompt_file_js.write_text('write a hello function')
input_file_paths_3 = {'prompt_file': str(prompt_file_js)}
command_options_3 = {'default_language': 'python'}
with patch('pdd.construct_paths.get_extension', side_effect=lambda l: '.js' if l == 'javascript' else '.py' if l == 'python' else ''), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
_, _, _, language = construct_paths(
input_file_paths_3, True, True, 'generate', command_options_3
)
assert language == 'javascript'

# Case 4: default_language is case-insensitive
command_options_4 = {'default_language': 'Python'}
with patch('pdd.construct_paths.get_extension', return_value='.py'), \
patch('pdd.construct_paths.get_language', return_value=None), \
patch('pdd.construct_paths.generate_output_paths', return_value=mock_output_paths), \
patch('pdd.construct_paths._find_pddrc_file', return_value=None):
_, _, _, language = construct_paths(
input_file_paths, True, True, 'generate', command_options_4
)
assert language == 'python'


def test_construct_paths_bug_command_language_detection(tmpdir):
"""
Test that construct_paths correctly handles None language values for the bug command.
Expand Down Expand Up @@ -3035,3 +3101,28 @@ def test_construct_paths_sync_mode_respects_env_prompts_dir(tmp_path, monkeypatc
assert resolved_config["prompts_dir"] == "/custom/sync/prompts", \
f"Expected prompts_dir='/custom/sync/prompts' from PDD_PROMPTS_DIR in sync mode, got '{resolved_config['prompts_dir']}'"


# --- Tests for _get_known_languages ---

from pdd.construct_paths import _get_known_languages


class TestGetKnownLanguages:
"""Tests for _get_known_languages helper."""

def test_returns_set(self):
result = _get_known_languages()
assert isinstance(result, set)

def test_contains_common_languages(self):
known = _get_known_languages()
for lang in ['python', 'javascript', 'typescript', 'rust', 'go', 'java']:
assert lang in known

def test_all_lowercase(self):
known = _get_known_languages()
for lang in known:
assert lang == lang.lower()



Loading