diff --git a/rendercv/data_models.py b/rendercv/data_models.py index a28cef6..54cfa80 100644 --- a/rendercv/data_models.py +++ b/rendercv/data_models.py @@ -25,6 +25,7 @@ import json import re import ssl import pathlib +import warnings import pydantic import pydantic_extra_types.phone_numbers as pydantic_phone_numbers @@ -35,6 +36,9 @@ from .themes.moderncv import ModerncvThemeOptions from .themes.sb2nov import Sb2novThemeOptions from .themes.engineeringresumes import EngineeringresumesThemeOptions +# disable Pydantic warnings: +warnings.filterwarnings("ignore") + # Create a custom type called RenderCVDate that accepts only strings in YYYY-MM-DD or # YYYY-MM format: # This type is used to validate the date fields in the data. @@ -1126,6 +1130,69 @@ class RenderCVDataModel(RenderCVBaseModel): return theme_data_model +def set_or_update_a_value( + data_model: pydantic.BaseModel | dict | list, + key: str, + value: Any, + sub_model: pydantic.BaseModel | dict | list = None, +): + """Set or update a value in a data model for a specific key. For example, a key can + be `cv.sections.education.3.institution` and the value can be "Bogazici University". + + Args: + data_model (pydantic.BaseModel | dict | list): The data model to set or update + the value. + key (str): The key to set or update the value. + value (Any): The value to set or update. + sub_model (pydantic.BaseModel | dict | list, optional): The sub model to set or + update the value. This is used for recursive calls. When the value is set + to a sub model, the original data model is validated. Defaults to None. + """ + # recursively call this function until the last key is reached: + + # rename `sections` with `sections_input` since the key is `sections` is an alias: + key = key.replace("sections.", "sections_input.") + keys = key.split(".") + + if sub_model is not None: + model = sub_model + else: + model = data_model + + if len(keys) == 1: + # set the value: + if isinstance(model, pydantic.BaseModel): + setattr(model, key, value) + elif isinstance(model, dict): + model[key] = value + elif isinstance(model, list): + model[int(key)] = value + else: + raise ValueError( + "The data model should be either a Pydantic data model, dictionary, or" + " list.", + ) + + data_model.model_validate(data_model.model_dump(by_alias=True)) + else: + # get the first key and call the function with remaining keys: + first_key = keys[0] + key = ".".join(keys[1:]) + if isinstance(model, pydantic.BaseModel): + sub_model = getattr(model, first_key) + elif isinstance(model, dict): + sub_model = model[first_key] + elif isinstance(model, list): + sub_model = model[int(first_key)] + else: + raise ValueError( + "The data model should be either a Pydantic data model, dictionary, or" + " list.", + ) + + set_or_update_a_value(data_model, key, value, sub_model) + + def read_input_file( file_path: pathlib.Path, ) -> RenderCVDataModel: diff --git a/tests/test_data_models.py b/tests/test_data_models.py index 5aa204d..f8c8148 100644 --- a/tests/test_data_models.py +++ b/tests/test_data_models.py @@ -2,6 +2,7 @@ from datetime import date as Date import json import pathlib import os +import re import shutil import pydantic @@ -59,6 +60,52 @@ def test_format_date(date, expected_date_string): assert dm.format_date(date) == expected_date_string +@pytest.mark.parametrize( + "key, value", + [ + ("cv.phone", "+905555555555"), + ("cv.email", "test@example.com"), + ("cv.sections.education.0.degree", "PhD"), + ("design.page_size", "a4paper"), + ], +) +def test_set_or_update_a_value(rendercv_data_model, key, value): + dm.set_or_update_a_value(rendercv_data_model, key, value) + + # replace with regex pattern: + key = re.sub(r"sections\.([^\.]*?)\.(\d+)", 'sections_input["\\1"][\\2]', key) + + assert eval(f"rendercv_data_model.{key}") == value + + +@pytest.mark.parametrize( + "key, value", + [ + ("cv.phones", "+905555555555"), + ("cv.emssdsail", ""), + ("cv.sections.education.99.degree", "PhD"), + ("dessssign.page_size", "a4paper"), + ], +) +def test_set_or_update_a_value_invalid_keys(rendercv_data_model, key, value): + with pytest.raises((ValueError, KeyError, IndexError, AttributeError)): + dm.set_or_update_a_value(rendercv_data_model, key, value) + + +@pytest.mark.parametrize( + "key, value", + [ + ("cv.phone", "+9999995555555555"), + ("cv.email", "notanemail***"), + ("cv.sections.education.0.highlights", "this is not a list"), + ("design.page_size", "invalid_page_size"), + ], +) +def test_set_or_update_a_value_invalid_values(rendercv_data_model, key, value): + with pytest.raises(pydantic.ValidationError): + dm.set_or_update_a_value(rendercv_data_model, key, value) + + def test_read_input_file(input_file_path): # Update the auxiliary files if update_testdata is True if update_testdata: