Files
rendercv/tests/schema/test_rendercv_model_builder.py
Sina Atalay 5cc5fbdf9e Massive Refactor: Architecture Redesign and Technical Debt Cleanup (#528)
* Rename `data` folder with schema

* Start refactoring data models

* Work on entry models

* Keep working on entries

* Keep working on data models

* Push old data files

* Keep working on data models

* First draft of schema.cv

* Keep working on schema

* Keep working on schema

* Improve schema.models

* Keep working on rendercv.schema

* Work on schema.design

* Keep working on rendercv.schema

* Complete variant_class_generator

* Keep working rendercv.schema

* Keep working on rendercv.schema

* Final touches to rendercv.schema

* Improve json schema descriptions in rendercv.schema

* Start working on rendercv.schema tests

* Keep implementing rendercv.schema tests

* Add more tests for rendercv.schema

* Improve rendercv.schema

* Improve docstrings and comments in rendercv.schema

* Implement better pydantic error handling in `rendercv.schema`

* Improve variant class system

* Fix rendercv.schema tests

* Start working on rendercv.templater

* Update template names

* Switching to new rendercv typst template soon

* Work on new templater

* Rename renderer with renderer_old

* Don't use utils in rendercv.schema

* Complete connections

* Update renderer folder structure

* Work on new renderer

* Work on new renderer

* Date processing on new renderer

* Improve date processing, support multiple emails, phones, and websites

* Improve markdown to Typst

* Complete entry template processing

* Time span computation in new renderer

* Better entry templates

* Setup new templates

* Improve rendercv.schema

* Start adding tests for rendercv.renderer

* New markdown parser!

* Improve markdown to typst conversion

* Finalize markdown parser

* Add new test files for rendercv.renderer

* Fix cv and connections

* Add connections test

* Improve connection tests

* Improve entry templates

* Add model processor tests

* Improve templater

* Rename old folders

* Improve schema

* Add file generation logic to renderer

* Fix naming issues

* Fix schema tests

* Add path type tests

* Add font family and typst dimension type tests

* Rename old tests

* Fix design tests

* Start integration testing of renderer

* Improve entry tempates

* Handle nested highlights properly

* Finalize Typst preamble template

* Start working on new CLI

* Remove old test files

* Implement override dictionary in new schema

* Start working on new CLI

* Better prints on render command

* New structure

* New render printer

* Add all the commands to new CLI

* Work on new command in new cli

* Improve new command

* Add error handler to new cli

* Work on create theme command

* Complete create theme command

* Remove old source files

* Improve exceptions

* Create new docs

* Add writing tests guide

* Fix cli printer and write tests

* Test copy templates

* Add app tests

* Bring back accidentally removed files

* Imporve cli and tests

* Fix path issues

* Improve

* Improve

* Add reference file comparison tests

* Fix path resolver

* Start working on test_pdf_png

* Implement comparison of multiple files (png)

* Start testing typst

* Fix templating issues

* Fix header and entry templates issues

* Implement short second rows

* Fix date issues

* Fix nested bullets and add summary

* Update testdata

* Implement footer

* Update testdata

* Reimagined design and locale schema, first iteration

* Reimagined design and locale second iteration

* Update design and locale schemas

* Adapt templater to the new design and locale

* Fix tests

* Update lib.typ and testdata for the new locale and design

* Implement proper tests with all combinations of entries

* Remove some docstrings

* fix connections logic

* Improve

* Start working on examples

* Update testdata

* Fix long second row issue

* fix templating issues

* Fix lib.typ issues

* Update testdata

* Fix clean_trailing_parts

* Update test cv

* update test cv

* Update theme defaults

* update schema and fix moderncv

* Fix moderncv issues

* Update testdata

* Update testdata and examples

* Fix issues about photo

* Fix typst photo path issues

* improve entry templates from yaml

* add new locale

* Rename writing tests doc

* Update writing tests

* Improve tests

* Add more cli tests

* Increase test coverage

* Rename variant pydantic model generator

* Improve tests

* Update testdata and improve tests

* Format, fix pre-commit errors

* Fix scripts and update entry figures

* Improve tests

* Write docstrings of schema

* Write schema docstrings

* Setup api reference

* Start working on new docs

* Work on docs

* Improve progress panel of render command

* Finalize new docs index

* Complete CLI docs

* Work on YAML input structure page

* Finalize user guide

* Start working on developer guide

* Improve api reference

* Improve developer guide

* Improve developer guide

* Improve developer gide

* Improve developer guide

* Improve developer guide

* Update developer guide

* Improve developer guide

* Improve developer guide

* Improve developer guide

* Developer guide first draft

* update developer guide

* Update examples

* Update testdata

* Handle wrong installation (rendercv instead of rendercv[full])

* Remove unnecessary files

* Write set up vs code page

* Update README.md

* Change docs description

* Compress design options gif

* minor updates

* Polish all the json schema descriptions

* Update testdata and examples

* Remove some emdashed from docs

* Add whatsapp support

* Add TestEscapeTypstCharacters to tests

* Implement custom connections

* Add page break before sections feature

* Revert page break before sections feature

* Rebase to main

* Fix social network tests, update schema
2025-12-09 17:03:56 +03:00

485 lines
17 KiB
Python

import io
import pathlib
from datetime import date as Date
import pytest
import ruamel.yaml
from rendercv.exception import RenderCVUserError
from rendercv.schema.models.rendercv_model import RenderCVModel
from rendercv.schema.rendercv_model_builder import (
build_rendercv_dictionary,
build_rendercv_dictionary_and_model,
build_rendercv_model_from_commented_map,
)
from rendercv.schema.sample_generator import dictionary_to_yaml
@pytest.fixture
def minimal_input_dict():
return {
"cv": {"name": "John Doe"},
"design": {"theme": "classic"},
}
@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)
result = build_rendercv_dictionary(yaml_input)
assert result["cv"]["name"] == "John Doe"
assert result["design"]["theme"] == "classic"
@pytest.mark.parametrize("input_type", ["string", "file"])
@pytest.mark.parametrize(
("overlay_key", "overlay_content"),
[
("design", {"design": {"theme": "engineeringresumes"}}),
("locale", {"locale": {"language": "turkish"}}),
(
"settings",
{"settings": {"render_command": {"pdf_path": "custom.pdf"}}},
),
],
)
def test_single_overlay(
self,
minimal_input_dict,
create_yaml_file_fixture,
overlay_key,
overlay_content,
input_type,
):
main_input = {
**minimal_input_dict,
"locale": {"language": "english"},
"settings": {"original_setting": "original"},
}
main_yaml = dictionary_to_yaml(main_input)
# Create overlay as string or file
if input_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}
result = build_rendercv_dictionary(main_yaml, **kwargs) # pyright: ignore[reportArgumentType]
assert result[overlay_key] == overlay_content[overlay_key]
assert result["cv"]["name"] == "John Doe"
def test_all_overlays_simultaneously(self, create_yaml_file_fixture):
main_input = {
"cv": {"name": "John Doe"},
"design": {"theme": "classic"},
"locale": {"language": "english"},
"settings": {"render_command": {"pdf_path": "original.pdf"}},
}
design_overlay = {"design": {"theme": "sb2nov"}}
locale_overlay = {"locale": {"language": "turkish"}}
settings_overlay = {
"settings": {"render_command": {"pdf_path": "replaced.pdf"}}
}
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)
settings_file = create_yaml_file_fixture("settings.yaml", settings_overlay)
result = build_rendercv_dictionary(
main_file,
design_file_path_or_contents=design_file,
locale_file_path_or_contents=locale_file,
settings_file_path_or_contents=settings_file,
)
assert result["cv"]["name"] == "John Doe"
assert result["design"]["theme"] == "sb2nov"
assert result["locale"]["language"] == "turkish"
assert result["settings"]["render_command"]["pdf_path"] == "replaced.pdf"
@pytest.mark.parametrize(
("override_key", "override_value"),
[
("typst_path", "custom.typ"),
("pdf_path", "output.pdf"),
("markdown_path", "output.md"),
("html_path", "output.html"),
("png_path", "output.png"),
("dont_generate_html", True),
("dont_generate_markdown", True),
("dont_generate_pdf", True),
("dont_generate_png", True),
],
)
def test_render_command_single_override(
self, minimal_input_dict, override_key, override_value
):
yaml_input = dictionary_to_yaml(minimal_input_dict)
kwargs = {override_key: override_value}
result = build_rendercv_dictionary(yaml_input, **kwargs)
assert result["settings"]["render_command"][override_key] == override_value
def test_render_command_multiple_overrides(self, minimal_input_dict):
yaml_input = dictionary_to_yaml(minimal_input_dict)
result = build_rendercv_dictionary(
yaml_input,
pdf_path="output.pdf",
typst_path="output.typ",
dont_generate_html=True,
dont_generate_markdown=True,
)
render_cmd = result["settings"]["render_command"]
assert render_cmd["pdf_path"] == "output.pdf"
assert render_cmd["typst_path"] == "output.typ"
assert render_cmd["dont_generate_html"] is True
assert render_cmd["dont_generate_markdown"] is True
def test_render_command_preserves_existing_settings(self):
input_dict = {
"cv": {"name": "John Doe"},
"design": {"theme": "classic"},
"settings": {
"render_command": {"pdf_path": "existing.pdf"},
"other_setting": "preserved",
},
}
yaml_input = dictionary_to_yaml(input_dict)
result = build_rendercv_dictionary(yaml_input, typst_path="new.typ")
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)
result = build_rendercv_dictionary(
main_file,
locale_file_path_or_contents=locale_file,
pdf_path="custom.pdf",
dont_generate_png=True,
)
assert result["locale"]["language"] == "turkish"
assert result["settings"]["render_command"]["pdf_path"] == "custom.pdf"
assert result["settings"]["render_command"]["dont_generate_png"] is True
@pytest.mark.parametrize(
("overrides", "expected_checks"),
[
(
{"cv.name": "Jane Doe"},
[("cv", "name", "Jane Doe")],
),
(
{"design.theme": "sb2nov"},
[("design", "theme", "sb2nov")],
),
(
{"cv.name": "Jane Doe", "design.theme": "engineeringresumes"},
[("cv", "name", "Jane Doe"), ("design", "theme", "engineeringresumes")],
),
],
)
def test_overrides_parameter(self, minimal_input_dict, overrides, expected_checks):
yaml_input = dictionary_to_yaml(minimal_input_dict)
result = build_rendercv_dictionary(yaml_input, overrides=overrides)
for path_and_value in expected_checks:
value = result
for key in path_and_value[:-1]:
value = value[key]
assert value == path_and_value[-1]
def test_overrides_with_nested_paths(self, minimal_input_dict):
input_dict = {
**minimal_input_dict,
"cv": {
"name": "John Doe",
"sections": {"education": [{"institution": "MIT", "degree": "PhD"}]},
},
}
yaml_input = dictionary_to_yaml(input_dict)
result = build_rendercv_dictionary(
yaml_input,
overrides={
"cv.sections.education.0.institution": "Harvard",
"cv.sections.education.0.degree": "MS",
},
)
assert result["cv"]["sections"]["education"][0]["institution"] == "Harvard"
assert result["cv"]["sections"]["education"][0]["degree"] == "MS"
assert result["cv"]["name"] == "John Doe"
class TestBuildRendercvModelFromDictionary:
def test_basic_model_creation_without_optionals(self, minimal_input_dict):
model = build_rendercv_model_from_commented_map(minimal_input_dict)
assert isinstance(model, RenderCVModel)
assert model.cv.name == "John Doe"
assert model._input_file_path is None
def test_with_input_file_path(self, minimal_input_dict, tmp_path):
input_file_path = tmp_path / "test.yaml"
model = build_rendercv_model_from_commented_map(
minimal_input_dict, input_file_path
)
assert isinstance(model, RenderCVModel)
assert model._input_file_path == input_file_path
def test_without_input_file_path(self, minimal_input_dict):
model = build_rendercv_model_from_commented_map(minimal_input_dict)
assert model._input_file_path is None
@pytest.mark.parametrize(
"settings",
[
{"current_date": Date(2023, 6, 15)},
None,
{},
],
)
def test_validation_context_current_date(self, minimal_input_dict, settings):
input_dict = minimal_input_dict.copy()
if settings is not None:
input_dict["settings"] = settings
model = build_rendercv_model_from_commented_map(input_dict)
assert isinstance(model, RenderCVModel)
def test_validation_context_with_input_file_path(
self, minimal_input_dict, tmp_path
):
input_file_path = tmp_path / "test.yaml"
custom_date = Date(2024, 3, 10)
input_dict = {
**minimal_input_dict,
"settings": {"current_date": custom_date},
}
model = build_rendercv_model_from_commented_map(input_dict, input_file_path) # pyright: ignore[reportArgumentType]
assert isinstance(model, RenderCVModel)
assert model._input_file_path == input_file_path
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
_, 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
@pytest.mark.parametrize("overlay_type", ["string", "file"])
@pytest.mark.parametrize("overlay_key", ["design", "locale", "settings"])
def test_with_single_overlay(
self, minimal_input_dict, create_yaml_file_fixture, overlay_type, overlay_key
):
overlay_content = {
"design": {"design": {"theme": "sb2nov"}},
"locale": {"locale": {"language": "turkish"}},
"settings": {"settings": {"render_command": {"pdf_path": "custom.pdf"}}},
}[overlay_key]
main_yaml = dictionary_to_yaml(minimal_input_dict)
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}
_, 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):
main_input = {
"cv": {"name": "John Doe"},
"design": {"theme": "classic"},
"locale": {"language": "english"},
}
design_overlay = {"design": {"theme": "sb2nov"}}
locale_overlay = {"locale": {"language": "turkish"}}
settings_overlay = {
"settings": {"render_command": {"markdown_path": "custom.md"}}
}
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)
settings_file = create_yaml_file_fixture("settings.yaml", settings_overlay)
_, model = build_rendercv_dictionary_and_model(
main_file,
design_file_path_or_contents=design_file,
locale_file_path_or_contents=locale_file,
settings_file_path_or_contents=settings_file,
)
assert isinstance(model, RenderCVModel)
assert model.cv.name == "John Doe"
assert model.design.theme == "sb2nov"
assert model._input_file_path == main_file
@pytest.mark.parametrize(
"overrides",
[
{"pdf_path": "custom.pdf"},
{"typst_path": "custom.typ", "markdown_path": "custom.md"},
{"dont_generate_html": True, "dont_generate_pdf": True},
{
"pdf_path": "all.pdf",
"typst_path": "all.typ",
"dont_generate_png": True,
},
],
)
def test_with_render_command_overrides(self, minimal_input_dict, overrides):
main_yaml = dictionary_to_yaml(minimal_input_dict)
_, model = build_rendercv_dictionary_and_model(main_yaml, **overrides)
assert isinstance(model, RenderCVModel)
for key, value in overrides.items():
model_value = getattr(model.settings.render_command, key)
if isinstance(value, str) and key.endswith("_path"):
assert model_value == pathlib.Path(value).resolve()
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)
_, model = build_rendercv_dictionary_and_model(
main_file,
locale_file_path_or_contents=locale_file,
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
@pytest.mark.parametrize(
("overrides", "expected_name"),
[
({"cv.name": "Jane Doe"}, "Jane Doe"),
({"cv.name": "Bob Smith"}, "Bob Smith"),
],
)
def test_with_overrides_parameter(
self, minimal_input_dict, overrides, expected_name
):
yaml_input = dictionary_to_yaml(minimal_input_dict)
_, model = build_rendercv_dictionary_and_model(yaml_input, overrides=overrides)
assert isinstance(model, RenderCVModel)
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)
assert isinstance(model, RenderCVModel)
def test_with_yaml_string_using_ruamel(self):
input_dictionary = {
"cv": {"name": "John Doe"},
"design": {"theme": "classic"},
}
yaml_object = ruamel.yaml.YAML()
yaml_object.width = 60
yaml_object.indent(mapping=2, sequence=4, offset=2)
with io.StringIO() as string_stream:
yaml_object.dump(input_dictionary, string_stream)
yaml_string = string_stream.getvalue()
_, 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")
with pytest.raises(RenderCVUserError):
build_rendercv_dictionary_and_model(invalid_file_path)
def test_nonexistent_file_raises_error(self, tmp_path):
non_existent_file_path = tmp_path / "non_existent_file.yaml"
with pytest.raises(RenderCVUserError):
build_rendercv_dictionary_and_model(non_existent_file_path)