Skip to content

Commit 0b048a9

Browse files
committed
new validate command tests added; validate feature fixes and updates
1 parent ee50ab7 commit 0b048a9

16 files changed

Lines changed: 211 additions & 22 deletions

prich/cli/validate.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import ValidationError as PydanticValidationError
77
from prich.constants import PRICH_DIR_NAME
88
from prich.models.template import CommandStep, PythonStep
9-
from prich.core.file_scope import classify_path
9+
from prich.core.file_scope import classify_path, normalize_path
1010
from prich.core.loaders import find_template_files, load_template_model, get_env_vars, _load_yaml
1111
from prich.core.utils import console_print, shorten_path, get_prich_dir, is_just_filename, get_cwd_dir, get_home_dir
1212

@@ -21,14 +21,16 @@ def template_model_doctor(template_yaml: dict, model_load_error: PydanticValidat
2121
if template_yaml and err.get("loc"):
2222
# hide extra layering
2323
if err.get("loc")[0] == "steps":
24-
if err.get("loc")[2] in ['llm', 'command', 'python', 'render']:
24+
if len(err.get("loc")) >= 3 and err.get("loc")[2] in ['llm', 'command', 'python', 'render']:
2525
details = err.get("loc")[2]
2626
err['loc'] = tuple(err.get("loc")[:2] + err.get("loc")[3:])
2727

2828
trace_dir = template_yaml.copy()
2929
loc_list = err.get("loc")[:-1] if len(err.get("loc")) > 1 else err.get("loc")
3030
for x in loc_list:
3131
try:
32+
if trace_dir[x] is None:
33+
break
3234
trace_dir = trace_dir[x]
3335
except Exception:
3436
break
@@ -45,8 +47,10 @@ def template_model_doctor(template_yaml: dict, model_load_error: PydanticValidat
4547
template_overview = re.sub("(\\.\\.\\.)", f"[yellow]+{err.get('loc')[-1]}: ...[/yellow]\n\\1",
4648
template_overview, count=1)
4749
else:
50+
if 'Input tag ' in err.get('msg'):
51+
err["msg"] = err['msg'].replace("Input tag ", "Field value ").replace(" any of the expected tags", " any of the expected values")
4852
if 'Input should be' in err.get('msg'):
49-
err["msg"] = err['msg'].replace("Input should be", "Field value should be")
53+
err["msg"] = err['msg'].replace("Input should be ", "Field value should be ")
5054
highligh_block = err.get('loc')[-1] if isinstance(err.get('loc')[-1], str) else err.get('loc')[-2]
5155
template_overview = re.sub(f"([\n]*(?:\\s+)|^)({highligh_block})(:)", "\\1[red]\\2[/red]\\3",
5256
template_overview, count=1)
@@ -92,7 +96,7 @@ def template_model_doctor(template_yaml: dict, model_load_error: PydanticValidat
9296
else:
9397
doc = "See Template Content Documentation https://oleks-dev.github.io/prich/reference/template/content/"
9498
err_loc_string = re.sub(f"({err.get('loc')[-1]})$", "[red]\\1[/red]", err_loc_string)
95-
found_issues_list.append(f"""{len(found_issues_list)+1}. [red]{err.get('msg')}[/red] '[white]{err_loc_string}[/white]':\n[white]{template_overview}[/white]{doc}""")
99+
found_issues_list.append(f"""{len(found_issues_list)+1}. [red]{err.get('msg')}[/red] at '[white]{err_loc_string}[/white]':\n[white]{template_overview}[/white]{doc}""")
96100
return found_issues_list
97101

98102

@@ -111,7 +115,10 @@ def validate_templates(template_id: str, validate_file: Path, global_only: bool,
111115
if validate_file and (global_only or local_only or template_id):
112116
raise click.ClickException(f"When YAML file is selected it doesn't combine with local, global, or id options, use: 'prich validate --file ./{PRICH_DIR_NAME}/templates/test-template/test-template.yaml'")
113117

114-
if validate_file and not validate_file.exists():
118+
if validate_file:
119+
validate_file = normalize_path(validate_file, cwd=get_cwd_dir())
120+
121+
if validate_file and (not validate_file.exists() or not validate_file.is_file()):
115122
raise click.ClickException(f"Failed to find {validate_file} template file.")
116123

117124
# Load Template Files
@@ -154,10 +161,9 @@ def validate_templates(template_id: str, validate_file: Path, global_only: bool,
154161
model_failures_count = 0
155162
output = []
156163
try:
157-
if template_file.is_file():
158-
template_yaml = _load_yaml(template_file)
159-
template_id = template_yaml.get("id") if template_yaml else None
160-
template_name = template_yaml.get("name") if template_yaml else None
164+
template_yaml = _load_yaml(template_file)
165+
template_id = template_yaml.get("id") if template_yaml else None
166+
template_name = template_yaml.get("name") if template_yaml else None
161167
try:
162168
template = load_template_model(template_file)
163169
except PydanticValidationError as e:
@@ -169,8 +175,18 @@ def validate_templates(template_id: str, validate_file: Path, global_only: bool,
169175
raise click.ClickException(f"1. [red]{str(e)}[/red]")
170176
if template.venv in ["isolated", "shared"]:
171177
venv_folder = (Path(template.folder) / "scripts") if template.venv == "isolated" else get_prich_dir() / "venv"
178+
python_steps = [step for step in template.steps if step.type == 'python']
179+
if not python_steps:
180+
extra_note = ". There are no steps with type 'python' found, if python is not used you can remove the 'venv' parameter from the template"
181+
else:
182+
extra_note = ""
183+
if template.venv == 'isolated':
184+
installation_note = f" Install it by running 'prich venv-install {template.id}'."
185+
else:
186+
# TODO: introduce help for shared venv installation
187+
installation_note = ""
172188
if not venv_folder.exists():
173-
failures_list.append(f"{len(failures_list)+1}. [red]Failed to find {template.venv} venv at {shorten_path(str(venv_folder))}.[/red] Install it by running 'prich venv-install {template.id}'.")
189+
failures_list.append(f"{len(failures_list)+1}. [red]Failed to find {template.venv} venv at {shorten_path(str(venv_folder))}{extra_note}.[/red]{installation_note}")
174190
idx = 0
175191
for step in template.steps:
176192
idx += 1
@@ -196,7 +212,7 @@ def validate_templates(template_id: str, validate_file: Path, global_only: bool,
196212
output.append(f"- {template.id} [dim]({template.source.value}) {shorten_path(str(template_file))}[/dim]: ")
197213
if len(failures_list) > 0:
198214
failures_found = True
199-
output[-1] += f"[red]is not valid[/red] ({len(failures_list)} issues)"
215+
output[-1] += f"[red]is not valid[/red] ({len(failures_list)} issue{'s' if len(failures_list)>1 else ''})"
200216
failures = ' ' + '\n '.join(failures_list)
201217
output.append(failures)
202218
output.append("")
@@ -206,11 +222,11 @@ def validate_templates(template_id: str, validate_file: Path, global_only: bool,
206222
failures_found = True
207223
template_source = classify_path(template_file)
208224
error_lines = ' ' + '\n '.join([x for x in e.message.split('\n')]) + '\n'
209-
output.append(f"""- {f"{template_id} " if template_id else ''}[dim]({template_source.value}) {shorten_path(str(template_file))}[/dim]: [red]is not valid[/red] {f'({model_failures_count} issues)' if model_failures_count > 0 else '(1 issue)'}\n [red]Failed to load template{f" {template_id}" if template_id else ""}{f" ({template_name})" if template_name else ""}[/red]:\n{error_lines}""")
225+
output.append(f"""- {f"{template_id} " if template_id else ''}[dim]({template_source.value}) {shorten_path(str(template_file))}[/dim]: [red]is not valid[/red] {f'({model_failures_count} issue{"s" if model_failures_count > 1 else ""})' if model_failures_count > 0 else '(1 issue)'}\n [red]Failed to load template{f" {template_id}" if template_id else ""}{f" ({template_name})" if template_name else ""}[/red]:\n{error_lines}""")
210226
except Exception as e:
211227
failures_found = True
212228
template_source = classify_path(template_file)
213-
output.append(f"""- {f"{template_id} " if template_id else ''}[dim]({template_source.value}) {shorten_path(str(template_file))}[/dim]: [red]is not valid[/red] (1 issue)\n [red]Failed to load template{f" {template_id}" if template_id else ""}{f" ({template_name})" if template_name else ""}[/red]:\n {str(e)}""")
229+
output.append(f"""- {f"{template_id} " if template_id else ''}[dim]({template_source.value}) {shorten_path(str(template_file))}[/dim]: [red]is not valid[/red] (1 issue)\n [red]Failed to load template{f" {template_id}" if template_id else ""}{f" ({template_name})" if template_name else ""}[/red]:\n 1. {str(e)}""")
214230
if (invalid_only and failures_found) or not invalid_only:
215231
console_print('\n'.join(output))
216232
if failures_found:

prich/core/file_scope.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from prich.models.file_scope import FileScope
77

88

9-
def _normalize(p: Path, *, cwd: Path) -> Path:
9+
def normalize_path(p: Path, *, cwd: Path) -> Path:
1010
"""
1111
Expand ~, make absolute relative to cwd, and resolve as much as possible.
1212
Works for non-existent paths too (resolves the existing parent).
@@ -66,9 +66,9 @@ def classify_path(
6666
global_root = home / PRICH_DIR_NAME
6767

6868
if follow_symlinks:
69-
p = _normalize(file, cwd=cwd)
70-
lr = _normalize(local_root, cwd=cwd)
71-
gr = _normalize(global_root, cwd=cwd)
69+
p = normalize_path(file, cwd=cwd)
70+
lr = normalize_path(local_root, cwd=cwd)
71+
gr = normalize_path(global_root, cwd=cwd)
7272
else:
7373
# Don't resolve symlinks; still expand and absolutize
7474
p = (cwd / Path(file).expanduser()) if not Path(file).expanduser().is_absolute() else Path(file).expanduser()

prich/core/loaders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
def _load_yaml(path: Path) -> Dict:
1717
import yaml
18-
if not path.exists():
18+
if not path.exists() or not path.is_file():
1919
return {}
2020
with path.open("r", encoding="utf-8") as f:
2121
return yaml.safe_load(f) or {}

tests/resources/wrong_templates/empty.yaml

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Test Template
2+
version: '1.0'
3+
description: Example template
4+
tags:
5+
- example
6+
- writer
7+
steps:
8+
- name: Ask to generate text
9+
type: llm
10+
input: Generate short phrase
11+
schema_version: '1.0'
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
id: test-template
2+
version: '1.0'
3+
description: Example template
4+
steps:
5+
- name: Ask to generate text
6+
type: llm
7+
input: Generate short phrase
8+
schema_version: '1.0'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id: "test"
2+
name: "test"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
id: test-template
2+
name: Test Template
3+
version: '1.0'
4+
description: Example template - Generate text about specified topic
5+
venv: "shared"
6+
steps:
7+
- name: Ask to generate text
8+
type: llm
9+
instructions: You are {{ role }}
10+
input: Generate text about {{ topic }}
11+
schema_version: '1.0'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
id: test-template
2+
name: test template
3+
version: '1.0'
4+
description: Example template
5+
steps:
6+
schema_version: '1.0'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
id: test-template
2+
name: Test Template
3+
version: '1.0'
4+
description: Example template - Generate text about specified topic
5+
venv: "isolated"
6+
steps:
7+
- name: Ask to generate text
8+
type: llm
9+
instructions: You are {{ role }}
10+
input: Generate text about {{ topic }}
11+
schema_version: '1.0'

0 commit comments

Comments
 (0)