Coverage for src / rendercv / schema / sample_generator.py: 100%
93 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import io
2import json
3import pathlib
4import re
5from typing import overload
7import ruamel.yaml
8import ruamel.yaml.nodes
9import ruamel.yaml.representer
11from rendercv import __version__
12from rendercv.exception import RenderCVUserError
14from .models.cv.cv import Cv
15from .models.design.built_in_design import available_themes, built_in_design_adapter
16from .models.locale.locale import available_locales, locale_adapter
17from .models.rendercv_model import RenderCVModel
18from .rendercv_model_builder import read_yaml
21def comment_out_section_sub_fields(
22 yaml_string: str, *, section_header: str, next_section_header: str
23) -> str:
24 """Comment out YAML sub-fields between a section header and the next section.
26 Why:
27 Sample YAML files show design and locale options as comments so beginners
28 see the structure without being overwhelmed. This splits on exact section
29 boundaries, comments the lines between them, then reassembles.
31 Args:
32 yaml_string: Full YAML string.
33 section_header: The section header line(s) to find (e.g., "design:\\n theme: classic\\n").
34 next_section_header: The next section header that ends this section's sub-fields.
36 Returns:
37 YAML string with sub-fields of the target section commented out.
38 """
39 before_section, _, after_section_header = yaml_string.partition(section_header)
40 sub_fields, _, rest = after_section_header.partition(next_section_header)
42 commented_lines = [
43 f" {line.replace(' ', '# ', 1)}"
44 for line in sub_fields.splitlines(keepends=False)
45 ]
47 return (
48 before_section
49 + section_header
50 + "\n".join(commented_lines)
51 + "\n"
52 + next_section_header
53 + rest
54 )
57def expand_nested_bullets(yaml_string: str) -> str:
58 """Expand inline nested bullets in YAML list items to indented sub-items.
60 Why:
61 YAML dumper renders highlights like ``"Main point - Sub point"`` as a
62 single string. Users expect nested bullets to appear as indented sub-items
63 in the generated sample file for readability.
65 Args:
66 yaml_string: YAML string with potential inline nested bullets.
68 Returns:
69 YAML string with nested bullets expanded to indented sub-items.
70 """
71 return "\n".join(
72 (
73 re.sub(r"(?<! ) - (?! )", "\n - ", line)
74 if re.match(r"\s+- ", line)
75 else line
76 )
77 for line in yaml_string.split("\n")
78 )
81def dictionary_to_yaml(dictionary: dict) -> str:
82 """Convert dictionary to formatted YAML string with multiline preservation.
84 Why:
85 Sample YAML generation must produce readable output with proper
86 formatting for multiline strings. Custom representer ensures
87 bullet points and descriptions use pipe syntax.
89 Args:
90 dictionary: Data structure to convert.
92 Returns:
93 Formatted YAML string.
94 """
96 # Source: https://gist.github.com/alertedsnake/c521bc485b3805aa3839aef29e39f376
97 def str_representer(
98 dumper: ruamel.yaml.representer.Representer, data: str
99 ) -> ruamel.yaml.nodes.ScalarNode:
100 if len(data.splitlines()) > 1: # check for multiline string
101 return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
102 return dumper.represent_scalar("tag:yaml.org,2002:str", data)
104 yaml_object = ruamel.yaml.YAML()
105 yaml_object.encoding = "utf-8"
106 yaml_object.width = 9999
107 yaml_object.indent(mapping=2, sequence=4, offset=2)
108 yaml_object.representer.add_representer(str, str_representer)
110 with io.StringIO() as string_stream:
111 yaml_object.dump(dictionary, string_stream)
112 return string_stream.getvalue()
115def create_sample_rendercv_pydantic_model(
116 *, name: str = "John Doe", theme: str = "classic", locale: str = "english"
117) -> RenderCVModel:
118 """Build sample CV model from sample content.
120 Why:
121 New users need working examples to understand structure. Sample content
122 provides realistic content that validates successfully and renders
123 to all output formats without errors.
125 Args:
126 name: Person's full name.
127 theme: Design theme identifier.
128 locale: Locale language identifier.
130 Returns:
131 Validated model with sample content.
132 """
133 sample_content = pathlib.Path(__file__).parent / "sample_content.yaml"
134 sample_content_dictionary = read_yaml(sample_content)["cv"]
135 cv = Cv(**sample_content_dictionary)
137 cv.name = name
139 design = built_in_design_adapter.validate_python({"theme": theme})
140 validated_locale = locale_adapter.validate_python({"language": locale})
142 return RenderCVModel(cv=cv, design=design, locale=validated_locale)
145@overload
146def create_sample_yaml_input_file(
147 *,
148 file_path: None,
149 name: str = "John Doe",
150 theme: str = "classic",
151 locale: str = "english",
152) -> str: ...
153@overload
154def create_sample_yaml_input_file(
155 *,
156 file_path: pathlib.Path,
157 name: str = "John Doe",
158 theme: str = "classic",
159 locale: str = "english",
160) -> None: ...
161def create_sample_yaml_input_file(
162 *,
163 file_path: pathlib.Path | None = None,
164 name: str = "John Doe",
165 theme: str = "classic",
166 locale: str = "english",
167) -> str | None:
168 """Generate formatted sample YAML with schema hint and commented design options.
170 Why:
171 New command provides users with immediately usable CV templates.
172 JSON schema hint enables IDE autocompletion, commented design
173 fields show customization options without overwhelming beginners.
175 Example:
176 ```py
177 yaml_content = create_sample_yaml_input_file(
178 file_path=pathlib.Path("John_Doe_CV.yaml"), name="John Doe", theme="classic"
179 )
180 # File written with schema hint, cv content, and commented design
181 ```
183 Args:
184 file_path: Optional path to write file.
185 name: Person's full name.
186 theme: Design theme identifier.
187 locale: Language/date format identifier.
189 Returns:
190 YAML string if file_path is None, otherwise None after writing file.
191 """
192 if theme not in available_themes:
193 message = (
194 f"The theme {theme} is not available. The available themes are:"
195 f" {available_themes}"
196 )
197 raise RenderCVUserError(message)
199 if locale not in available_locales:
200 message = (
201 f"The locale {locale} is not available. The available locales are:"
202 f" {available_locales}. \n\nBut you can continue with `English`, and then"
203 " write your own `locale` field in the input file."
204 )
205 raise RenderCVUserError(message)
207 data_model = create_sample_rendercv_pydantic_model(
208 name=name, theme=theme, locale=locale
209 )
211 data_model_as_dictionary = rendercv_model_to_dictionary(data_model)
213 yaml_string = expand_nested_bullets(dictionary_to_yaml(data_model_as_dictionary))
215 # Add a comment to the first line, for JSON Schema:
216 schema_comment = (
217 "# yaml-language-server:"
218 f" $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v{__version__}/schema.json\n"
219 )
220 yaml_string = schema_comment + yaml_string
222 # Comment out design and locale sub-fields so beginners see the structure
223 # without being overwhelmed by configuration options.
224 yaml_string = comment_out_section_sub_fields(
225 yaml_string,
226 section_header=f"design:\n theme: {theme}\n",
227 next_section_header=f"locale:\n language: {locale}\n",
228 )
229 yaml_string = comment_out_section_sub_fields(
230 yaml_string,
231 section_header=f"locale:\n language: {locale}\n",
232 next_section_header="settings:\n",
233 )
235 if file_path is not None:
236 file_path.write_text(yaml_string, encoding="utf-8")
238 return yaml_string
241def rendercv_model_to_dictionary(data_model: RenderCVModel) -> dict:
242 """Convert a RenderCVModel to a plain dictionary via JSON serialization.
244 Why:
245 The YAML library sometimes has problems with the dictionary returned by
246 Pydantic's model_dump(). Converting through JSON first produces clean
247 Python dicts that serialize reliably.
249 Args:
250 data_model: Validated RenderCV model.
252 Returns:
253 Plain dictionary representation of the model.
254 """
255 data_model_as_json = data_model.model_dump_json(
256 exclude_none=False,
257 by_alias=True,
258 )
259 return json.loads(data_model_as_json)
262@overload
263def create_sample_yaml_file(
264 *,
265 dictionary: dict,
266 file_path: None,
267) -> str: ...
268@overload
269def create_sample_yaml_file(
270 *,
271 dictionary: dict,
272 file_path: pathlib.Path,
273) -> None: ...
274def create_sample_yaml_file(
275 *,
276 dictionary: dict,
277 file_path: pathlib.Path | None = None,
278) -> str | None:
279 """Convert a dictionary to formatted YAML and optionally write to file.
281 Why:
282 Multiple sample file generators share the same conversion and
283 file-writing logic. Centralizing it avoids duplication across
284 create_sample_cv_file, create_sample_design_file, etc.
286 Args:
287 dictionary: Data structure to convert to YAML.
288 file_path: Optional path to write file.
290 Returns:
291 YAML string if file_path is None, otherwise None after writing file.
292 """
293 yaml_string = expand_nested_bullets(dictionary_to_yaml(dictionary))
295 if file_path is not None:
296 file_path.write_text(yaml_string, encoding="utf-8")
298 return yaml_string
301@overload
302def create_sample_cv_file(
303 *,
304 file_path: None,
305 name: str = "John Doe",
306) -> str: ...
307@overload
308def create_sample_cv_file(
309 *,
310 file_path: pathlib.Path,
311 name: str = "John Doe",
312) -> None: ...
313def create_sample_cv_file(
314 *,
315 file_path: pathlib.Path | None = None,
316 name: str = "John Doe",
317) -> str | None:
318 """Generate a sample YAML file containing only the CV section.
320 Why:
321 Standalone CV files let users focus on content separately from
322 design, locale, and settings configuration.
324 Args:
325 file_path: Optional path to write file.
326 name: Person's full name.
328 Returns:
329 YAML string if file_path is None, otherwise None after writing file.
330 """
331 data_model = create_sample_rendercv_pydantic_model(name=name)
332 dictionary = rendercv_model_to_dictionary(data_model)
333 return create_sample_yaml_file(
334 dictionary={"cv": dictionary["cv"]}, file_path=file_path
335 )
338@overload
339def create_sample_design_file(
340 *,
341 file_path: None,
342 theme: str = "classic",
343) -> str: ...
344@overload
345def create_sample_design_file(
346 *,
347 file_path: pathlib.Path,
348 theme: str = "classic",
349) -> None: ...
350def create_sample_design_file(
351 *,
352 file_path: pathlib.Path | None = None,
353 theme: str = "classic",
354) -> str | None:
355 """Generate a sample YAML file containing only the design section.
357 Why:
358 Standalone design files let users explore all theme options
359 without mixing in CV content or locale settings.
361 Args:
362 file_path: Optional path to write file.
363 theme: Design theme identifier.
365 Returns:
366 YAML string if file_path is None, otherwise None after writing file.
367 """
368 if theme not in available_themes:
369 message = (
370 f"The theme {theme} is not available. The available themes are:"
371 f" {available_themes}"
372 )
373 raise RenderCVUserError(message)
375 data_model = create_sample_rendercv_pydantic_model(theme=theme)
376 dictionary = rendercv_model_to_dictionary(data_model)
377 return create_sample_yaml_file(
378 dictionary={"design": dictionary["design"]}, file_path=file_path
379 )
382@overload
383def create_sample_locale_file(
384 *,
385 file_path: None,
386 locale: str = "english",
387) -> str: ...
388@overload
389def create_sample_locale_file(
390 *,
391 file_path: pathlib.Path,
392 locale: str = "english",
393) -> None: ...
394def create_sample_locale_file(
395 *,
396 file_path: pathlib.Path | None = None,
397 locale: str = "english",
398) -> str | None:
399 """Generate a sample YAML file containing only the locale section.
401 Why:
402 Standalone locale files let users customize language and date
403 formatting independently from CV content and design.
405 Args:
406 file_path: Optional path to write file.
407 locale: Language/date format identifier.
409 Returns:
410 YAML string if file_path is None, otherwise None after writing file.
411 """
412 if locale not in available_locales:
413 message = (
414 f"The locale {locale} is not available. The available locales are:"
415 f" {available_locales}. \n\nBut you can continue with `English`, and then"
416 " write your own `locale` field in the input file."
417 )
418 raise RenderCVUserError(message)
420 data_model = create_sample_rendercv_pydantic_model(locale=locale)
421 dictionary = rendercv_model_to_dictionary(data_model)
422 return create_sample_yaml_file(
423 dictionary={"locale": dictionary["locale"]}, file_path=file_path
424 )
427@overload
428def create_sample_settings_file(
429 *,
430 file_path: None,
431 omitted_fields: list[str] | None = None,
432) -> str: ...
433@overload
434def create_sample_settings_file(
435 *,
436 file_path: pathlib.Path,
437 omitted_fields: list[str] | None = None,
438) -> None: ...
439def create_sample_settings_file(
440 *,
441 file_path: pathlib.Path | None = None,
442 omitted_fields: list[str] | None = None,
443) -> str | None:
444 """Generate a sample YAML file containing only the settings section.
446 Why:
447 Standalone settings files let users configure render options
448 independently from CV content, design, and locale.
450 Args:
451 file_path: Optional path to write file.
453 Returns:
454 YAML string if file_path is None, otherwise None after writing file.
455 """
456 data_model = create_sample_rendercv_pydantic_model()
457 dictionary = rendercv_model_to_dictionary(data_model)
458 if omitted_fields is not None:
459 for field in omitted_fields:
460 dictionary["settings"].pop(field, None)
461 return create_sample_yaml_file(
462 dictionary={"settings": dictionary["settings"]}, file_path=file_path
463 )