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