Coverage for rendercv/data/models/design.py: 100%
46 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
1"""
2The `rendercv.data.models.design` module contains the data model of the `design` field
3of the input file.
4"""
6import importlib
7import importlib.util
8import pathlib
9from typing import Annotated, Any, Type
11import pydantic
13from ...themes import (
14 ClassicThemeOptions,
15 EngineeringresumesThemeOptions,
16 ModerncvThemeOptions,
17 Sb2novThemeOptions,
18)
19from . import entry_types
20from .base import RenderCVBaseModelWithoutExtraKeys
22# ======================================================================================
23# Create validator functions: ==========================================================
24# ======================================================================================
27def validate_design_options(
28 design: Any,
29 available_theme_options: dict[str, Type],
30 available_entry_type_names: list[str],
31) -> Any:
32 """Chech if the design options are for a built-in theme or a custom theme. If it is
33 a built-in theme, validate it with the corresponding data model. If it is a custom
34 theme, check if the necessary files are provided and validate it with the custom
35 theme data model, found in the `__init__.py` file of the custom theme folder.
37 Args:
38 design (Any | RenderCVBuiltinDesign): The design options to validate.
39 available_theme_options (dict[str, Type]): The available theme options. The keys
40 are the theme names and the values are the corresponding data models.
41 available_entry_type_names (list[str]): The available entry type names. These
42 are used to validate if all the templates are provided in the custom theme
43 folder.
45 Returns:
46 Any: The validated design as a Pydantic data model.
47 """
48 if isinstance(design, tuple(available_theme_options.values())):
49 # Then it means it is an already validated built-in theme. Return it as it is:
50 return design
51 elif design["theme"] in available_theme_options:
52 # Then it is a built-in theme, but it is not validated yet. Validate it and
53 # return it:
54 ThemeDataModel = available_theme_options[design["theme"]]
55 return ThemeDataModel(**design)
56 else:
57 # It is a custom theme. Validate it:
58 theme_name: str = str(design["theme"])
60 # Custom theme should only contain letters and digits:
61 if not theme_name.isalnum():
62 raise ValueError(
63 "The custom theme name should only contain letters and digits.",
64 "theme", # this is the location of the error
65 theme_name, # this is value of the error
66 )
68 custom_theme_folder = pathlib.Path(theme_name)
70 # Check if the custom theme folder exists:
71 if not custom_theme_folder.exists():
72 raise ValueError(
73 f"The custom theme folder `{custom_theme_folder}` does not exist."
74 " It should be in the working directory as the input file.",
75 "", # this is the location of the error
76 theme_name, # this is value of the error
77 )
79 # check if all the necessary files are provided in the custom theme folder:
80 required_entry_files = [
81 entry_type_name + ".j2.tex"
82 for entry_type_name in available_entry_type_names
83 ]
84 required_files = [
85 "SectionBeginning.j2.tex", # section beginning template
86 "SectionEnding.j2.tex", # section ending template
87 "Preamble.j2.tex", # preamble template
88 "Header.j2.tex", # header template
89 ] + required_entry_files
91 for file in required_files:
92 file_path = custom_theme_folder / file
93 if not file_path.exists():
94 raise ValueError(
95 f"You provided a custom theme, but the file `{file}` is not"
96 f" found in the folder `{custom_theme_folder}`.",
97 "", # This is the location of the error
98 theme_name, # This is value of the error
99 )
101 # Import __init__.py file from the custom theme folder if it exists:
102 path_to_init_file = pathlib.Path(f"{theme_name}/__init__.py")
104 if path_to_init_file.exists():
105 spec = importlib.util.spec_from_file_location(
106 "theme",
107 path_to_init_file,
108 )
110 theme_module = importlib.util.module_from_spec(spec) # type: ignore
111 try:
112 spec.loader.exec_module(theme_module) # type: ignore
113 except SyntaxError:
114 raise ValueError(
115 f"The custom theme {theme_name}'s __init__.py file has a syntax"
116 " error. Please fix it.",
117 )
118 except ImportError:
119 raise ValueError(
120 f"The custom theme {theme_name}'s __init__.py file has an"
121 " import error. If you have copy-pasted RenderCV's built-in"
122 " themes, make sure to update the import statements (e.g.,"
123 ' "from . import" to "from rendercv.themes import").',
124 )
126 ThemeDataModel = getattr(
127 theme_module, f"{theme_name.capitalize()}ThemeOptions" # type: ignore
128 )
130 # Initialize and validate the custom theme data model:
131 theme_data_model = ThemeDataModel(**design)
132 else:
133 # Then it means there is no __init__.py file in the custom theme folder.
134 # Create a dummy data model and use that instead.
135 class ThemeOptionsAreNotProvided(RenderCVBaseModelWithoutExtraKeys):
136 theme: str = theme_name
138 theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
140 return theme_data_model
143# ======================================================================================
144# Create custom types: =================================================================
145# ======================================================================================
147# Create a custom type named RenderCVBuiltinDesign:
148# It is a union of all the design options and the correct design option is determined by
149# the theme field, thanks to Pydantic's discriminator feature.
150# See https://docs.pydantic.dev/2.7/concepts/fields/#discriminator for more information
151RenderCVBuiltinDesign = Annotated[
152 ClassicThemeOptions
153 | ModerncvThemeOptions
154 | Sb2novThemeOptions
155 | EngineeringresumesThemeOptions,
156 pydantic.Field(discriminator="theme"),
157]
159# Create a custom type named RenderCVDesign:
160# RenderCV supports custom themes as well. Therefore, `Any` type is used to allow custom
161# themes. However, the JSON Schema generation is skipped, otherwise, the JSON Schema
162# would accept any `design` field in the YAML input file.
163RenderCVDesign = Annotated[
164 pydantic.json_schema.SkipJsonSchema[Any] | RenderCVBuiltinDesign,
165 pydantic.BeforeValidator(
166 lambda design: validate_design_options(
167 design,
168 available_theme_options=available_theme_options,
169 available_entry_type_names=entry_types.available_entry_type_names,
170 )
171 ),
172]
175available_theme_options = {
176 "classic": ClassicThemeOptions,
177 "moderncv": ModerncvThemeOptions,
178 "sb2nov": Sb2novThemeOptions,
179 "engineeringresumes": EngineeringresumesThemeOptions,
180}
182available_themes = list(available_theme_options.keys())