mirror of
https://github.com/rendercv/rendercv.git
synced 2026-04-22 15:59:45 -04:00
Improve schema model builder
This commit is contained in:
@@ -186,10 +186,12 @@ def cli_command_render(
|
||||
] = None,
|
||||
extra_data_model_override_arguments: typer.Context = None, # ty: ignore[invalid-parameter-default]
|
||||
):
|
||||
input_file_path = pathlib.Path(input_file_name)
|
||||
|
||||
arguments: BuildRendercvModelArguments = {
|
||||
"design_file_path_or_contents": design if design else None,
|
||||
"locale_file_path_or_contents": locale if locale else None,
|
||||
"settings_file_path_or_contents": settings if settings else None,
|
||||
"design_yaml_file": design.read_text(encoding="utf-8") if design else None,
|
||||
"locale_yaml_file": locale.read_text(encoding="utf-8") if locale else None,
|
||||
"settings_yaml_file": settings.read_text(encoding="utf-8") if settings else None,
|
||||
"typst_path": typst_path,
|
||||
"pdf_path": pdf_path,
|
||||
"markdown_path": markdown_path,
|
||||
@@ -202,7 +204,6 @@ def cli_command_render(
|
||||
"dont_generate_png": dont_generate_png,
|
||||
"overrides": parse_override_arguments(extra_data_model_override_arguments),
|
||||
}
|
||||
input_file_path = pathlib.Path(input_file_name)
|
||||
|
||||
with ProgressPanel(quiet=quiet) as progress_panel:
|
||||
if watch:
|
||||
|
||||
@@ -15,6 +15,7 @@ from rendercv.schema.rendercv_model_builder import (
|
||||
BuildRendercvModelArguments,
|
||||
build_rendercv_dictionary_and_model,
|
||||
)
|
||||
from rendercv.schema.yaml_reader import read_yaml
|
||||
|
||||
from .progress_panel import ProgressPanel
|
||||
|
||||
@@ -72,37 +73,39 @@ def timed_step[T, **P](
|
||||
|
||||
|
||||
def run_rendercv(
|
||||
main_input_file_path_or_contents: pathlib.Path | str,
|
||||
input_file_path: pathlib.Path,
|
||||
progress: ProgressPanel,
|
||||
**kwargs: Unpack[BuildRendercvModelArguments],
|
||||
):
|
||||
"""Execute complete CV generation pipeline with progress tracking and error handling.
|
||||
|
||||
Why:
|
||||
Orchestrates the full flow: YAML → Pydantic validation → Typst generation →
|
||||
PDF/PNG/HTML/Markdown outputs. Catches all error types and displays them
|
||||
through progress panel for clean CLI experience.
|
||||
|
||||
Example:
|
||||
```py
|
||||
with ProgressPanel() as progress:
|
||||
run_rendercv(
|
||||
Path("cv.yaml"), progress, pdf_path="output.pdf", dont_generate_png=True
|
||||
)
|
||||
# Generates PDF, skips PNG, shows progress for each step
|
||||
```
|
||||
|
||||
Args:
|
||||
main_input_file_path_or_contents: YAML file path or raw content string.
|
||||
input_file_path: Path to the main YAML input file.
|
||||
progress: Progress panel for output display.
|
||||
kwargs: Optional overrides for design/locale files, output paths, and generation flags.
|
||||
kwargs: Optional YAML overlay strings, output paths, and generation flags.
|
||||
"""
|
||||
try:
|
||||
main_yaml = input_file_path.read_text(encoding="utf-8")
|
||||
|
||||
# Resolve design/locale file references from the YAML itself
|
||||
# (CLI flags override YAML references)
|
||||
main_dict = read_yaml(main_yaml)
|
||||
rc = main_dict.get("settings", {}).get("render_command", {})
|
||||
|
||||
if not kwargs.get("design_yaml_file") and rc.get("design"):
|
||||
design_path = (input_file_path.parent / rc["design"]).resolve()
|
||||
kwargs["design_yaml_file"] = design_path.read_text(encoding="utf-8") # ty: ignore[literal-required]
|
||||
|
||||
if not kwargs.get("locale_yaml_file") and rc.get("locale"):
|
||||
locale_path = (input_file_path.parent / rc["locale"]).resolve()
|
||||
kwargs["locale_yaml_file"] = locale_path.read_text(encoding="utf-8") # ty: ignore[literal-required]
|
||||
|
||||
_, rendercv_model = timed_step(
|
||||
"Validated the input file",
|
||||
progress,
|
||||
build_rendercv_dictionary_and_model,
|
||||
main_input_file_path_or_contents,
|
||||
main_yaml,
|
||||
input_file_path=input_file_path,
|
||||
**kwargs,
|
||||
)
|
||||
typst_path = timed_step(
|
||||
|
||||
@@ -14,9 +14,9 @@ from .yaml_reader import read_yaml
|
||||
|
||||
|
||||
class BuildRendercvModelArguments(TypedDict, total=False):
|
||||
design_file_path_or_contents: pathlib.Path | str | None
|
||||
locale_file_path_or_contents: pathlib.Path | str | None
|
||||
settings_file_path_or_contents: pathlib.Path | str | None
|
||||
design_yaml_file: str | None
|
||||
locale_yaml_file: str | None
|
||||
settings_yaml_file: str | None
|
||||
typst_path: pathlib.Path | str | None
|
||||
pdf_path: pathlib.Path | str | None
|
||||
markdown_path: pathlib.Path | str | None
|
||||
@@ -31,52 +31,31 @@ class BuildRendercvModelArguments(TypedDict, total=False):
|
||||
|
||||
|
||||
def build_rendercv_dictionary(
|
||||
main_input_file_path_or_contents: pathlib.Path | str,
|
||||
main_yaml_file: str,
|
||||
**kwargs: Unpack[BuildRendercvModelArguments],
|
||||
) -> CommentedMap:
|
||||
"""Merge main YAML with overlays and CLI overrides into final dictionary.
|
||||
|
||||
Why:
|
||||
Users need modular configuration (separate design/locale files) and
|
||||
quick testing (CLI overrides). This pipeline applies all modifications
|
||||
before validation, ensuring users see complete configuration errors.
|
||||
|
||||
Example:
|
||||
```py
|
||||
data = build_rendercv_dictionary(
|
||||
pathlib.Path("cv.yaml"),
|
||||
design_file_path_or_contents=pathlib.Path("classic.yaml"),
|
||||
overrides={"cv.phone": "+1234567890"},
|
||||
)
|
||||
# data contains merged cv + design + overrides
|
||||
```
|
||||
|
||||
Args:
|
||||
main_input_file_path_or_contents: Primary CV YAML file or string.
|
||||
kwargs: Optional YAML overlay paths, output paths, generation flags, and CLI overrides.
|
||||
main_yaml_file: Primary CV YAML content string.
|
||||
kwargs: Optional YAML overlay strings, output paths, generation flags, and CLI overrides.
|
||||
|
||||
Returns:
|
||||
Merged dictionary ready for validation.
|
||||
"""
|
||||
input_dict = read_yaml(main_input_file_path_or_contents)
|
||||
input_dict = read_yaml(main_yaml_file)
|
||||
input_dict.setdefault("settings", {}).setdefault("render_command", {})
|
||||
|
||||
# Optional YAML overlays (process settings first to avoid overwriting design/locale paths)
|
||||
yaml_overlays: dict[str, pathlib.Path | str | None] = {
|
||||
"settings": kwargs.get("settings_file_path_or_contents"),
|
||||
"design": kwargs.get("design_file_path_or_contents"),
|
||||
"locale": kwargs.get("locale_file_path_or_contents"),
|
||||
yaml_overlays: dict[str, str | None] = {
|
||||
"settings": kwargs.get("settings_yaml_file"),
|
||||
"design": kwargs.get("design_yaml_file"),
|
||||
"locale": kwargs.get("locale_yaml_file"),
|
||||
}
|
||||
|
||||
for key, path_or_contents in yaml_overlays.items():
|
||||
if path_or_contents:
|
||||
if isinstance(path_or_contents, str) or key == "settings":
|
||||
input_dict[key] = read_yaml(path_or_contents)[key]
|
||||
elif isinstance(path_or_contents, pathlib.Path):
|
||||
input_dict["settings"]["render_command"][key] = path_or_contents
|
||||
input_dict.setdefault("settings", {}).setdefault("render_command", {})
|
||||
for key, yaml_content in yaml_overlays.items():
|
||||
if yaml_content:
|
||||
input_dict[key] = read_yaml(yaml_content)[key]
|
||||
|
||||
# Optional render-command overrides
|
||||
render_overrides: dict[str, pathlib.Path | str | bool | None] = {
|
||||
"typst_path": kwargs.get("typst_path"),
|
||||
"pdf_path": kwargs.get("pdf_path"),
|
||||
@@ -107,11 +86,6 @@ def build_rendercv_model_from_commented_map(
|
||||
) -> RenderCVModel:
|
||||
"""Validate merged dictionary and build Pydantic model with error mapping.
|
||||
|
||||
Why:
|
||||
Validation transforms raw YAML into type-safe objects. When validation
|
||||
fails, CommentedMap metadata enables precise error location reporting
|
||||
instead of generic Pydantic messages.
|
||||
|
||||
Args:
|
||||
commented_map: Merged dictionary with line/column metadata.
|
||||
input_file_path: Source file path for context and photo resolution.
|
||||
@@ -127,18 +101,6 @@ def build_rendercv_model_from_commented_map(
|
||||
)
|
||||
}
|
||||
model = RenderCVModel.model_validate(commented_map, context=validation_context)
|
||||
if model.settings.render_command.design:
|
||||
design = read_yaml(model.settings.render_command.design)
|
||||
model.design = RenderCVModel.model_validate(
|
||||
design,
|
||||
context=validation_context,
|
||||
).design
|
||||
if model.settings.render_command.locale:
|
||||
locale = read_yaml(model.settings.render_command.locale)
|
||||
model.locale = RenderCVModel.model_validate(
|
||||
locale,
|
||||
context=validation_context,
|
||||
).locale
|
||||
except pydantic.ValidationError as e:
|
||||
validation_errors = parse_validation_errors(e, commented_map)
|
||||
raise RenderCVUserValidationError(validation_errors) from e
|
||||
@@ -147,36 +109,21 @@ def build_rendercv_model_from_commented_map(
|
||||
|
||||
|
||||
def build_rendercv_dictionary_and_model(
|
||||
main_input_file_path_or_contents: pathlib.Path | str,
|
||||
main_yaml_file: str,
|
||||
*,
|
||||
input_file_path: pathlib.Path | None = None,
|
||||
**kwargs: Unpack[BuildRendercvModelArguments],
|
||||
) -> tuple[CommentedMap, RenderCVModel]:
|
||||
"""Complete pipeline from raw input to validated model.
|
||||
|
||||
Why:
|
||||
Main entry point for render command combines merging and validation
|
||||
in one call. Returns both dictionary and model because error handlers
|
||||
need dictionary metadata for location mapping.
|
||||
|
||||
Example:
|
||||
```py
|
||||
data, model = build_rendercv_dictionary_and_model(
|
||||
pathlib.Path("cv.yaml"), pdf_path="output.pdf"
|
||||
)
|
||||
# model.cv.name is validated, data preserves YAML line numbers
|
||||
```
|
||||
"""Complete pipeline from raw YAML string to validated model.
|
||||
|
||||
Args:
|
||||
main_input_file_path_or_contents: Primary CV YAML file or string.
|
||||
kwargs: Optional YAML overlay paths, output paths, generation flags, and CLI overrides.
|
||||
main_yaml_file: Primary CV YAML content string.
|
||||
input_file_path: Source file path for validation context (path resolution).
|
||||
kwargs: Optional YAML overlay strings, output paths, generation flags, and CLI overrides.
|
||||
|
||||
Returns:
|
||||
Tuple of merged dictionary and validated model.
|
||||
"""
|
||||
d = build_rendercv_dictionary(main_input_file_path_or_contents, **kwargs)
|
||||
input_file_path = (
|
||||
main_input_file_path_or_contents
|
||||
if isinstance(main_input_file_path_or_contents, pathlib.Path)
|
||||
else None
|
||||
)
|
||||
d = build_rendercv_dictionary(main_yaml_file, **kwargs)
|
||||
m = build_rendercv_model_from_commented_map(d, input_file_path)
|
||||
return d, m
|
||||
|
||||
@@ -52,7 +52,10 @@ class TestCliCommandNew:
|
||||
assert input_file_path.read_text(encoding="utf-8") == "existing content"
|
||||
else:
|
||||
# Make sure it's a valid YAML file
|
||||
build_rendercv_dictionary_and_model(input_file_path)
|
||||
yaml_content = input_file_path.read_text(encoding="utf-8")
|
||||
build_rendercv_dictionary_and_model(
|
||||
yaml_content, input_file_path=input_file_path
|
||||
)
|
||||
|
||||
# Typst templates
|
||||
if not create_typst_templates:
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import date as Date
|
||||
import pytest
|
||||
import ruamel.yaml
|
||||
|
||||
from rendercv.exception import RenderCVUserError
|
||||
from rendercv.exception import RenderCVUserError, RenderCVUserValidationError
|
||||
from rendercv.schema.models.rendercv_model import RenderCVModel
|
||||
from rendercv.schema.rendercv_model_builder import (
|
||||
build_rendercv_dictionary,
|
||||
@@ -23,38 +23,23 @@ def minimal_input_dict():
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_yaml_file_fixture(tmp_path):
|
||||
"""Factory fixture to create temporary YAML files."""
|
||||
|
||||
def create_file(name: str, dictionary: dict) -> pathlib.Path:
|
||||
file_path = tmp_path / name
|
||||
yaml_content = dictionary_to_yaml(dictionary)
|
||||
file_path.write_text(yaml_content, encoding="utf-8")
|
||||
return file_path
|
||||
|
||||
return create_file
|
||||
|
||||
|
||||
class TestBuildRendercvDictionary:
|
||||
@pytest.mark.parametrize(
|
||||
"input_type",
|
||||
["string", "file"],
|
||||
)
|
||||
def test_basic_input_without_overlays(
|
||||
self, minimal_input_dict, create_yaml_file_fixture, input_type
|
||||
):
|
||||
if input_type == "string":
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
else: # file
|
||||
yaml_input = create_yaml_file_fixture("input.yaml", minimal_input_dict)
|
||||
def test_basic_input(self, minimal_input_dict):
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
|
||||
result = build_rendercv_dictionary(yaml_input)
|
||||
|
||||
assert result["cv"]["name"] == "John Doe"
|
||||
assert result["design"]["theme"] == "classic"
|
||||
|
||||
@pytest.mark.parametrize("input_type", ["string", "file"])
|
||||
def test_ensures_settings_and_render_command_exist(self, minimal_input_dict):
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
|
||||
result = build_rendercv_dictionary(yaml_input)
|
||||
|
||||
assert "settings" in result
|
||||
assert "render_command" in result["settings"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("overlay_key", "overlay_content"),
|
||||
[
|
||||
@@ -66,93 +51,89 @@ class TestBuildRendercvDictionary:
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_single_overlay(
|
||||
self,
|
||||
minimal_input_dict,
|
||||
create_yaml_file_fixture,
|
||||
overlay_key,
|
||||
overlay_content,
|
||||
input_type,
|
||||
):
|
||||
def test_single_overlay(self, minimal_input_dict, overlay_key, overlay_content):
|
||||
main_input = {
|
||||
**minimal_input_dict,
|
||||
"locale": {"language": "english"},
|
||||
"settings": {"original_setting": "original"},
|
||||
}
|
||||
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
overlay_yaml = dictionary_to_yaml(overlay_content)
|
||||
|
||||
# Create overlay as string or file
|
||||
if input_type == "string" or overlay_key == "settings":
|
||||
overlay_input = dictionary_to_yaml(overlay_content)
|
||||
else: # file
|
||||
overlay_input = create_yaml_file_fixture(
|
||||
f"{overlay_key}.yaml", overlay_content
|
||||
)
|
||||
|
||||
kwargs = {f"{overlay_key}_file_path_or_contents": overlay_input}
|
||||
kwargs = {f"{overlay_key}_yaml_file": overlay_yaml}
|
||||
result = build_rendercv_dictionary(main_yaml, **kwargs) # pyright: ignore[reportArgumentType]
|
||||
|
||||
# Behavior differs based on input type:
|
||||
# - String: merges immediately into the dictionary
|
||||
# - Path: stores reference in settings.render_command for later loading
|
||||
if input_type == "string" or overlay_key == "settings":
|
||||
assert result[overlay_key] == overlay_content[overlay_key]
|
||||
else: # file
|
||||
assert result["settings"]["render_command"][overlay_key] == overlay_input
|
||||
|
||||
assert result[overlay_key] == overlay_content[overlay_key]
|
||||
assert result["cv"]["name"] == "John Doe"
|
||||
|
||||
def test_all_overlays_simultaneously(self, create_yaml_file_fixture):
|
||||
def test_design_overlay_replaces_original(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
|
||||
result = build_rendercv_dictionary(main_yaml, design_yaml_file=design_yaml)
|
||||
|
||||
assert result["design"]["theme"] == "sb2nov"
|
||||
|
||||
def test_locale_overlay_replaces_original(self, minimal_input_dict):
|
||||
main_input = {**minimal_input_dict, "locale": {"language": "english"}}
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
result = build_rendercv_dictionary(main_yaml, locale_yaml_file=locale_yaml)
|
||||
|
||||
assert result["locale"]["language"] == "turkish"
|
||||
|
||||
def test_all_overlays_simultaneously(self, minimal_input_dict):
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
**minimal_input_dict,
|
||||
"locale": {"language": "english"},
|
||||
"settings": {"render_command": {"pdf_path": "original.pdf"}},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
design_yaml = dictionary_to_yaml(design_overlay)
|
||||
locale_yaml = dictionary_to_yaml(locale_overlay)
|
||||
|
||||
result = build_rendercv_dictionary(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
main_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
)
|
||||
|
||||
assert result["cv"]["name"] == "John Doe"
|
||||
# File overlays are stored as paths in settings.render_command
|
||||
assert result["settings"]["render_command"]["design"] == design_file
|
||||
assert result["settings"]["render_command"]["locale"] == locale_file
|
||||
# Original values remain unchanged when using file overlays
|
||||
assert result["design"]["theme"] == "classic"
|
||||
assert result["locale"]["language"] == "english"
|
||||
assert result["design"]["theme"] == "sb2nov"
|
||||
assert result["locale"]["language"] == "turkish"
|
||||
|
||||
def test_settings_overlay_without_render_command_allows_design_path_overlay(
|
||||
self, create_yaml_file_fixture
|
||||
):
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
}
|
||||
settings_overlay = {"settings": {"current_date": "2024-01-01"}}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
settings_file = create_yaml_file_fixture("settings.yaml", settings_overlay)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
def test_settings_overlay_with_design_overlay(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
settings_yaml = dictionary_to_yaml(
|
||||
{"settings": {"current_date": "2024-01-01"}}
|
||||
)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
|
||||
result = build_rendercv_dictionary(
|
||||
main_file,
|
||||
settings_file_path_or_contents=settings_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
main_yaml,
|
||||
settings_yaml_file=settings_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
)
|
||||
|
||||
assert result["settings"]["current_date"] == "2024-01-01"
|
||||
assert result["settings"]["render_command"]["design"] == design_file
|
||||
assert result["design"]["theme"] == "sb2nov"
|
||||
|
||||
def test_none_overlays_are_ignored(self, minimal_input_dict):
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
|
||||
result = build_rendercv_dictionary(
|
||||
yaml_input,
|
||||
design_yaml_file=None,
|
||||
locale_yaml_file=None,
|
||||
settings_yaml_file=None,
|
||||
)
|
||||
|
||||
assert result["cv"]["name"] == "John Doe"
|
||||
assert result["design"]["theme"] == "classic"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("override_key", "override_value"),
|
||||
@@ -211,25 +192,18 @@ class TestBuildRendercvDictionary:
|
||||
assert result["settings"]["render_command"]["typst_path"] == "new.typ"
|
||||
assert result["settings"]["other_setting"] == "preserved"
|
||||
|
||||
def test_combined_overlays_and_render_overrides(self, create_yaml_file_fixture):
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
def test_combined_overlays_and_render_overrides(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
result = build_rendercv_dictionary(
|
||||
main_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
main_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
pdf_path="custom.pdf",
|
||||
dont_generate_png=True,
|
||||
)
|
||||
|
||||
# File overlay is stored as path in settings.render_command
|
||||
assert result["settings"]["render_command"]["locale"] == locale_file
|
||||
assert result["locale"]["language"] == "turkish"
|
||||
assert result["settings"]["render_command"]["pdf_path"] == "custom.pdf"
|
||||
assert result["settings"]["render_command"]["dont_generate_png"] is True
|
||||
|
||||
@@ -341,42 +315,41 @@ class TestBuildRendercvModelFromDictionary:
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model._input_file_path == input_file_path
|
||||
|
||||
def test_invalid_input_raises_validation_error(self):
|
||||
invalid_dict = {"cv": {"name": 123}, "design": {"theme": "nonexistent_theme"}}
|
||||
|
||||
with pytest.raises(RenderCVUserValidationError):
|
||||
build_rendercv_model_from_commented_map(invalid_dict)
|
||||
|
||||
|
||||
class TestBuildRendercvModel:
|
||||
@pytest.mark.parametrize(
|
||||
"input_type",
|
||||
["string", "file"],
|
||||
)
|
||||
def test_basic_model_creation(
|
||||
self, minimal_input_dict, create_yaml_file_fixture, input_type
|
||||
):
|
||||
if input_type == "string":
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
expected_file_path = None
|
||||
else: # file
|
||||
yaml_input = create_yaml_file_fixture("input.yaml", minimal_input_dict)
|
||||
expected_file_path = yaml_input
|
||||
def test_basic_model_creation(self, minimal_input_dict):
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
|
||||
_, model = build_rendercv_dictionary_and_model(yaml_input)
|
||||
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model.cv.name == "John Doe"
|
||||
assert model._input_file_path == expected_file_path
|
||||
assert model._input_file_path is None
|
||||
|
||||
def test_basic_model_creation_with_input_file_path(
|
||||
self, minimal_input_dict, tmp_path
|
||||
):
|
||||
yaml_input = dictionary_to_yaml(minimal_input_dict)
|
||||
file_path = tmp_path / "input.yaml"
|
||||
|
||||
_, model = build_rendercv_dictionary_and_model(
|
||||
yaml_input, input_file_path=file_path
|
||||
)
|
||||
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model._input_file_path == file_path
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("overlay_type", "overlay_key"),
|
||||
[
|
||||
("string", "design"),
|
||||
("string", "locale"),
|
||||
("string", "settings"),
|
||||
("file", "design"),
|
||||
("file", "locale"),
|
||||
# Note: settings overlay via file is not supported
|
||||
],
|
||||
"overlay_key",
|
||||
["design", "locale", "settings"],
|
||||
)
|
||||
def test_with_single_overlay(
|
||||
self, minimal_input_dict, create_yaml_file_fixture, overlay_type, overlay_key
|
||||
):
|
||||
def test_with_single_overlay(self, minimal_input_dict, overlay_key):
|
||||
overlay_content = {
|
||||
"design": {"design": {"theme": "sb2nov"}},
|
||||
"locale": {"locale": {"language": "turkish"}},
|
||||
@@ -384,43 +357,32 @@ class TestBuildRendercvModel:
|
||||
}[overlay_key]
|
||||
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
overlay_yaml = dictionary_to_yaml(overlay_content)
|
||||
|
||||
if overlay_type == "string":
|
||||
overlay_input = dictionary_to_yaml(overlay_content)
|
||||
else: # file
|
||||
overlay_input = create_yaml_file_fixture(
|
||||
f"{overlay_key}.yaml", overlay_content
|
||||
)
|
||||
|
||||
kwargs = {f"{overlay_key}_file_path_or_contents": overlay_input}
|
||||
kwargs = {f"{overlay_key}_yaml_file": overlay_yaml}
|
||||
_, model = build_rendercv_dictionary_and_model(main_yaml, **kwargs) # pyright: ignore[reportArgumentType]
|
||||
|
||||
assert isinstance(model, RenderCVModel)
|
||||
|
||||
def test_with_all_overlays(self, create_yaml_file_fixture):
|
||||
def test_with_all_overlays(self):
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
"locale": {"language": "english"},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
_, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
main_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
)
|
||||
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model.cv.name == "John Doe"
|
||||
# File overlays should be loaded and applied
|
||||
assert model.design.theme == "sb2nov"
|
||||
assert model._input_file_path == main_file
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides",
|
||||
@@ -448,23 +410,18 @@ class TestBuildRendercvModel:
|
||||
else:
|
||||
assert model_value == value
|
||||
|
||||
def test_combined_overlays_and_overrides(
|
||||
self, minimal_input_dict, create_yaml_file_fixture
|
||||
):
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", minimal_input_dict)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
def test_combined_overlays_and_overrides(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
_, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
main_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
pdf_path="output.pdf",
|
||||
dont_generate_html=True,
|
||||
)
|
||||
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model._input_file_path == main_file
|
||||
assert model.settings.render_command.pdf_path.name == "output.pdf"
|
||||
assert model.settings.render_command.dont_generate_html is True
|
||||
|
||||
@@ -486,8 +443,12 @@ class TestBuildRendercvModel:
|
||||
assert model.cv.name == expected_name
|
||||
|
||||
def test_with_fixture_input_file(self, input_file_path):
|
||||
_, model = build_rendercv_dictionary_and_model(input_file_path)
|
||||
yaml_content = input_file_path.read_text(encoding="utf-8")
|
||||
_, model = build_rendercv_dictionary_and_model(
|
||||
yaml_content, input_file_path=input_file_path
|
||||
)
|
||||
assert isinstance(model, RenderCVModel)
|
||||
assert model._input_file_path == input_file_path
|
||||
|
||||
def test_with_yaml_string_using_ruamel(self):
|
||||
input_dictionary = {
|
||||
@@ -505,186 +466,107 @@ class TestBuildRendercvModel:
|
||||
_, model = build_rendercv_dictionary_and_model(yaml_string)
|
||||
assert isinstance(model, RenderCVModel)
|
||||
|
||||
def test_invalid_file_extension_raises_error(self, tmp_path):
|
||||
invalid_file_path = tmp_path / "invalid.extension"
|
||||
invalid_file_path.write_text("dummy content", encoding="utf-8")
|
||||
|
||||
def test_empty_yaml_raises_error(self):
|
||||
with pytest.raises(RenderCVUserError):
|
||||
build_rendercv_dictionary_and_model(invalid_file_path)
|
||||
build_rendercv_dictionary_and_model("")
|
||||
|
||||
def test_nonexistent_file_raises_error(self, tmp_path):
|
||||
non_existent_file_path = tmp_path / "non_existent_file.yaml"
|
||||
def test_invalid_yaml_raises_error(self):
|
||||
with pytest.raises(RenderCVUserValidationError):
|
||||
build_rendercv_dictionary_and_model("cv:\n name: 123\n")
|
||||
|
||||
with pytest.raises(RenderCVUserError):
|
||||
build_rendercv_dictionary_and_model(non_existent_file_path)
|
||||
|
||||
def test_design_file_overlay_loads_and_applies(self, create_yaml_file_fixture):
|
||||
"""Test that design file overlay is loaded from settings.render_command.design and applied to model."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
def test_design_overlay_merges_into_dictionary_and_model(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
|
||||
dictionary, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
main_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
)
|
||||
|
||||
# Dictionary should have file path stored, not the overlay content
|
||||
assert dictionary["settings"]["render_command"]["design"] == design_file
|
||||
assert dictionary["design"]["theme"] == "classic" # Original unchanged
|
||||
|
||||
# Model should have the design from the file applied
|
||||
# Overlay content should be merged directly into dictionary
|
||||
assert dictionary["design"]["theme"] == "sb2nov"
|
||||
# No file path should be stored in settings.render_command
|
||||
assert "design" not in dictionary["settings"]["render_command"]
|
||||
# Model should reflect the overlay
|
||||
assert model.design.theme == "sb2nov"
|
||||
|
||||
def test_design_overlay_applies_with_settings_overlay(
|
||||
self, create_yaml_file_fixture
|
||||
):
|
||||
"""Ensure design overlay applies even when settings overlay is provided."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
}
|
||||
settings_overlay = {
|
||||
"settings": {
|
||||
"render_command": {
|
||||
"typst_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.typ",
|
||||
"pdf_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.pdf",
|
||||
"markdown_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.md",
|
||||
"html_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.html",
|
||||
"png_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.png",
|
||||
"dont_generate_markdown": False,
|
||||
"dont_generate_html": False,
|
||||
"dont_generate_typst": False,
|
||||
"dont_generate_pdf": False,
|
||||
"dont_generate_png": False,
|
||||
def test_design_overlay_with_settings_overlay(self, minimal_input_dict):
|
||||
main_yaml = dictionary_to_yaml(minimal_input_dict)
|
||||
settings_yaml = dictionary_to_yaml(
|
||||
{
|
||||
"settings": {
|
||||
"render_command": {
|
||||
"typst_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.typ",
|
||||
"pdf_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.pdf",
|
||||
"markdown_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.md",
|
||||
"html_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.html",
|
||||
"png_path": "rendercv_output/NAME_IN_SNAKE_CASE_CV.png",
|
||||
"dont_generate_markdown": False,
|
||||
"dont_generate_html": False,
|
||||
"dont_generate_typst": False,
|
||||
"dont_generate_pdf": False,
|
||||
"dont_generate_png": False,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
settings_file = create_yaml_file_fixture("settings.yaml", settings_overlay)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
|
||||
_, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
settings_file_path_or_contents=settings_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
main_yaml,
|
||||
settings_yaml_file=settings_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
)
|
||||
|
||||
assert model.design.theme != "classic"
|
||||
assert model.design.theme == "sb2nov"
|
||||
|
||||
def test_locale_file_overlay_loads_and_applies(self, create_yaml_file_fixture):
|
||||
"""Test that locale file overlay is loaded from settings.render_command.locale and applied to model."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
"locale": {"language": "english"},
|
||||
}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
|
||||
dictionary, _ = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
)
|
||||
|
||||
# Dictionary should have file path stored, not the overlay content
|
||||
assert dictionary["settings"]["render_command"]["locale"] == locale_file
|
||||
assert dictionary["locale"]["language"] == "english" # Original unchanged
|
||||
|
||||
# Model should have the locale from the file applied
|
||||
# Note: locale is validated and merged, so we just verify it's applied
|
||||
|
||||
def test_both_design_and_locale_file_overlays_load_and_apply(
|
||||
self, create_yaml_file_fixture
|
||||
):
|
||||
"""Test that both design and locale file overlays are loaded and applied together."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
"locale": {"language": "english"},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "engineeringresumes"}}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_file = create_yaml_file_fixture("design.yaml", design_overlay)
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay)
|
||||
def test_locale_overlay_merges_into_dictionary_and_model(self, minimal_input_dict):
|
||||
main_input = {**minimal_input_dict, "locale": {"language": "english"}}
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
dictionary, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_file,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
main_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
)
|
||||
|
||||
# Dictionary should have file paths stored
|
||||
assert dictionary["settings"]["render_command"]["design"] == design_file
|
||||
assert dictionary["settings"]["render_command"]["locale"] == locale_file
|
||||
assert dictionary["design"]["theme"] == "classic" # Original unchanged
|
||||
assert dictionary["locale"]["language"] == "english" # Original unchanged
|
||||
# Overlay content should be merged directly into dictionary
|
||||
assert dictionary["locale"]["language"] == "turkish"
|
||||
# No file path should be stored in settings.render_command
|
||||
assert "locale" not in dictionary["settings"]["render_command"]
|
||||
# Model should reflect the overlay
|
||||
assert model.locale.language == "turkish"
|
||||
|
||||
# Model should have both overlays applied
|
||||
def test_both_design_and_locale_overlays(self, minimal_input_dict):
|
||||
main_input = {**minimal_input_dict, "locale": {"language": "english"}}
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "engineeringresumes"}})
|
||||
locale_yaml = dictionary_to_yaml({"locale": {"language": "turkish"}})
|
||||
|
||||
dictionary, model = build_rendercv_dictionary_and_model(
|
||||
main_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
locale_yaml_file=locale_yaml,
|
||||
)
|
||||
|
||||
assert dictionary["design"]["theme"] == "engineeringresumes"
|
||||
assert dictionary["locale"]["language"] == "turkish"
|
||||
assert model.design.theme == "engineeringresumes"
|
||||
assert model.locale.language == "turkish"
|
||||
|
||||
def test_string_overlay_merges_immediately(self, create_yaml_file_fixture):
|
||||
"""Test that string overlays are merged immediately into dictionary."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_yaml = dictionary_to_yaml(design_overlay)
|
||||
def test_design_overlay_independent_of_locale_overlay(self, minimal_input_dict):
|
||||
"""Design and locale overlays should merge independently."""
|
||||
main_input = {**minimal_input_dict, "locale": {"language": "english"}}
|
||||
main_yaml = dictionary_to_yaml(main_input)
|
||||
design_yaml = dictionary_to_yaml({"design": {"theme": "sb2nov"}})
|
||||
|
||||
dictionary, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_yaml,
|
||||
main_yaml,
|
||||
design_yaml_file=design_yaml,
|
||||
)
|
||||
|
||||
# Dictionary should have overlay content merged directly
|
||||
# Design should be overridden
|
||||
assert dictionary["design"]["theme"] == "sb2nov"
|
||||
# No file path should be stored
|
||||
assert "design" not in dictionary["settings"]["render_command"]
|
||||
|
||||
# Model should have the design from the string overlay
|
||||
assert model.design.theme == "sb2nov"
|
||||
|
||||
def test_mixed_string_and_file_overlays(self, create_yaml_file_fixture):
|
||||
"""Test that string and file overlays can be used together."""
|
||||
main_input = {
|
||||
"cv": {"name": "John Doe"},
|
||||
"design": {"theme": "classic"},
|
||||
"locale": {"language": "english"},
|
||||
}
|
||||
design_overlay = {"design": {"theme": "sb2nov"}}
|
||||
locale_overlay = {"locale": {"language": "turkish"}}
|
||||
|
||||
main_file = create_yaml_file_fixture("main.yaml", main_input)
|
||||
design_yaml = dictionary_to_yaml(design_overlay) # String
|
||||
locale_file = create_yaml_file_fixture("locale.yaml", locale_overlay) # File
|
||||
|
||||
dictionary, model = build_rendercv_dictionary_and_model(
|
||||
main_file,
|
||||
design_file_path_or_contents=design_yaml,
|
||||
locale_file_path_or_contents=locale_file,
|
||||
)
|
||||
|
||||
# Design string overlay should be merged directly
|
||||
assert dictionary["design"]["theme"] == "sb2nov"
|
||||
assert "design" not in dictionary["settings"]["render_command"]
|
||||
|
||||
# Locale file overlay should be stored as path
|
||||
assert dictionary["settings"]["render_command"]["locale"] == locale_file
|
||||
assert dictionary["locale"]["language"] == "english" # Original unchanged
|
||||
|
||||
# Both should be applied in the model
|
||||
assert model.design.theme == "sb2nov"
|
||||
# Locale should remain original
|
||||
assert dictionary["locale"]["language"] == "english"
|
||||
|
||||
Reference in New Issue
Block a user