refactor to resolve circular dependencies

This commit is contained in:
Sina Atalay
2024-07-01 18:31:08 +03:00
parent 7a947380d6
commit b5b1593423
17 changed files with 856 additions and 711 deletions

View File

@@ -22,10 +22,10 @@ from .printer import (
warn_if_new_version_is_available,
warning,
welcome,
handle_and_print_raised_exceptions,
)
from .utilities import (
copy_templates,
handle_exceptions,
parse_render_command_override_arguments,
)
@@ -50,7 +50,7 @@ app = typer.Typer(
# allow extra arguments for updating the data model:
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
@handle_exceptions
@handle_and_print_raised_exceptions
def cli_command_render(
input_file_name: Annotated[
str, typer.Argument(help="Name of the YAML input file.")
@@ -316,12 +316,22 @@ def cli_command_new(
theme_folder = copy_templates(theme, pathlib.Path.cwd())
if theme_folder is not None:
created_files_and_folders.append(theme_folder.name)
else:
warning(
f'The theme folder "{theme}" already exists! The theme files are not'
" created."
)
if not dont_create_markdown_source_files:
# copy the package's markdown files to the current directory
markdown_folder = copy_templates("markdown", pathlib.Path.cwd())
if markdown_folder is not None:
created_files_and_folders.append(markdown_folder.name)
else:
warning(
'The "markdown" folder already exists! The Markdown files are not'
" created."
)
if len(created_files_and_folders) > 0:
created_files_and_folders_string = ",\n".join(created_files_and_folders)

View File

@@ -1,87 +0,0 @@
"""
The `rendercv.cli.handlers` contains all the functions that are used to handle all
exceptions that can be raised by `rendercv` during the execution of the CLI.
"""
import functools
from typing import Callable
import jinja2
import pydantic
import ruamel.yaml
import ruamel.yaml.parser
import typer
from .printer import error, print_validation_errors
def handle_exceptions(function: Callable) -> Callable:
"""Return a wrapper function that handles exceptions.
A decorator in Python is a syntactic convenience that allows a Python to interpret
the code below:
```python
@handle_exceptions
def my_function():
pass
```
as
```python
handle_exceptions(my_function)()
```
which is step by step equivalent to
1. Execute `#!python handle_exceptions(my_function)` which will return the
function called `wrapper`.
2. Execute `#!python wrapper()`.
Args:
function (Callable): The function to be wrapped.
Returns:
Callable: The wrapped function.
"""
@functools.wraps(function)
def wrapper(*args, **kwargs):
try:
function(*args, **kwargs)
except pydantic.ValidationError as e:
print_validation_errors(e)
except ruamel.yaml.YAMLError as e:
error(
"There is a YAML error in the input file!\n\nTry to use quotation marks"
" to make sure the YAML parser understands the field is a string.",
e,
)
except FileNotFoundError as e:
error(e)
except UnicodeDecodeError as e:
# find the problematic character that cannot be decoded with utf-8
bad_character = str(e.object[e.start : e.end])
try:
bad_character_context = str(e.object[e.start - 16 : e.end + 16])
except IndexError:
bad_character_context = ""
error(
"The input file contains a character that cannot be decoded with"
f" UTF-8 ({bad_character}):\n {bad_character_context}",
)
except ValueError as e:
error(e)
except typer.Exit:
pass
except jinja2.exceptions.TemplateSyntaxError as e:
error(
f"There is a problem with the template ({e.filename}) at line"
f" {e.lineno}!",
e,
)
except RuntimeError as e:
error(e)
return wrapper

View File

@@ -9,6 +9,7 @@ import pydantic
import rich
import typer
from rich import print
import rich.live
from .. import __version__
from .utilities import (
@@ -16,6 +17,15 @@ from .utilities import (
get_latest_version_number_from_pypi,
)
import functools
from typing import Callable
import jinja2
import pydantic
import ruamel.yaml
import ruamel.yaml.parser
import typer
class LiveProgressReporter(rich.live.Live):
"""This class is a wrapper around `rich.live.Live` that provides the live progress
@@ -337,3 +347,75 @@ def print_validation_errors(exception: pydantic.ValidationError):
print(table)
error() # exit the program
def handle_and_print_raised_exceptions(function: Callable) -> Callable:
"""Return a wrapper function that handles exceptions.
A decorator in Python is a syntactic convenience that allows a Python to interpret
the code below:
```python
@handle_exceptions
def my_function():
pass
```
as
```python
handle_exceptions(my_function)()
```
which is step by step equivalent to
1. Execute `#!python handle_exceptions(my_function)` which will return the
function called `wrapper`.
2. Execute `#!python wrapper()`.
Args:
function (Callable): The function to be wrapped.
Returns:
Callable: The wrapped function.
"""
@functools.wraps(function)
def wrapper(*args, **kwargs):
try:
function(*args, **kwargs)
except pydantic.ValidationError as e:
print_validation_errors(e)
except ruamel.yaml.YAMLError as e:
error(
"There is a YAML error in the input file!\n\nTry to use quotation marks"
" to make sure the YAML parser understands the field is a string.",
e,
)
except FileNotFoundError as e:
error(e)
except UnicodeDecodeError as e:
# find the problematic character that cannot be decoded with utf-8
bad_character = str(e.object[e.start : e.end])
try:
bad_character_context = str(e.object[e.start - 16 : e.end + 16])
except IndexError:
bad_character_context = ""
error(
"The input file contains a character that cannot be decoded with"
f" UTF-8 ({bad_character}):\n {bad_character_context}",
)
except ValueError as e:
error(e)
except typer.Exit:
pass
except jinja2.exceptions.TemplateSyntaxError as e:
error(
f"There is a problem with the template ({e.filename}) at line"
f" {e.lineno}!",
e,
)
except RuntimeError as e:
error(e)
return wrapper

View File

@@ -11,8 +11,6 @@ from typing import Optional
import typer
from .printer import error, warning
def get_latest_version_number_from_pypi() -> Optional[str]:
"""Get the latest version number of RenderCV from PyPI.
@@ -74,7 +72,6 @@ def copy_templates(
folder_name: str,
copy_to: pathlib.Path,
new_folder_name: Optional[str] = None,
suppress_warning: bool = False,
) -> Optional[pathlib.Path]:
"""Copy one of the folders found in `rendercv.templates` to `copy_to`.
@@ -92,18 +89,6 @@ def copy_templates(
destination = copy_to / folder_name
if destination.exists():
if not suppress_warning:
if folder_name != "markdown":
warning(
f'The theme folder "{folder_name}" already exists! New theme files'
" are not created."
)
else:
warning(
'The folder "markdown" already exists! New Markdown files are not'
" created."
)
return None
else:
# copy the folder but don't include __init__.py:
@@ -138,7 +123,7 @@ def parse_render_command_override_arguments(
# below parses `ctx.args` accordingly.
if len(extra_arguments.args) % 2 != 0:
error(
raise ValueError(
"There is a problem with the extra arguments! Each key should have"
" a corresponding value."
)
@@ -147,7 +132,7 @@ def parse_render_command_override_arguments(
key = extra_arguments.args[i]
value = extra_arguments.args[i + 1]
if not key.startswith("--"):
error(f"The key ({key}) should start with double dashes!")
raise ValueError(f"The key ({key}) should start with double dashes!")
key = key.replace("--", "")

View File

@@ -33,10 +33,20 @@ from .generators import (
get_a_sample_data_model,
)
from .types import (
from .field_types import (
available_social_networks,
)
from .model_types import (
available_entry_type_names,
available_themes,
available_social_networks,
Entry,
SectionInput,
)
from .computed_fields import (
format_date,
)
from .utilities import set_or_update_a_value, dictionary_to_yaml

View File

@@ -5,11 +5,48 @@ calculate the time span between two dates, the date string, the URL of a social
etc.
"""
from datetime import date as Date
from typing import Optional
from . import models
# from .models import locale_catalog, CurriculumVitae
from . import utilities as util
from . import validators as val
# from . import validators as val
def format_date(date: Date, use_full_name: bool = False) -> str:
"""Formats a `Date` object to a string in the following format: "Jan 2021". The
month names are taken from the `locale_catalog` dictionary from the
`rendercv.data_models.models` module.
Example:
```python
format_date(Date(2024, 5, 1))
```
will return
`#!python "May 2024"`
Args:
date (Date): The date to format.
use_full_name (bool, optional): If `True`, the full name of the month will be
used. Defaults to `False`.
Returns:
str: The formatted date.
"""
if use_full_name:
month_names = locale_catalog["full_names_of_months"]
else:
month_names = locale_catalog["abbreviations_for_months"]
month = int(date.strftime("%m"))
month_abbreviation = month_names[month - 1]
year = date.strftime(format="%Y")
date_string = f"{month_abbreviation} {year}"
return date_string
def compute_time_span_string(
@@ -80,18 +117,16 @@ def compute_time_span_string(
if how_many_years == 0:
how_many_years_string = None
elif how_many_years == 1:
how_many_years_string = f"1 {models.locale_catalog['year']}"
how_many_years_string = f"1 {locale_catalog['year']}"
else:
how_many_years_string = f"{how_many_years} {models.locale_catalog['years']}"
how_many_years_string = f"{how_many_years} {locale_catalog['years']}"
# Calculate the number of months between start_date and end_date:
how_many_months = round((timespan_in_days % 365) / 30)
if how_many_months <= 1:
how_many_months_string = f"1 {models.locale_catalog['month']}"
how_many_months_string = f"1 {locale_catalog['month']}"
else:
how_many_months_string = (
f"{how_many_months} {models.locale_catalog['months']}"
)
how_many_months_string = f"{how_many_months} {locale_catalog['months']}"
# Combine howManyYearsString and howManyMonthsString:
if how_many_years_string is None:
@@ -145,7 +180,7 @@ def compute_date_string(
if show_only_years:
date_string = str(date_object.year)
else:
date_string = util.format_date(date_object)
date_string = format_date(date_object)
except ValueError:
# Then it is a custom date string (e.g., "My Custom Date")
date_string = str(date)
@@ -159,10 +194,10 @@ def compute_date_string(
if show_only_years:
start_date = date_object.year
else:
start_date = util.format_date(date_object)
start_date = format_date(date_object)
if end_date == "present":
end_date = models.locale_catalog["present"]
end_date = locale_catalog["present"]
elif isinstance(end_date, int):
# Then it means only the year is provided
end_date = str(end_date)
@@ -172,9 +207,9 @@ def compute_date_string(
if show_only_years:
end_date = date_object.year
else:
end_date = util.format_date(date_object)
end_date = format_date(date_object)
date_string = f"{start_date} {models.locale_catalog['to']} {end_date}"
date_string = f"{start_date} {locale_catalog['to']} {end_date}"
else:
# Neither date, start_date, nor end_date are provided, so return an empty
@@ -214,7 +249,7 @@ def compute_social_network_url(network: str, username: str):
return url
def compute_connections(cv: models.CurriculumVitae) -> list[dict[str, str]]:
def compute_connections(cv) -> list[dict[str, str]]:
"""Bring together all the connections in the CV, such as social networks, phone
number, email, etc and return them as a list of dictionaries. Each dictionary
contains the following keys: "latex_icon", "url", "clean_url", and "placeholder."
@@ -304,36 +339,36 @@ def compute_connections(cv: models.CurriculumVitae) -> list[dict[str, str]]:
return connections
def compute_sections(
sections_input: Optional[dict[str, models.SectionInput]],
) -> list[models.SectionBase]:
"""Compute the sections of the CV based on the input sections.
# def compute_sections(
# sections_input: Optional[dict[str, models.SectionInput]],
# ) -> list[models.SectionBase]:
# """Compute the sections of the CV based on the input sections.
The original `sections` input is a dictionary where the keys are the section titles
and the values are the list of entries in that section. This function converts the
input sections to a list of `SectionBase` objects. This makes it easier to work with
the sections in the rest of the code.
# The original `sections` input is a dictionary where the keys are the section titles
# and the values are the list of entries in that section. This function converts the
# input sections to a list of `SectionBase` objects. This makes it easier to work with
# the sections in the rest of the code.
Args:
sections_input (Optional[dict[str, SectionInput]]): The input sections.
Returns:
list[SectionBase]: The computed sections.
"""
sections: list[models.SectionBase] = []
# Args:
# sections_input (Optional[dict[str, SectionInput]]): The input sections.
# Returns:
# list[SectionBase]: The computed sections.
# """
# sections: list[models.SectionBase] = []
if sections_input is not None:
for title, section_or_entries in sections_input.items():
title = util.dictionary_key_to_proper_section_title(title)
# if sections_input is not None:
# for title, section_or_entries in sections_input.items():
# title = util.dictionary_key_to_proper_section_title(title)
entry_type_name = val.validate_an_entry_type_and_get_entry_type_name(
section_or_entries[0]
)
# entry_type_name = val.validate_an_entry_type_and_get_entry_type_name(
# section_or_entries[0]
# )
section = models.SectionBase(
title=title,
entry_type=entry_type_name,
entries=section_or_entries,
)
sections.append(section)
# section = models.SectionBase(
# title=title,
# entry_type=entry_type_name,
# entries=section_or_entries,
# )
# sections.append(section)
return sections
# return sections

View File

@@ -0,0 +1,64 @@
"""
The `rendercv.data_models.types` module contains all the custom types created for
RenderCV, and it contains important information about the package, such as
`available_themes`, `available_social_networks`, etc.
"""
from typing import Annotated, Literal, Optional, get_args
import pydantic
from . import field_validators as field_val
# See https://docs.pydantic.dev/2.7/concepts/types/#custom-types and
# https://docs.pydantic.dev/2.7/concepts/validators/#annotated-validators
# for more information about custom types.
# Create custom types for dates:
# ExactDate that accepts only strings in YYYY-MM-DD or YYYY-MM format:
ExactDate = Annotated[
str,
pydantic.Field(
pattern=r"\d{4}-\d{2}(-\d{2})?",
),
]
# ArbitraryDate that accepts either an integer or a string, but it is validated with
# `validate_date_field` function:
ArbitraryDate = Annotated[
Optional[int | str],
pydantic.BeforeValidator(field_val.validate_date_field),
]
# StartDate that accepts either an integer or an ExactDate, but it is validated with
# `validate_start_and_end_date_fields` function:
StartDate = Annotated[
Optional[int | ExactDate],
pydantic.BeforeValidator(field_val.validate_start_and_end_date_fields),
]
# EndDate that accepts either an integer, the string "present", or an ExactDate, but it
# is validated with `validate_start_and_end_date_fields` function:
EndDate = Annotated[
Optional[Literal["present"] | int | ExactDate],
pydantic.BeforeValidator(field_val.validate_start_and_end_date_fields),
]
# Create a custom type named SocialNetworkName:
SocialNetworkName = Literal[
"LinkedIn",
"GitHub",
"GitLab",
"Instagram",
"ORCID",
"Mastodon",
"StackOverflow",
"ResearchGate",
"YouTube",
"Google Scholar",
]
# ======================================================================================
# Create variables that show the available stuff: ======================================
# ======================================================================================
available_social_networks = get_args(SocialNetworkName)

View File

@@ -0,0 +1,115 @@
"""
The `rendercv.data_models.validators` module contains all the functions used to validate
the data models of RenderCV, in addition to Pydantic inner validation.
"""
import re
from datetime import date as Date
from typing import Optional
import pydantic
from . import utilities as util
# Create a URL validator:
url_validator = pydantic.TypeAdapter(pydantic.HttpUrl)
def validate_url(url: str) -> str:
"""Validate a URL.
Args:
url (str): The URL to validate.
Returns:
str: The validated URL.
"""
url_validator.validate_strings(url)
return url
def validate_date_field(date: Optional[int | str]) -> Optional[int | str]:
"""Check if the `date` field is provided correctly.
Args:
date (Optional[int | str]): The date to validate.
Returns:
Optional[int | str]: The validated date.
"""
date_is_provided = date is not None
if date_is_provided:
if isinstance(date, str):
if re.fullmatch(r"\d{4}-\d{2}(-\d{2})?", date):
# Then it is in YYYY-MM-DD or YYYY-MMY format
# Check if it is a valid date:
util.get_date_object(date)
elif re.fullmatch(r"\d{4}", date):
# Then it is in YYYY format, so, convert it to an integer:
# This is not required for start_date and end_date because they
# can't be casted into a general string. For date, this needs to
# be done manually, because it can be a general string.
date = int(date)
elif isinstance(date, Date):
# Pydantic parses YYYY-MM-DD dates as datetime.date objects. We need to
# convert them to strings because that is how RenderCV uses them.
date = date.isoformat()
return date
def validate_start_and_end_date_fields(
date: str | Date,
) -> str:
"""Check if the `start_date` and `end_date` fields are provided correctly.
Args:
date (Optional[Literal["present"] | int | RenderCVDate]): The date to validate.
Returns:
Optional[Literal["present"] | int | RenderCVDate]: The validated date.
"""
date_is_provided = date is not None
if date_is_provided:
if isinstance(date, Date):
# Pydantic parses YYYY-MM-DD dates as datetime.date objects. We need to
# convert them to strings because that is how RenderCV uses them.
date = date.isoformat()
elif date != "present":
# Validate the date:
util.get_date_object(date)
return date
def validate_a_social_network_username(username: str, network: str) -> str:
"""Check if the `username` field in the `SocialNetwork` model is provided correctly.
Args:
username (str): The username to validate.
Returns:
str: The validated username.
"""
if network == "Mastodon":
mastodon_username_pattern = r"@[^@]+@[^@]+"
if not re.fullmatch(mastodon_username_pattern, username):
raise ValueError(
'Mastodon username should be in the format "@username@domain"!'
)
if network == "StackOverflow":
stackoverflow_username_pattern = r"\d+\/[^\/]+"
if not re.fullmatch(stackoverflow_username_pattern, username):
raise ValueError(
'StackOverflow username should be in the format "user_id/username"!'
)
if network == "YouTube":
if username.startswith("@"):
raise ValueError(
'YouTube username should not start with "@"! Remove "@" from the'
" beginning of the username."
)
return username

View File

@@ -11,9 +11,12 @@ from typing import Any, Optional
import pydantic
from ..themes.classic import ClassicThemeOptions
from ..themes.sb2nov import Sb2novThemeOptions
from ..themes.moderncv import ModerncvThemeOptions
from ..themes.engineeringresumes import EngineeringresumesThemeOptions
from . import models
from . import utilities as utils
from . import validators as val
from . import field_validators as field_val
def get_a_sample_data_model(
@@ -269,10 +272,14 @@ def get_a_sample_data_model(
sections=sections, # type: ignore
)
if theme == "classic":
design = ClassicThemeOptions(theme="classic", show_timespan_in=["Experience"])
else:
design = val.rendercv_design_validator.validate_python({"theme": theme})
themes = {
"classic": ClassicThemeOptions,
"moderncv": ModerncvThemeOptions,
"sb2nov": Sb2novThemeOptions,
"engineeringresumes": EngineeringresumesThemeOptions,
}
design = themes[theme](theme=theme)
return models.RenderCVDataModel(cv=cv, design=design)

View File

@@ -0,0 +1,93 @@
from typing import Annotated, Any
import pydantic
from ..themes.classic import ClassicThemeOptions
from ..themes.engineeringresumes import EngineeringresumesThemeOptions
from ..themes.moderncv import ModerncvThemeOptions
from ..themes.sb2nov import Sb2novThemeOptions
from .models import (
OneLineEntry,
NormalEntry,
ExperienceEntry,
EducationEntry,
PublicationEntry,
BulletEntry,
)
from . import model_validators
# See https://docs.pydantic.dev/2.7/concepts/types/#custom-types and
# https://docs.pydantic.dev/2.7/concepts/validators/#annotated-validators
# for more information about custom types.
# Create a custom type named Entry:
Entry = (
OneLineEntry
| NormalEntry
| ExperienceEntry
| EducationEntry
| PublicationEntry
| BulletEntry
| str
)
# Entry.__args__[:-1] is a tuple of all the entry types except str:
available_entry_types = Entry.__args__[:-1]
available_entry_type_names = [
entry_type.__name__ for entry_type in available_entry_types
] + ["TextEntry"]
# Create a custom type named ListOfEntries:
ListOfEntries = list[Entry]
# Create a custom type named SectionInput so that it can be validated with
# `validate_a_section` function.
SectionInput = Annotated[
ListOfEntries,
pydantic.PlainValidator(
lambda entries: model_validators.validate_a_section(
entries, entry_types=available_entry_types
)
),
]
# Create a custom type named RenderCVBuiltinDesign:
# It is a union of all the design options and the correct design option is determined by
# the theme field, thanks to Pydantic's discriminator feature.
# See https://docs.pydantic.dev/2.7/concepts/fields/#discriminator for more information
RenderCVBuiltinDesign = Annotated[
ClassicThemeOptions
| ModerncvThemeOptions
| Sb2novThemeOptions
| EngineeringresumesThemeOptions,
pydantic.Field(discriminator="theme"),
]
available_theme_options = {
"classic": ClassicThemeOptions,
"moderncv": ModerncvThemeOptions,
"sb2nov": Sb2novThemeOptions,
"engineeringresumes": EngineeringresumesThemeOptions,
}
available_themes = list(available_theme_options.keys())
# Create a custom type named RenderCVDesign:
# RenderCV supports custom themes as well. Therefore, `Any` type is used to allow custom
# themes. However, the JSON Schema generation is skipped, otherwise, the JSON Schema
# would accept any `design` field in the YAML input file.
RenderCVDesign = Annotated[
RenderCVBuiltinDesign | pydantic.json_schema.SkipJsonSchema[Any],
pydantic.PlainValidator(
lambda design: model_validators.validate_design_options(
design,
available_theme_options=available_theme_options,
available_entry_type_names=available_entry_type_names,
)
),
]

View File

@@ -0,0 +1,365 @@
from typing import Optional, Any, Type, Literal
import pathlib
import pydantic
import importlib
import importlib.util
# from .types import (
# available_entry_types,
# available_theme_options,
# available_themes,
# available_entry_type_names,
# # RenderCVBuiltinDesign,
# )
from . import utilities as util
from . import field_types
from .models import RenderCVBaseModel
class SectionBase(RenderCVBaseModel):
"""This class is the parent class of all the section types. It is being used
in RenderCV internally, and it is not meant to be used directly by the users.
It is used by `rendercv.data_models.utilities.create_a_section_model` function to
create a section model based on any entry type.
"""
title: str
entry_type: str
entries: list[Any]
def create_a_section_validator(entry_type: Type) -> Type[SectionBase]:
"""Create a section model based on the entry type. See [Pydantic's documentation
about dynamic model
creation](https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation)
for more information.
The section model is used to validate a section.
Args:
entry_type (Type[Entry]): The entry type to create the section model. It's not
an instance of the entry type, but the entry type itself.
Returns:
Type[SectionBase]: The section model.
"""
if entry_type == str:
model_name = "SectionWithTextEntries"
entry_type_name = "TextEntry"
else:
model_name = "SectionWith" + entry_type.__name__.replace("Entry", "Entries")
entry_type_name = entry_type.__name__
SectionModel = pydantic.create_model(
model_name,
entry_type=(Literal[entry_type_name], ...), # type: ignore
entries=(list[entry_type], ...),
__base__=SectionBase,
)
return SectionModel
def validate_and_adjust_dates(
start_date: field_types.StartDate,
end_date: field_types.EndDate,
date: Optional[field_types.ArbitraryDate],
) -> tuple[field_types.StartDate, field_types.EndDate, field_types.ArbitraryDate]:
"""Check if the dates are provided correctly and make the necessary adjustments.
Args:
entry (EntryBase): The entry to validate its dates.
Returns:
EntryBase: The validated
"""
date_is_provided = date is not None
start_date_is_provided = start_date is not None
end_date_is_provided = end_date is not None
if date_is_provided:
# If only date is provided, ignore start_date and end_date:
start_date = None
end_date = None
elif not start_date_is_provided and end_date_is_provided:
# If only end_date is provided, assume it is a one-day event and act like
# only the date is provided:
date = end_date
start_date = None
end_date = None
elif start_date_is_provided:
start_date = util.get_date_object(start_date)
if not end_date_is_provided:
# If only start_date is provided, assume it is an ongoing event, i.e.,
# the end_date is present:
end_date = "present"
if end_date != "present":
end_date = util.get_date_object(end_date)
if start_date > end_date:
raise ValueError(
'"start_date" can not be after "end_date"!',
"start_date", # This is the location of the error
str(start_date), # This is value of the error
)
return start_date, end_date, date
def get_characteristic_entry_attributes(
entry_types: list[Type],
) -> dict[Type, set[str]]:
"""Get the characteristic attributes of the entry types.
Args:
entry_types (list[Type]): The entry types to get their characteristic
attributes.
Returns:
dict[Type, list[str]]: The characteristic attributes of the entry types.
"""
# Look at all the entry types, collect their attributes with
# EntryType.model_fields.keys() and find the common ones.
all_attributes = []
for EntryType in entry_types:
all_attributes.extend(EntryType.model_fields.keys())
common_attributes = set(
attribute for attribute in all_attributes if all_attributes.count(attribute) > 1
)
# Store each entry type's characteristic attributes in a dictionary:
characteristic_entry_attributes = {}
for EntryType in entry_types:
characteristic_entry_attributes[EntryType] = (
set(EntryType.model_fields.keys()) - common_attributes
)
return characteristic_entry_attributes
def get_entry_type_name_and_section_validator(
entry: dict[str, Any] | str, entry_types: list[Type]
) -> tuple[str, Type[SectionBase]]:
"""Get the entry type name and the section validator based on the entry.
It takes an entry (as a dictionary or a string) and a list of entry types. Then
it determines the entry type and creates a section validator based on the entry
type.
Args:
entry (dict[str, Any] | str): The entry to determine its type.
entry_types (list[Type]): The entry types to determine the entry type. These
are not instances of the entry types, but the entry types themselves. `str`
type should not be included in this list.
Returns:
tuple[str, Type[SectionBase]]: The entry type name and the section validator.
"""
characteristic_entry_attributes = get_characteristic_entry_attributes(entry_types)
if isinstance(entry, dict):
entry_type_name = None # the entry type is not determined yet
for (
EntryType,
characteristic_attributes,
) in characteristic_entry_attributes.items():
# If at least one of the characteristic_entry_attributes is in the entry,
# then it means the entry is of this type:
if characteristic_attributes & set(entry.keys()):
entry_type_name = EntryType.__name__
section_type = create_a_section_validator(EntryType)
break
if entry_type_name is None:
raise ValueError("The entry is not provided correctly.")
elif isinstance(entry, str):
# Then it is a TextEntry
entry_type_name = "TextEntry"
section_type = create_a_section_validator(str)
else:
# Then the entry is already initialized with a data model:
entry_type_name = entry.__class__.__name__
section_type = create_a_section_validator(entry.__class__)
return entry_type_name, section_type
def validate_a_section(sections_input: list[Any], entry_types: list[Type]) -> list[Any]:
"""Validate a list of entries (a section) based on the entry types.
Sections input is a list of entries. Since there are multiple entry types, it is not
possible to validate it directly. Firstly, the entry type is determined with the
`get_entry_type_name_and_section_validator` function. If the entry type cannot be
determined, an error is raised. If the entry type is determined, the rest of the
list is validated with the section validator.
Args:
sections_input (list[Any]): The sections input to validate.
entry_types (list[Type]): The entry types to determine the entry type. These
are not instances of the entry types, but the entry types themselves. `str`
type should not be included in this list.
Returns:
list[Any]: The validated sections input.
"""
if isinstance(sections_input, list):
# Find the entry type based on the first identifiable entry:
entry_type_name = None
section_type = None
for entry in sections_input:
try:
entry_type_name, section_type = (
get_entry_type_name_and_section_validator(entry, entry_types)
)
break
except ValueError:
# If the entry type cannot be determined, try the next entry:
pass
if entry_type_name is None or section_type is None:
raise ValueError(
"RenderCV couldn't match this section with any entry types! Please"
" check the entries and make sure they are provided correctly.",
"", # This is the location of the error
"", # This is value of the error
)
section = {
"title": "Test Section",
"entry_type": entry_type_name,
"entries": sections_input,
}
try:
section_type.model_validate(
section,
)
except pydantic.ValidationError as e:
new_error = ValueError(
"There are problems with the entries. RenderCV detected the entry type"
f" of this section to be {entry_type_name}! The problems are shown"
" below.",
"", # This is the location of the error
"", # This is value of the error
)
raise new_error from e
return sections_input
def validate_design_options(
design: Any,
available_theme_options: dict[str, Type],
available_entry_type_names: list[str],
) -> Any:
"""Chech if the design options are for a built-in theme or a custom theme. If it is
a built-in theme, validate it with the corresponding data model. If it is a custom
theme, check if the necessary files are provided and validate it with the custom
theme data model, found in the `__init__.py` file of the custom theme folder.
Args:
design (Any | RenderCVBuiltinDesign): The design options to validate.
available_theme_options (dict[str, Type]): The available theme options. The keys
are the theme names and the values are the corresponding data models.
available_entry_type_names (list[str]): The available entry type names. These
are used to validate if all the templates are provided in the custom theme
folder.
Returns:
Any: The validated design as a Pydantic data model.
"""
if isinstance(design, available_theme_options):
# Then it means it is an already validated built-in theme. Return it as it is:
return design
elif design["theme"] in available_theme_options:
# Then it is a built-in theme, but it is not validated yet. Validate it and
# return it:
ThemeDataModel = available_theme_options[design["theme"]]
return ThemeDataModel(**design)
else:
# It is a custom theme. Validate it:
theme_name: str = str(design["theme"])
# Check if the theme name is valid:
if not theme_name.isalpha():
raise ValueError(
"The custom theme name should contain only letters.",
"theme", # this is the location of the error
theme_name, # this is value of the error
)
custom_theme_folder = pathlib.Path(theme_name)
# Check if the custom theme folder exists:
if not custom_theme_folder.exists():
raise ValueError(
f"The custom theme folder `{custom_theme_folder}` does not exist."
" It should be in the working directory as the input file.",
"", # this is the location of the error
theme_name, # this is value of the error
)
# check if all the necessary files are provided in the custom theme folder:
required_entry_files = [
entry_type_name + ".j2.tex"
for entry_type_name in available_entry_type_names
]
required_files = [
"SectionBeginning.j2.tex", # section beginning template
"SectionEnding.j2.tex", # section ending template
"Preamble.j2.tex", # preamble template
"Header.j2.tex", # header template
] + required_entry_files
for file in required_files:
file_path = custom_theme_folder / file
if not file_path.exists():
raise ValueError(
f"You provided a custom theme, but the file `{file}` is not"
f" found in the folder `{custom_theme_folder}`.",
"", # This is the location of the error
theme_name, # This is value of the error
)
# Import __init__.py file from the custom theme folder if it exists:
path_to_init_file = pathlib.Path(f"{theme_name}/__init__.py")
if path_to_init_file.exists():
spec = importlib.util.spec_from_file_location(
"theme",
path_to_init_file,
)
theme_module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(theme_module) # type: ignore
except SyntaxError:
raise ValueError(
f"The custom theme {theme_name}'s __init__.py file has a syntax"
" error. Please fix it.",
)
except ImportError:
raise ValueError(
f"The custom theme {theme_name}'s __init__.py file has an"
" import error. If you have copy-pasted RenderCV's built-in"
" themes, make sure to update the import statements (e.g.,"
' "from . import" to "from rendercv.themes import").',
)
ThemeDataModel = getattr(
theme_module, f"{theme_name.capitalize()}ThemeOptions" # type: ignore
)
# Initialize and validate the custom theme data model:
theme_data_model = ThemeDataModel(**design)
else:
# Then it means there is no __init__.py file in the custom theme folder.
# Create a dummy data model and use that instead.
class ThemeOptionsAreNotProvided(RenderCVBaseModel):
theme: str = theme_name
theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
return theme_data_model

View File

@@ -14,9 +14,9 @@ import pydantic_extra_types.phone_numbers as pydantic_phone_numbers
from ..themes.classic import ClassicThemeOptions
from . import computed_fields as cf
from . import types
from . import field_types
from . import utilities as util
from . import validators as val
from . import field_validators
# Disable Pydantic warnings:
# warnings.filterwarnings("ignore")
@@ -63,7 +63,7 @@ class EntryWithDate(RenderCVBaseModel):
fields.
"""
date: types.ArbitraryDate = pydantic.Field(
date: field_types.ArbitraryDate = pydantic.Field(
default=None,
title="Date",
description=(
@@ -165,7 +165,7 @@ class EntryBase(EntryWithDate):
description="The location of the event.",
examples=["Istanbul, Türkiye"],
)
start_date: types.StartDate = pydantic.Field(
start_date: field_types.StartDate = pydantic.Field(
default=None,
title="Start Date",
description=(
@@ -173,7 +173,7 @@ class EntryBase(EntryWithDate):
),
examples=["2020-09-24"],
)
end_date: types.EndDate = pydantic.Field(
end_date: field_types.EndDate = pydantic.Field(
default=None,
title="End Date",
description=(
@@ -195,7 +195,12 @@ class EntryBase(EntryWithDate):
"""Call the `validate_adjust_dates_of_an_entry` function to validate the
dates.
"""
return val.validate_and_adjust_dates_of_an_entry(self)
self.start_date, self.end_date, self.date = (
model_validators.validate_and_adjust_dates(
start_date=self.start_date, end_date=self.end_date, date=self.date
)
)
return self
@functools.cached_property
def date_string(self) -> str:
@@ -301,27 +306,17 @@ class EducationEntry(EntryBase, EducationEntryBase):
# ======================================================================================
class SectionBase(RenderCVBaseModel):
"""This class is the parent class of all the section types. It is being used
in RenderCV internally, and it is not meant to be used directly by the users.
It is used by `rendercv.data_models.utilities.create_a_section_model` function to
create a section model based on any entry type.
"""
title: str
entry_type: str
entries: types.ListOfEntries
# ======================================================================================
# Full RenderCV data models: ===========================================================
# ======================================================================================
from . import model_types
from . import model_validators
class SocialNetwork(RenderCVBaseModel):
"""This class is the data model of a social network."""
network: types.SocialNetworkName = pydantic.Field(
network: field_types.SocialNetworkName = pydantic.Field(
title="Social Network",
description="Name of the social network.",
)
@@ -342,7 +337,9 @@ class SocialNetwork(RenderCVBaseModel):
network = info.data["network"]
username = val.validate_a_social_network_username(username, network)
username = field_validators.validate_a_social_network_username(
username, network
)
return username
@@ -352,7 +349,7 @@ class SocialNetwork(RenderCVBaseModel):
if self.network == "Mastodon":
# All the other social networks have valid URLs. Mastodon URLs contain both
# the username and the domain. So, we need to validate if the url is valid.
val.validate_url(self.url)
field_validators.validate_url(self.url)
return self
@@ -402,7 +399,7 @@ class CurriculumVitae(RenderCVBaseModel):
title="Social Networks",
description="The social networks of the person.",
)
sections_input: Optional[dict[str, types.SectionInput]] = pydantic.Field(
sections_input: Optional[dict[str, model_types.SectionInput]] = pydantic.Field(
default=None,
title="Sections",
description="The sections of the CV.",
@@ -421,7 +418,7 @@ class CurriculumVitae(RenderCVBaseModel):
return connections
@functools.cached_property
def sections(self) -> list[SectionBase]:
def sections(self) -> list[model_validators.SectionBase]:
"""Return all the sections of the CV with their titles as a list of
`SectionBase` instances and cache `sections` as an attribute of the instance.
"""
@@ -551,7 +548,7 @@ class RenderCVDataModel(RenderCVBaseModel):
title="Curriculum Vitae",
description="The data of the CV.",
)
design: types.RenderCVDesign = pydantic.Field(
design: model_types.RenderCVDesign = pydantic.Field(
default=ClassicThemeOptions(theme="classic"),
title="Design",
description=(

View File

@@ -1,123 +0,0 @@
"""
The `rendercv.data_models.types` module contains all the custom types created for
RenderCV, and it contains important information about the package, such as
`available_themes`, `available_social_networks`, etc.
"""
from typing import Annotated, Any, Literal, Optional, get_args
import pydantic
from ..themes.classic import ClassicThemeOptions
from ..themes.engineeringresumes import EngineeringresumesThemeOptions
from ..themes.moderncv import ModerncvThemeOptions
from ..themes.sb2nov import Sb2novThemeOptions
from . import models
from . import validators as val
# See https://docs.pydantic.dev/2.7/concepts/types/#custom-types and
# https://docs.pydantic.dev/2.7/concepts/validators/#annotated-validators
# for more information about custom types.
# Create custom types for dates:
# ExactDate that accepts only strings in YYYY-MM-DD or YYYY-MM format:
ExactDate = Annotated[
str,
pydantic.Field(
pattern=r"\d{4}-\d{2}(-\d{2})?",
),
]
# ArbitraryDate that accepts either an integer or a string, but it is validated with
# `validate_date_field` function:
ArbitraryDate = Annotated[
Optional[int | str],
pydantic.BeforeValidator(val.validate_date_field),
]
# StartDate that accepts either an integer or an ExactDate, but it is validated with
# `validate_start_and_end_date_fields` function:
StartDate = Annotated[
Optional[int | ExactDate],
pydantic.BeforeValidator(val.validate_start_and_end_date_fields),
]
# EndDate that accepts either an integer, the string "present", or an ExactDate, but it
# is validated with `validate_start_and_end_date_fields` function:
EndDate = Annotated[
Optional[Literal["present"] | int | ExactDate],
pydantic.BeforeValidator(val.validate_start_and_end_date_fields),
]
# Create a custom type named RenderCVBuiltinDesign:
# It is a union of all the design options and the correct design option is determined by
# the theme field, thanks to Pydantic's discriminator feature.
# See https://docs.pydantic.dev/2.7/concepts/fields/#discriminator for more information
RenderCVBuiltinDesign = Annotated[
ClassicThemeOptions
| ModerncvThemeOptions
| Sb2novThemeOptions
| EngineeringresumesThemeOptions,
pydantic.Field(discriminator="theme"),
]
# Create a custom type named RenderCVDesign:
# RenderCV supports custom themes as well. Therefore, `Any` type is used to allow custom
# themes. However, the JSON Schema generation is skipped, otherwise, the JSON Schema
# would accept any `design` field in the YAML input file.
RenderCVDesign = Annotated[
pydantic.json_schema.SkipJsonSchema[Any] | RenderCVBuiltinDesign,
pydantic.BeforeValidator(val.validate_a_custom_theme),
]
# Create a custom type named Entry:
Entry = (
models.OneLineEntry
| models.NormalEntry
| models.ExperienceEntry
| models.EducationEntry
| models.PublicationEntry
| models.BulletEntry
| str
)
# Create a custom type named ListOfEntries:
ListOfEntries = list[Entry]
# Create a custom type named SectionInput so that it can be validated with
# `validate_section_input` function.
SectionInput = Annotated[
ListOfEntries,
pydantic.BeforeValidator(val.validate_a_section),
]
# Create a custom type named SocialNetworkName:
SocialNetworkName = Literal[
"LinkedIn",
"GitHub",
"GitLab",
"Instagram",
"ORCID",
"Mastodon",
"StackOverflow",
"ResearchGate",
"YouTube",
"Google Scholar",
]
# ======================================================================================
# Create variables that show the available stuff: ======================================
# ======================================================================================
available_social_networks = get_args(SocialNetworkName)
# Entry.__args__[:-1] is a tuple of all the entry types except str:
available_entry_types = Entry.__args__[:-1]
available_entry_type_names = [
entry_type.__name__ for entry_type in available_entry_types
] + ["TextEntry"]
available_theme_options = get_args(RenderCVBuiltinDesign)[0]
available_themes = ["classic", "moderncv", "sb2nov", "engineeringresumes"]

View File

@@ -62,46 +62,6 @@ def get_date_object(date: str | int) -> Date:
return date_object
def format_date(
date: Date, locale_catalog: dict[str, str | list[str]], use_full_name: bool = False
) -> str:
"""Formats a `Date` object to a string in the following format: "Jan 2021". The
month abbreviation is taken from the locale catalog's `abbreviations_for_months` or
`full_names_of_months` field.
Example:
```python
format_date(Date(2024, 5, 1))
```
will return
`#!python "May 2024"`
Args:
date (Date): The date to format.
locale_catalog (dict[str, str | list[str]]): The locale catalog to use for
formatting the date.
use_full_name (bool, optional): If `True`, the full name of the month will be
used. Defaults to `False`.
Returns:
str: The formatted date.
"""
# Month abbreviations,
# taken from: https://web.library.yale.edu/cataloging/months
if use_full_name:
month_names = locale_catalog["full_names_of_months"]
else:
month_names = locale_catalog["abbreviations_for_months"]
month = int(date.strftime("%m"))
month_abbreviation = month_names[month - 1]
year = date.strftime(format="%Y")
date_string = f"{month_abbreviation} {year}"
return date_string
def dictionary_key_to_proper_section_title(key: str) -> str:
"""Convert a dictionary key to a proper section title.

View File

@@ -1,366 +0,0 @@
"""
The `rendercv.data_models.validators` module contains all the functions used to validate
the data models of RenderCV, in addition to Pydantic inner validation.
"""
import importlib
import importlib.machinery
import importlib.util
import pathlib
import re
from datetime import date as Date
from typing import Any, Optional
import pydantic
from . import models
# from .types import (
# available_entry_types,
# available_theme_options,
# available_themes,
# available_entry_type_names,
# # RenderCVBuiltinDesign,
# )
from . import utilities as util
# Create a URL validator:
# url_validator = pydantic.TypeAdapter(pydantic.HttpUrl)
# Create a RenderCVDesign validator:
# rendercv_design_validator = pydantic.TypeAdapter(RenderCVBuiltinDesign)
def validate_url(url: str) -> str:
"""Validate a URL.
Args:
url (str): The URL to validate.
Returns:
str: The validated URL.
"""
url_validator.validate_strings(url)
return url
def validate_date_field(date: Optional[int | str]) -> Optional[int | str]:
"""Check if the `date` field is provided correctly.
Args:
date (Optional[int | str]): The date to validate.
Returns:
Optional[int | str]: The validated date.
"""
date_is_provided = date is not None
if date_is_provided:
if isinstance(date, str):
if re.fullmatch(r"\d{4}-\d{2}(-\d{2})?", date):
# Then it is in YYYY-MM-DD or YYYY-MMY format
# Check if it is a valid date:
util.get_date_object(date)
elif re.fullmatch(r"\d{4}", date):
# Then it is in YYYY format, so, convert it to an integer:
# This is not required for start_date and end_date because they
# can't be casted into a general string. For date, this needs to
# be done manually, because it can be a general string.
date = int(date)
elif isinstance(date, Date):
# Pydantic parses YYYY-MM-DD dates as datetime.date objects. We need to
# convert them to strings because that is how RenderCV uses them.
date = date.isoformat()
return date
def validate_start_and_end_date_fields(
date: str | Date,
) -> str:
"""Check if the `start_date` and `end_date` fields are provided correctly.
Args:
date (Optional[Literal["present"] | int | RenderCVDate]): The date to validate.
Returns:
Optional[Literal["present"] | int | RenderCVDate]: The validated date.
"""
date_is_provided = date is not None
if date_is_provided:
if isinstance(date, Date):
# Pydantic parses YYYY-MM-DD dates as datetime.date objects. We need to
# convert them to strings because that is how RenderCV uses them.
date = date.isoformat()
elif date != "present":
# Validate the date:
util.get_date_object(date)
return date
def validate_and_adjust_dates_of_an_entry(entry: models.EntryBase):
"""Check if the dates are provided correctly and make the necessary adjustments.
Args:
entry (EntryBase): The entry to validate its dates.
Returns:
EntryBase: The validated entry.
"""
date_is_provided = entry.date is not None
start_date_is_provided = entry.start_date is not None
end_date_is_provided = entry.end_date is not None
if date_is_provided:
# If only date is provided, ignore start_date and end_date:
entry.start_date = None
entry.end_date = None
elif not start_date_is_provided and end_date_is_provided:
# If only end_date is provided, assume it is a one-day event and act like
# only the date is provided:
entry.date = entry.end_date
entry.start_date = None
entry.end_date = None
elif start_date_is_provided:
start_date = util.get_date_object(entry.start_date)
if not end_date_is_provided:
# If only start_date is provided, assume it is an ongoing event, i.e.,
# the end_date is present:
entry.end_date = "present"
if entry.end_date != "present":
end_date = util.get_date_object(entry.end_date)
if start_date > end_date:
raise ValueError(
'"start_date" can not be after "end_date"!',
"start_date", # This is the location of the error
str(start_date), # This is value of the error
)
return entry
def validate_a_social_network_username(username: str, network: str) -> str:
"""Check if the `username` field in the `SocialNetwork` model is provided correctly.
Args:
username (str): The username to validate.
Returns:
str: The validated username.
"""
if network == "Mastodon":
mastodon_username_pattern = r"@[^@]+@[^@]+"
if not re.fullmatch(mastodon_username_pattern, username):
raise ValueError(
'Mastodon username should be in the format "@username@domain"!'
)
if network == "StackOverflow":
stackoverflow_username_pattern = r"\d+\/[^\/]+"
if not re.fullmatch(stackoverflow_username_pattern, username):
raise ValueError(
'StackOverflow username should be in the format "user_id/username"!'
)
if network == "YouTube":
if username.startswith("@"):
raise ValueError(
'YouTube username should not start with "@"! Remove "@" from the'
" beginning of the username."
)
return username
def validate_a_section(sections_input: list[Any]) -> list[Any]:
"""Validate a list of entries (a section).
Sections input is a list of entries. Since there are multiple entry types, it is not
possible to validate it directly. Firstly, the entry type is determined with the
`get_entry_and_section_type` function. If the entry type cannot be determined, an
error is raised. If the entry type is determined, the rest of the list is validated
based on the determined entry type.
Args:
sections_input (list[Any]): The sections input to validate.
Returns:
list[Any]: The validated sections input.
"""
if isinstance(sections_input, list):
# Find the entry type based on the first identifiable entry:
entry_type = None
section_type = None
for entry in sections_input:
try:
entry_type, section_type = (
validate_an_entry_type_and_get_entry_type_name(entry)
)
break
except ValueError:
pass
if entry_type is None or section_type is None:
raise ValueError(
"RenderCV couldn't match this section with any entry types! Please"
" check the entries and make sure they are provided correctly.",
"", # This is the location of the error
"", # This is value of the error
)
section = {
"title": "Test Section",
"entry_type": entry_type,
"entries": sections_input,
}
try:
section_type.model_validate(
section,
)
except pydantic.ValidationError as e:
new_error = ValueError(
"There are problems with the entries. RenderCV detected the entry type"
f" of this section to be {entry_type}! The problems are shown below.",
"", # This is the location of the error
"", # This is value of the error
)
raise new_error from e
return sections_input
def validate_an_entry_type_and_get_entry_type_name(
entry: dict[str, Any] | str,
) -> str:
"""Determine the entry type based on an entry.
Args:
entry: The entry to determine the type.
Returns:
str: The name of the entry type.
"""
# Get the class attributes of EntryBase class:
common_attributes = set(models.EntryBase.model_fields.keys())
if isinstance(entry, dict):
entry_type_name = None # the entry type is not determined yet
for EntryType in available_entry_types:
characteristic_entry_attributes = (
set(EntryType.model_fields.keys()) - common_attributes
)
# If at least one of the characteristic_entry_attributes is in the entry,
# then it means the entry is of this type:
if characteristic_entry_attributes & set(entry.keys()):
entry_type_name = EntryType.__name__
break
if entry_type_name is None:
raise ValueError("The entry is not provided correctly.")
elif isinstance(entry, str):
# Then it is a TextEntry
entry_type_name = "TextEntry"
return entry_type_name
def validate_a_custom_theme(
design: Any,
) -> Any:
"""Validate a custom theme.
Args:
design (Any | RenderCVBuiltinDesign): The design to validate.
Returns:
RenderCVBuiltinDesign | Any: The validated design.
"""
if (
isinstance(design, available_theme_options)
or design["theme"] in available_themes
):
# Then it means it is a built-in theme. Return it as it is:
return design
theme_name: str = str(design["theme"])
# Check if the theme name is valid:
if not theme_name.isalpha():
raise ValueError(
"The custom theme name should contain only letters.",
"theme", # this is the location of the error
theme_name, # this is value of the error
)
custom_theme_folder = pathlib.Path(theme_name)
# Check if the custom theme folder exists:
if not custom_theme_folder.exists():
raise ValueError(
f"The custom theme folder `{custom_theme_folder}` does not exist."
" It should be in the working directory as the input file.",
"", # this is the location of the error
theme_name, # this is value of the error
)
# check if all the necessary files are provided in the custom theme folder:
required_entry_files = [
entry_type_name + ".j2.tex" for entry_type_name in available_entry_type_names
]
required_files = [
"SectionBeginning.j2.tex", # section beginning template
"SectionEnding.j2.tex", # section ending template
"Preamble.j2.tex", # preamble template
"Header.j2.tex", # header template
] + required_entry_files
for file in required_files:
file_path = custom_theme_folder / file
if not file_path.exists():
raise ValueError(
f"You provided a custom theme, but the file `{file}` is not"
f" found in the folder `{custom_theme_folder}`.",
"", # This is the location of the error
theme_name, # This is value of the error
)
# Import __init__.py file from the custom theme folder if it exists:
path_to_init_file = pathlib.Path(f"{theme_name}/__init__.py")
if path_to_init_file.exists():
spec = importlib.util.spec_from_file_location(
"theme",
path_to_init_file,
)
theme_module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(theme_module) # type: ignore
except SyntaxError:
raise ValueError(
f"The custom theme {theme_name}'s __init__.py file has a syntax"
" error. Please fix it.",
)
except ImportError:
raise ValueError(
f"The custom theme {theme_name}'s __init__.py file has an"
" import error. If you have copy-pasted RenderCV's built-in"
" themes, make sure tto update the import statements (e.g.,"
' "from . import" to "from rendercv.themes import").',
)
ThemeDataModel = getattr(
theme_module, f"{theme_name.capitalize()}ThemeOptions" # type: ignore
)
# Initialize and validate the custom theme data model:
theme_data_model = ThemeDataModel(**design)
else:
# Then it means there is no __init__.py file in the custom theme folder.
# Create a dummy data model and use that instead.
class ThemeOptionsAreNotProvided(models.RenderCVBaseModel):
theme: str = theme_name
theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
return theme_data_model

View File

@@ -44,13 +44,12 @@ class ClassicThemeOptions(ThemeOptions):
description="The font family of the CV. The default value is Source Sans 3.",
)
show_timespan_in: list[str] = pydantic.Field(
default=[],
default=["Experience"],
title="Show Time Span in These Sections",
description=(
"The time span will be shown in the date and location column in these"
" sections. The input should be a list of section titles as strings"
" (case-sensitive). The default value is an empty list, which means the"
" time span will not be shown in any section."
" (case-sensitive). The default value is ['Experience']."
),
)
margins: MarginsForClassic = pydantic.Field(

View File

@@ -10,7 +10,6 @@ import ruamel.yaml
import typer.testing
import rendercv.cli as cli
import rendercv.cli.handlers as handlers
import rendercv.cli.printer as printer
import rendercv.cli.utilities as util
import rendercv.data_models as dm