Coverage for rendercv/data/models/design.py: 100%
57 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +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 os
9import pathlib
10from typing import Annotated, Any
12import pydantic
14from ...themes import (
15 ClassicThemeOptions,
16 EngineeringclassicThemeOptions,
17 EngineeringresumesThemeOptions,
18 ModerncvThemeOptions,
19 Sb2novThemeOptions,
20)
21from . import entry_types
22from .base import RenderCVBaseModelWithoutExtraKeys
24# ======================================================================================
25# Create validator functions: ==========================================================
26# ======================================================================================
29def validate_design_options(
30 design: Any,
31 available_theme_options: dict[str, type],
32 available_entry_type_names: list[str],
33) -> Any:
34 """Chech if the design options are for a built-in theme or a custom theme. If it is
35 a built-in theme, validate it with the corresponding data model. If it is a custom
36 theme, check if the necessary files are provided and validate it with the custom
37 theme data model, found in the `__init__.py` file of the custom theme folder.
39 Args:
40 design: The design options to validate.
41 available_theme_options: The available theme options. The keys are the theme
42 names and the values are the corresponding data models.
43 available_entry_type_names: The available entry type names. These are used to
44 validate if all the templates are provided in the custom theme folder.
46 Returns:
47 The validated design as a Pydantic data model.
48 """
49 from .rendercv_data_model import INPUT_FILE_DIRECTORY
51 original_working_directory = pathlib.Path.cwd()
53 # Change the working directory to the input file directory:
55 if isinstance(design, tuple(available_theme_options.values())):
56 # Then it means it is an already validated built-in theme. Return it as it is:
57 return design
58 if design["theme"] in available_theme_options:
59 # Then it is a built-in theme, but it is not validated yet. Validate it and
60 # return it:
61 ThemeDataModel = available_theme_options[design["theme"]]
62 return ThemeDataModel(**design)
63 # It is a custom theme. Validate it:
64 theme_name: str = str(design["theme"])
66 # Custom theme should only contain letters and digits:
67 if not theme_name.isalnum():
68 message = "The custom theme name should only contain letters and digits."
69 raise ValueError(
70 message,
71 "theme", # this is the location of the error
72 theme_name, # this is value of the error
73 )
75 if INPUT_FILE_DIRECTORY is None:
76 theme_parent_folder = pathlib.Path.cwd()
77 else:
78 theme_parent_folder = INPUT_FILE_DIRECTORY
80 custom_theme_folder = theme_parent_folder / theme_name
82 # Check if the custom theme folder exists:
83 if not custom_theme_folder.exists():
84 message = (
85 (
86 f"The custom theme folder `{custom_theme_folder}` does not exist."
87 " It should be in the working directory as the input file."
88 ),
89 )
90 raise ValueError(
91 message,
92 "", # this is the location of the error
93 theme_name, # this is value of the error
94 )
96 # check if all the necessary files are provided in the custom theme folder:
97 required_entry_files = [
98 custom_theme_folder / (entry_type_name + ".j2.typ")
99 for entry_type_name in available_entry_type_names
100 ]
101 required_files = [
102 custom_theme_folder / "SectionBeginning.j2.typ", # section beginning template
103 custom_theme_folder / "SectionEnding.j2.typ", # section ending template
104 custom_theme_folder / "Preamble.j2.typ", # preamble template
105 custom_theme_folder / "Header.j2.typ", # header template
106 *required_entry_files,
107 ]
109 for file in required_files:
110 if not file.exists():
111 message = (
112 f"You provided a custom theme, but the file `{file}` is not"
113 f" found in the folder `{custom_theme_folder}`."
114 )
115 raise ValueError(
116 message,
117 "", # This is the location of the error
118 theme_name, # This is value of the error
119 )
121 # Import __init__.py file from the custom theme folder if it exists:
122 path_to_init_file = custom_theme_folder / "__init__.py"
124 if path_to_init_file.exists():
125 spec = importlib.util.spec_from_file_location(
126 "theme",
127 path_to_init_file,
128 )
130 theme_module = importlib.util.module_from_spec(spec) # type: ignore
131 try:
132 spec.loader.exec_module(theme_module) # type: ignore
133 except SyntaxError as e:
134 message = (
135 f"The custom theme {theme_name}'s __init__.py file has a syntax"
136 " error. Please fix it."
137 )
138 raise ValueError(message) from e
139 except ImportError as e:
140 message = (
141 (
142 f"The custom theme {theme_name}'s __init__.py file has an"
143 " import error. If you have copy-pasted RenderCV's built-in"
144 " themes, make sure to update the import statements (e.g.,"
145 ' "from . import" to "from rendercv.themes import").'
146 ),
147 )
149 raise ValueError(message) from e
151 ThemeDataModel = getattr(
152 theme_module,
153 f"{theme_name.capitalize()}ThemeOptions", # type: ignore
154 )
156 # Initialize and validate the custom theme data model:
157 theme_data_model = ThemeDataModel(**design)
158 else:
159 # Then it means there is no __init__.py file in the custom theme folder.
160 # Create a dummy data model and use that instead.
161 class ThemeOptionsAreNotProvided(RenderCVBaseModelWithoutExtraKeys):
162 theme: str = theme_name
164 theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
166 os.chdir(original_working_directory)
168 return theme_data_model
171# ======================================================================================
172# Create custom types: =================================================================
173# ======================================================================================
175available_theme_options = {
176 "classic": ClassicThemeOptions,
177 "sb2nov": Sb2novThemeOptions,
178 "engineeringresumes": EngineeringresumesThemeOptions,
179 "engineeringclassic": EngineeringclassicThemeOptions,
180 "moderncv": ModerncvThemeOptions,
181}
183available_themes = list(available_theme_options.keys())
185# Create a custom type named RenderCVBuiltinDesign:
186# It is a union of all the design options and the correct design option is determined by
187# the theme field, thanks to Pydantic's discriminator feature.
188# See https://docs.pydantic.dev/2.7/concepts/fields/#discriminator for more information
189RenderCVBuiltinDesign = Annotated[
190 ClassicThemeOptions
191 | Sb2novThemeOptions
192 | EngineeringresumesThemeOptions
193 | EngineeringclassicThemeOptions
194 | ModerncvThemeOptions,
195 pydantic.Field(discriminator="theme"),
196]
198# Create a custom type named RenderCVDesign:
199# RenderCV supports custom themes as well. Therefore, `Any` type is used to allow custom
200# themes. However, the JSON Schema generation is skipped, otherwise, the JSON Schema
201# would accept any `design` field in the YAML input file.
202RenderCVDesign = Annotated[
203 pydantic.json_schema.SkipJsonSchema[Any] | RenderCVBuiltinDesign,
204 pydantic.BeforeValidator(
205 lambda design: validate_design_options(
206 design,
207 available_theme_options=available_theme_options,
208 available_entry_type_names=entry_types.available_entry_type_names, # type: ignore
209 )
210 ),
211]