diff --git a/src/rendercv/cli/render_command/render_command.py b/src/rendercv/cli/render_command/render_command.py index 393bde09..61fcd00b 100644 --- a/src/rendercv/cli/render_command/render_command.py +++ b/src/rendercv/cli/render_command/render_command.py @@ -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: diff --git a/src/rendercv/cli/render_command/run_rendercv.py b/src/rendercv/cli/render_command/run_rendercv.py index 7b6de657..49ff066e 100644 --- a/src/rendercv/cli/render_command/run_rendercv.py +++ b/src/rendercv/cli/render_command/run_rendercv.py @@ -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( diff --git a/src/rendercv/schema/rendercv_model_builder.py b/src/rendercv/schema/rendercv_model_builder.py index 81398245..68cb4b5f 100644 --- a/src/rendercv/schema/rendercv_model_builder.py +++ b/src/rendercv/schema/rendercv_model_builder.py @@ -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 diff --git a/tests/cli/new_command/test_new_command.py b/tests/cli/new_command/test_new_command.py index 4309d9b9..f4165b71 100644 --- a/tests/cli/new_command/test_new_command.py +++ b/tests/cli/new_command/test_new_command.py @@ -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: diff --git a/tests/schema/test_rendercv_model_builder.py b/tests/schema/test_rendercv_model_builder.py index 44a6abfc..fc4eb57b 100644 --- a/tests/schema/test_rendercv_model_builder.py +++ b/tests/schema/test_rendercv_model_builder.py @@ -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"