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

1""" 

2The `rendercv.data.models.design` module contains the data model of the `design` field 

3of the input file. 

4""" 

5 

6import importlib 

7import importlib.util 

8import pathlib 

9from typing import Annotated, Any, Type 

10 

11import pydantic 

12 

13from ...themes import ( 

14 ClassicThemeOptions, 

15 EngineeringresumesThemeOptions, 

16 ModerncvThemeOptions, 

17 Sb2novThemeOptions, 

18) 

19from . import entry_types 

20from .base import RenderCVBaseModelWithoutExtraKeys 

21 

22# ====================================================================================== 

23# Create validator functions: ========================================================== 

24# ====================================================================================== 

25 

26 

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. 

36 

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. 

44 

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"]) 

59 

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 ) 

67 

68 custom_theme_folder = pathlib.Path(theme_name) 

69 

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 ) 

78 

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 

90 

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 ) 

100 

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") 

103 

104 if path_to_init_file.exists(): 

105 spec = importlib.util.spec_from_file_location( 

106 "theme", 

107 path_to_init_file, 

108 ) 

109 

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 ) 

125 

126 ThemeDataModel = getattr( 

127 theme_module, f"{theme_name.capitalize()}ThemeOptions" # type: ignore 

128 ) 

129 

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 

137 

138 theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name) 

139 

140 return theme_data_model 

141 

142 

143# ====================================================================================== 

144# Create custom types: ================================================================= 

145# ====================================================================================== 

146 

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] 

158 

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] 

173 

174 

175available_theme_options = { 

176 "classic": ClassicThemeOptions, 

177 "moderncv": ModerncvThemeOptions, 

178 "sb2nov": Sb2novThemeOptions, 

179 "engineeringresumes": EngineeringresumesThemeOptions, 

180} 

181 

182available_themes = list(available_theme_options.keys())