mirror of
https://github.com/rendercv/rendercv.git
synced 2026-10-23 04:44:41 -04:00
ensure type safety
This commit is contained in:
1 parent
b75139210f
commit
dc87688980
16 files changed
+198
-176
No files matched your search
@@ -1,8 +1,8 @@
|
||||
"""RenderCV package.
|
||||
|
||||
RenderCV is a $\LaTeX$ CV/resume framework that allows you to create a high-quality CV
|
||||
RenderCV is a $\\LaTeX$ CV/resume framework that allows you to create a high-quality CV
|
||||
as a PDF from a YAML file with Markdown syntax support and complete control over the
|
||||
$\LaTeX$ code.
|
||||
$\\LaTeX$ code.
|
||||
"""
|
||||
|
||||
__version__ = "1.11"
|
||||
+19
-18
@@ -130,27 +130,26 @@ def cli_command_render(
|
||||
' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].',
|
||||
),
|
||||
] = None,
|
||||
extra_data_model_override_argumets: typer.Context = None,
|
||||
extra_data_model_override_argumets: Optional[typer.Context] = None,
|
||||
):
|
||||
"""Render a CV from a YAML input file."""
|
||||
printer.welcome()
|
||||
|
||||
# Get paths:
|
||||
input_file_path = utilities.string_to_file_path(input_file_name)
|
||||
input_file_path: pathlib.Path = utilities.string_to_file_path(
|
||||
input_file_name
|
||||
) # type: ignore
|
||||
output_directory = pathlib.Path.cwd() / output_folder_name
|
||||
|
||||
paths: dict[
|
||||
Literal["latex", "pdf", "markdown", "html", "png"], Optional[pathlib.Path]
|
||||
] = {
|
||||
"latex": latex_path,
|
||||
"pdf": pdf_path,
|
||||
"markdown": markdown_path,
|
||||
"html": html_path,
|
||||
"png": png_path,
|
||||
"latex": utilities.string_to_file_path(latex_path),
|
||||
"pdf": utilities.string_to_file_path(pdf_path),
|
||||
"markdown": utilities.string_to_file_path(markdown_path),
|
||||
"html": utilities.string_to_file_path(html_path),
|
||||
"png": utilities.string_to_file_path(png_path),
|
||||
}
|
||||
for file_type, path in paths.items():
|
||||
if path:
|
||||
paths[file_type] = utilities.string_to_file_path(path)
|
||||
|
||||
# change the current working directory to the input file's directory (because
|
||||
# the template overrides are looked up in the current working directory):
|
||||
@@ -198,7 +197,7 @@ def cli_command_render(
|
||||
progress.finish_the_current_step()
|
||||
|
||||
progress.start_a_step("Rendering the LaTeX file to a PDF")
|
||||
pdf_file_path_in_output_folder = renderer.render_pdf_from_latex(
|
||||
pdf_file_path_in_output_folder = renderer.render_a_pdf_from_latex(
|
||||
latex_file_path_in_output_folder, use_local_latex_command
|
||||
)
|
||||
if paths["pdf"]:
|
||||
@@ -219,19 +218,21 @@ def cli_command_render(
|
||||
markdown_file_path_in_output_folder = renderer.render_a_markdown_file(
|
||||
data_model, output_directory
|
||||
)
|
||||
if markdown_path:
|
||||
utilities.copy_files(markdown_file_path_in_output_folder, markdown_path)
|
||||
if paths["markdown"]:
|
||||
utilities.copy_files(
|
||||
markdown_file_path_in_output_folder, paths["markdown"]
|
||||
)
|
||||
progress.finish_the_current_step()
|
||||
|
||||
if not dont_generate_html:
|
||||
progress.start_a_step(
|
||||
"Rendering the Markdown file to a HTML (for Grammarly)"
|
||||
)
|
||||
html_file_path_in_output_folder = renderer.render_html_from_markdown(
|
||||
html_file_path_in_output_folder = renderer.render_an_html_from_markdown(
|
||||
markdown_file_path_in_output_folder
|
||||
)
|
||||
if html_path:
|
||||
utilities.copy_files(html_file_path_in_output_folder, html_path)
|
||||
if paths["html"]:
|
||||
utilities.copy_files(html_file_path_in_output_folder, paths["html"])
|
||||
progress.finish_the_current_step()
|
||||
|
||||
|
||||
@@ -289,7 +290,7 @@ def cli_command_new(
|
||||
created_files_and_folders.append(input_file_path.name)
|
||||
except ValueError as e:
|
||||
# if the theme is not in the available themes, then raise an error
|
||||
printer.error(e)
|
||||
printer.error(exception=e)
|
||||
|
||||
if not dont_create_theme_source_files:
|
||||
# copy the package's theme files to the current directory
|
||||
@@ -352,7 +353,7 @@ def cli_command_create_theme(
|
||||
)
|
||||
|
||||
theme_folder = utilities.copy_templates(
|
||||
based_on, pathlib.Path.cwd(), new_folder_name=theme_name, suppress_warning=True
|
||||
based_on, pathlib.Path.cwd(), new_folder_name=theme_name
|
||||
)
|
||||
|
||||
if theme_folder is None:
|
||||
|
||||
@@ -14,6 +14,10 @@ import ruamel.yaml
|
||||
import ruamel.yaml.parser
|
||||
import typer
|
||||
from rich import print
|
||||
import rich.table
|
||||
import rich.progress
|
||||
import rich.text
|
||||
import rich.panel
|
||||
|
||||
from .. import __version__
|
||||
from . import utilities
|
||||
@@ -392,7 +396,7 @@ def handle_and_print_raised_exceptions(function: Callable) -> Callable:
|
||||
e,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
error(e)
|
||||
error(exception=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])
|
||||
@@ -406,7 +410,7 @@ def handle_and_print_raised_exceptions(function: Callable) -> Callable:
|
||||
f" UTF-8 ({bad_character}):\n {bad_character_context}",
|
||||
)
|
||||
except ValueError as e:
|
||||
error(e)
|
||||
error(exception=e)
|
||||
except typer.Exit:
|
||||
pass
|
||||
except jinja2.exceptions.TemplateSyntaxError as e:
|
||||
@@ -416,6 +420,6 @@ def handle_and_print_raised_exceptions(function: Callable) -> Callable:
|
||||
e,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
error(e)
|
||||
error(exception=e)
|
||||
|
||||
return wrapper
|
||||
+22
-13
@@ -12,24 +12,30 @@ from typing import Optional
|
||||
import pydantic
|
||||
import typer
|
||||
|
||||
from .. import data
|
||||
|
||||
def string_to_file_path(string: str) -> pathlib.Path:
|
||||
"""Convert a string to a pathlib.Path object.
|
||||
|
||||
def string_to_file_path(string: Optional[str]) -> Optional[pathlib.Path]:
|
||||
"""Convert a string to a pathlib.Path object. If the string is None, then return
|
||||
None.
|
||||
|
||||
Args:
|
||||
string (str): The string to be converted to a pathlib.Path object.
|
||||
Returns:
|
||||
pathlib.Path: The pathlib.Path object.
|
||||
"""
|
||||
return pathlib.Path(string).absolute()
|
||||
if string is not None:
|
||||
return pathlib.Path(string).absolute()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def set_or_update_a_value(
|
||||
data_model: pydantic.BaseModel | dict | list,
|
||||
data_model: pydantic.BaseModel,
|
||||
key: str,
|
||||
value: str,
|
||||
sub_model: pydantic.BaseModel | dict | list = None,
|
||||
):
|
||||
sub_model: Optional[pydantic.BaseModel | dict | list] = None,
|
||||
) -> pydantic.BaseModel: # type: ignore
|
||||
"""Set or update a value in a data model for a specific key. For example, a key can
|
||||
be `cv.sections.education.3.institution` and the value can be "Bogazici University".
|
||||
|
||||
@@ -98,19 +104,19 @@ def set_or_update_a_value(
|
||||
|
||||
|
||||
def set_or_update_values(
|
||||
data_model: pydantic.BaseModel,
|
||||
data_model: data.RenderCVDataModel,
|
||||
key_and_values: dict[str, str],
|
||||
) -> pydantic.BaseModel:
|
||||
"""Set or update values in a data model for specific keys. It uses the
|
||||
) -> data.RenderCVDataModel:
|
||||
"""Set or update values in `RenderCVDataModel` for specific keys. It uses the
|
||||
`set_or_update_a_value` function to set or update the values.
|
||||
|
||||
Args:
|
||||
data_model (pydantic.BaseModel): The data model to set or update the values.
|
||||
data_model (data.RenderCVDataModel): The data model to set or update the values.
|
||||
key_and_values (dict[str, str]): The key and value pairs to set or update.
|
||||
"""
|
||||
for key, value in key_and_values.items():
|
||||
try:
|
||||
data_model = set_or_update_a_value(data_model, key_and_values)
|
||||
data_model = set_or_update_a_value(data_model, key, value) # type: ignore
|
||||
except pydantic.ValidationError as e:
|
||||
raise e
|
||||
except (ValueError, KeyError, IndexError, AttributeError):
|
||||
@@ -119,7 +125,7 @@ def set_or_update_values(
|
||||
return data_model
|
||||
|
||||
|
||||
def copy_files(paths: list[pathlib.Path], new_path: pathlib.Path):
|
||||
def copy_files(paths: list[pathlib.Path] | pathlib.Path, new_path: pathlib.Path):
|
||||
"""Copy files to the given path. If there are multiple files, then rename the new
|
||||
path by adding a number to the end of the path.
|
||||
|
||||
@@ -127,6 +133,9 @@ def copy_files(paths: list[pathlib.Path], new_path: pathlib.Path):
|
||||
paths (list[pathlib.Path]): The paths of the files to be copied.
|
||||
new_path (pathlib.Path): The path to copy the files to.
|
||||
"""
|
||||
if isinstance(paths, pathlib.Path):
|
||||
paths = [paths]
|
||||
|
||||
if len(paths) == 1:
|
||||
shutil.copy2(paths[0], new_path)
|
||||
else:
|
||||
@@ -210,7 +219,7 @@ def copy_templates(
|
||||
Optional[pathlib.Path]: The path to the copied folder.
|
||||
"""
|
||||
# copy the package's theme files to the current directory
|
||||
template_directory = pathlib.Path(__file__).parent / "themes" / folder_name
|
||||
template_directory = pathlib.Path(__file__).parent.parent / "themes" / folder_name
|
||||
if new_folder_name:
|
||||
destination = copy_to / new_folder_name
|
||||
else:
|
||||
|
||||
@@ -34,6 +34,7 @@ from .models import (
|
||||
available_themes,
|
||||
format_date,
|
||||
SectionContents,
|
||||
available_entry_type_names,
|
||||
)
|
||||
from .reader import read_input_file
|
||||
|
||||
@@ -59,4 +60,5 @@ __all__ = [
|
||||
"Entry",
|
||||
"available_social_networks",
|
||||
"SectionContents",
|
||||
"available_entry_type_names",
|
||||
]
|
||||
@@ -36,6 +36,7 @@ from .entry_types import (
|
||||
NormalEntry,
|
||||
OneLineEntry,
|
||||
PublicationEntry,
|
||||
available_entry_type_names,
|
||||
)
|
||||
from .locale_catalog import LocaleCatalog
|
||||
from .rendercv_data_model import RenderCVDataModel
|
||||
@@ -57,4 +58,5 @@ __all__ = [
|
||||
"available_social_networks",
|
||||
"SectionContents",
|
||||
"available_themes",
|
||||
"available_entry_type_names",
|
||||
]
|
||||
@@ -8,7 +8,6 @@ import re
|
||||
from datetime import date as Date
|
||||
from typing import Optional
|
||||
|
||||
from .curriculum_vitae import CurriculumVitae
|
||||
from .locale_catalog import locale_catalog
|
||||
|
||||
|
||||
@@ -47,9 +46,9 @@ def format_date(date: Date, use_full_name: bool = False) -> str:
|
||||
|
||||
|
||||
def compute_time_span_string(
|
||||
start_date: Optional[str],
|
||||
end_date: Optional[str],
|
||||
date: Optional[str],
|
||||
start_date: Optional[str | int],
|
||||
end_date: Optional[str | int],
|
||||
date: Optional[str | int],
|
||||
) -> str:
|
||||
"""
|
||||
Return a time span string based on the provided dates.
|
||||
@@ -135,9 +134,9 @@ def compute_time_span_string(
|
||||
|
||||
|
||||
def compute_date_string(
|
||||
start_date: Optional[str],
|
||||
end_date: Optional[str],
|
||||
date: Optional[str],
|
||||
start_date: Optional[str | int],
|
||||
end_date: Optional[str | int],
|
||||
date: Optional[str | int],
|
||||
show_only_years: bool = False,
|
||||
) -> str:
|
||||
"""Return a date string based on the provided dates.
|
||||
@@ -194,7 +193,7 @@ def compute_date_string(
|
||||
start_date = format_date(date_object)
|
||||
|
||||
if end_date == "present":
|
||||
end_date = locale_catalog["present"]
|
||||
end_date = locale_catalog["present"] # type: ignore
|
||||
elif isinstance(end_date, int):
|
||||
# Then it means only the year is provided
|
||||
end_date = str(end_date)
|
||||
@@ -246,96 +245,6 @@ def compute_social_network_url(network: str, username: str):
|
||||
return url
|
||||
|
||||
|
||||
def compute_connections(cv: "CurriculumVitae") -> 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."
|
||||
|
||||
The connections are used in the header of the CV.
|
||||
|
||||
Args:
|
||||
cv (CurriculumVitae): The CV to compute the connections.
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: The computed connections.
|
||||
"""
|
||||
connections: list[dict[str, str]] = []
|
||||
|
||||
if cv.location is not None:
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faMapMarker*",
|
||||
"url": None,
|
||||
"clean_url": None,
|
||||
"placeholder": cv.location,
|
||||
}
|
||||
)
|
||||
|
||||
if cv.email is not None:
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faEnvelope[regular]",
|
||||
"url": f"mailto:{cv.email}",
|
||||
"clean_url": cv.email,
|
||||
"placeholder": cv.email,
|
||||
}
|
||||
)
|
||||
|
||||
if cv.phone is not None:
|
||||
phone_placeholder = cv.phone.replace("tel:", "").replace("-", " ")
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faPhone*",
|
||||
"url": f"{cv.phone}",
|
||||
"clean_url": phone_placeholder,
|
||||
"placeholder": phone_placeholder,
|
||||
}
|
||||
)
|
||||
|
||||
if cv.website is not None:
|
||||
website_placeholder = make_a_url_clean(cv.website)
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faLink",
|
||||
"url": cv.website,
|
||||
"clean_url": website_placeholder,
|
||||
"placeholder": website_placeholder,
|
||||
}
|
||||
)
|
||||
|
||||
if cv.social_networks is not None:
|
||||
icon_dictionary = {
|
||||
"LinkedIn": "\\faLinkedinIn",
|
||||
"GitHub": "\\faGithub",
|
||||
"GitLab": "\\faGitlab",
|
||||
"Instagram": "\\faInstagram",
|
||||
"Mastodon": "\\faMastodon",
|
||||
"ORCID": "\\faOrcid",
|
||||
"StackOverflow": "\\faStackOverflow",
|
||||
"ResearchGate": "\\faResearchgate",
|
||||
"YouTube": "\\faYoutube",
|
||||
"Google Scholar": "\\faGraduationCap",
|
||||
}
|
||||
for social_network in cv.social_networks:
|
||||
clean_url = make_a_url_clean(social_network.url)
|
||||
connection = {
|
||||
"latex_icon": icon_dictionary[social_network.network],
|
||||
"url": social_network.url,
|
||||
"clean_url": clean_url,
|
||||
"placeholder": social_network.username,
|
||||
}
|
||||
|
||||
if social_network.network == "StackOverflow":
|
||||
username = social_network.username.split("/")[1]
|
||||
connection["placeholder"] = username
|
||||
if social_network.network == "Google Scholar":
|
||||
connection["placeholder"] = "Google Scholar"
|
||||
|
||||
connections.append(connection)
|
||||
|
||||
return connections
|
||||
|
||||
|
||||
def make_a_url_clean(url: str) -> str:
|
||||
"""Make a URL clean by removing the protocol, www, and trailing slashes.
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def get_characteristic_entry_attributes(
|
||||
|
||||
|
||||
def get_entry_type_name_and_section_validator(
|
||||
entry: dict[str, Any] | str, entry_types: list[Type]
|
||||
entry: dict[str, Any] | str | Type, entry_types: list[Type]
|
||||
) -> tuple[str, Type[SectionBase]]:
|
||||
"""Get the entry type name and the section validator based on the entry.
|
||||
|
||||
@@ -126,10 +126,12 @@ def get_entry_type_name_and_section_validator(
|
||||
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
|
||||
characteristic_entry_attributes = get_characteristic_entry_attributes(
|
||||
entry_types
|
||||
)
|
||||
|
||||
for (
|
||||
EntryType,
|
||||
@@ -155,10 +157,12 @@ def get_entry_type_name_and_section_validator(
|
||||
entry_type_name = entry.__class__.__name__
|
||||
section_type = create_a_section_validator(entry.__class__)
|
||||
|
||||
return entry_type_name, section_type
|
||||
return entry_type_name, section_type # type: ignore
|
||||
|
||||
|
||||
def validate_a_section(sections_input: list[Any], entry_types: list[Type]) -> list[Any]:
|
||||
def validate_a_section(
|
||||
sections_input: list[Any], entry_types: list[Type]
|
||||
) -> list[entry_types.Entry]:
|
||||
"""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
|
||||
@@ -204,9 +208,10 @@ def validate_a_section(sections_input: list[Any], entry_types: list[Type]) -> li
|
||||
}
|
||||
|
||||
try:
|
||||
section_type.model_validate(
|
||||
section_object = section_type.model_validate(
|
||||
section,
|
||||
)
|
||||
sections_input = section_object.entries
|
||||
except pydantic.ValidationError as e:
|
||||
new_error = ValueError(
|
||||
"There are problems with the entries. RenderCV detected the entry type"
|
||||
@@ -217,6 +222,11 @@ def validate_a_section(sections_input: list[Any], entry_types: list[Type]) -> li
|
||||
)
|
||||
raise new_error from e
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
"Each section should be a list of entries! Please see the documentation for"
|
||||
" more information about the sections.",
|
||||
)
|
||||
return sections_input
|
||||
|
||||
|
||||
@@ -261,8 +271,8 @@ ListOfEntries = list[entry_types.Entry]
|
||||
# can be any of the available entry types. The section is validated with the
|
||||
# `validate_a_section` function.
|
||||
SectionContents = Annotated[
|
||||
ListOfEntries,
|
||||
pydantic.PlainValidator(
|
||||
pydantic.json_schema.SkipJsonSchema[Any] | ListOfEntries,
|
||||
pydantic.BeforeValidator(
|
||||
lambda entries: validate_a_section(
|
||||
entries, entry_types=entry_types.available_entry_models
|
||||
)
|
||||
@@ -389,11 +399,88 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def connections(self) -> list[dict[str, str]]:
|
||||
def connections(self) -> list[dict[str, Optional[str]]]:
|
||||
"""Return all the connections of the person as a list of dictionaries and cache
|
||||
`connections` as an attribute of the instance.
|
||||
`connections` as an attribute of the instance. The connections are used in the
|
||||
header of the CV.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Optional[str]]]: The connections of the person.
|
||||
"""
|
||||
connections = computers.compute_connections(self)
|
||||
|
||||
connections: list[dict[str, Optional[str]]] = []
|
||||
|
||||
if self.location is not None:
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faMapMarker*",
|
||||
"url": None,
|
||||
"clean_url": None,
|
||||
"placeholder": self.location,
|
||||
}
|
||||
)
|
||||
|
||||
if self.email is not None:
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faEnvelope[regular]",
|
||||
"url": f"mailto:{self.email}",
|
||||
"clean_url": self.email,
|
||||
"placeholder": self.email,
|
||||
}
|
||||
)
|
||||
|
||||
if self.phone is not None:
|
||||
phone_placeholder = self.phone.replace("tel:", "").replace("-", " ")
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faPhone*",
|
||||
"url": f"{self.phone}",
|
||||
"clean_url": phone_placeholder,
|
||||
"placeholder": phone_placeholder,
|
||||
}
|
||||
)
|
||||
|
||||
if self.website is not None:
|
||||
website_placeholder = computers.make_a_url_clean(str(self.website))
|
||||
connections.append(
|
||||
{
|
||||
"latex_icon": "\\faLink",
|
||||
"url": str(self.website),
|
||||
"clean_url": website_placeholder,
|
||||
"placeholder": website_placeholder,
|
||||
}
|
||||
)
|
||||
|
||||
if self.social_networks is not None:
|
||||
icon_dictionary = {
|
||||
"LinkedIn": "\\faLinkedinIn",
|
||||
"GitHub": "\\faGithub",
|
||||
"GitLab": "\\faGitlab",
|
||||
"Instagram": "\\faInstagram",
|
||||
"Mastodon": "\\faMastodon",
|
||||
"ORCID": "\\faOrcid",
|
||||
"StackOverflow": "\\faStackOverflow",
|
||||
"ResearchGate": "\\faResearchgate",
|
||||
"YouTube": "\\faYoutube",
|
||||
"Google Scholar": "\\faGraduationCap",
|
||||
}
|
||||
for social_network in self.social_networks:
|
||||
clean_url = computers.make_a_url_clean(social_network.url)
|
||||
connection = {
|
||||
"latex_icon": icon_dictionary[social_network.network],
|
||||
"url": social_network.url,
|
||||
"clean_url": clean_url,
|
||||
"placeholder": social_network.username,
|
||||
}
|
||||
|
||||
if social_network.network == "StackOverflow":
|
||||
username = social_network.username.split("/")[1]
|
||||
connection["placeholder"] = username
|
||||
if social_network.network == "Google Scholar":
|
||||
connection["placeholder"] = "Google Scholar"
|
||||
|
||||
connections.append(connection) # type: ignore
|
||||
|
||||
return connections
|
||||
|
||||
@@ -406,8 +493,6 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||
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.
|
||||
"""
|
||||
@@ -420,7 +505,8 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||
# The first entry can be used because all the entries in the section are
|
||||
# already validated with the `validate_a_section` function:
|
||||
entry_type_name, _ = get_entry_type_name_and_section_validator(
|
||||
entries[0]
|
||||
entries[0], # type: ignore
|
||||
entry_types=entry_types.available_entry_models,
|
||||
)
|
||||
|
||||
# SectionBase is used so that entries are not validated again:
|
||||
|
||||
@@ -42,7 +42,7 @@ def validate_design_options(
|
||||
Returns:
|
||||
Any: The validated design as a Pydantic data model.
|
||||
"""
|
||||
if isinstance(design, available_theme_options):
|
||||
if isinstance(design, tuple(available_theme_options.values())):
|
||||
# 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:
|
||||
@@ -104,7 +104,7 @@ def validate_design_options(
|
||||
path_to_init_file,
|
||||
)
|
||||
|
||||
theme_module = importlib.util.module_from_spec(spec)
|
||||
theme_module = importlib.util.module_from_spec(spec) # type: ignore
|
||||
try:
|
||||
spec.loader.exec_module(theme_module) # type: ignore
|
||||
except SyntaxError:
|
||||
@@ -158,8 +158,8 @@ RenderCVBuiltinDesign = Annotated[
|
||||
# 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(
|
||||
pydantic.json_schema.SkipJsonSchema[Any] | RenderCVBuiltinDesign,
|
||||
pydantic.BeforeValidator(
|
||||
lambda design: validate_design_options(
|
||||
design,
|
||||
available_theme_options=available_theme_options,
|
||||
|
||||
@@ -70,7 +70,7 @@ def validate_start_and_end_date_fields(
|
||||
|
||||
elif date != "present":
|
||||
# Validate the date:
|
||||
computers.get_date_object(date)
|
||||
computers.get_date_object(date)
|
||||
|
||||
return date
|
||||
|
||||
@@ -106,16 +106,16 @@ def validate_and_adjust_dates_for_an_entry(
|
||||
start_date = None
|
||||
end_date = None
|
||||
elif start_date_is_provided:
|
||||
start_date = computers.get_date_object(start_date)
|
||||
start_date_object = computers.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 = computers.get_date_object(end_date)
|
||||
end_date_object = computers.get_date_object(end_date)
|
||||
|
||||
if start_date > end_date:
|
||||
if start_date_object > end_date_object:
|
||||
raise ValueError(
|
||||
'"start_date" can not be after "end_date"!',
|
||||
"start_date", # This is the location of the error
|
||||
@@ -243,7 +243,7 @@ class PublicationEntryBase(RenderCVBaseModel):
|
||||
description="The journal or conference name.",
|
||||
)
|
||||
|
||||
@pydantic.model_validator(mode="after")
|
||||
@pydantic.model_validator(mode="after") # type: ignore
|
||||
def ignore_url_if_doi_is_given(self) -> "PublicationEntryBase":
|
||||
"""Check if DOI is provided and ignore the URL if it is provided."""
|
||||
doi_is_provided = self.doi is not None
|
||||
@@ -273,7 +273,7 @@ class PublicationEntryBase(RenderCVBaseModel):
|
||||
url_is_provided = self.url is not None
|
||||
|
||||
if url_is_provided:
|
||||
return computers.make_a_url_clean(self.url)
|
||||
return computers.make_a_url_clean(self.url) # type: ignore
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -324,7 +324,7 @@ class EntryBase(EntryWithDate):
|
||||
examples=["Did this.", "Did that."],
|
||||
)
|
||||
|
||||
@pydantic.model_validator(mode="after")
|
||||
@pydantic.model_validator(mode="after") # type: ignore
|
||||
def check_and_adjust_dates(self) -> "EntryBase":
|
||||
"""Call the `validate_adjust_dates_of_an_entry` function to validate the
|
||||
dates.
|
||||
@@ -456,7 +456,7 @@ Entry = (
|
||||
# Entry.__args__[:-1] is a tuple of all the entry types except `str``:
|
||||
# `str` (TextEntry) is not included because it's validation handled differently. It is
|
||||
# not a Pydantic model, but a string.
|
||||
available_entry_models = Entry.__args__[:-1]
|
||||
available_entry_models = list(Entry.__args__[:-1])
|
||||
|
||||
available_entry_type_names = [
|
||||
entry_type.__name__ for entry_type in available_entry_models
|
||||
|
||||
@@ -114,7 +114,7 @@ class LocaleCatalog(RenderCVBaseModel):
|
||||
def update_translations(cls, value: str, info: pydantic.ValidationInfo) -> str:
|
||||
"""Update the `locale_catalog` dictionary with the provided translations."""
|
||||
if value:
|
||||
locale_catalog[info.field_name] = value
|
||||
locale_catalog[info.field_name] = value # type: ignore
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import pydantic
|
||||
|
||||
from ...themes.classic import ClassicThemeOptions
|
||||
|
||||
# Disable Pydantic warnings:
|
||||
# warnings.filterwarnings("ignore")
|
||||
from .base import RenderCVBaseModel
|
||||
from .curriculum_vitae import CurriculumVitae
|
||||
from .design import RenderCVDesign
|
||||
@@ -37,4 +35,15 @@ class RenderCVDataModel(RenderCVBaseModel):
|
||||
description=(
|
||||
"The locale catalog of the CV to allow the support of multiple languages."
|
||||
),
|
||||
validate_default=True,
|
||||
)
|
||||
|
||||
@pydantic.field_validator("locale_catalog")
|
||||
@classmethod
|
||||
def initialize_locale_catalog(cls, locale_catalog: LocaleCatalog) -> LocaleCatalog:
|
||||
"""Even if the locale catalog is not provided, initialize it with the default
|
||||
values."""
|
||||
if locale_catalog is None:
|
||||
LocaleCatalog()
|
||||
|
||||
return locale_catalog
|
||||
@@ -12,13 +12,13 @@ import ruamel.yaml
|
||||
from . import models
|
||||
|
||||
|
||||
def read_a_yaml_file(file_path_or_contents: pathlib.Path) -> dict[str, Any]:
|
||||
def read_a_yaml_file(file_path_or_contents: pathlib.Path | str) -> dict[str, Any]:
|
||||
"""Read a YAML file and return its content as a dictionary. The YAML file can be
|
||||
given as a path to the file or as the contents of the file as a string.
|
||||
|
||||
Args:
|
||||
file_path_or_contents (pathlib.Path): The path to the YAML file or the contents
|
||||
of the YAML file as a string.
|
||||
file_path_or_contents (pathlib.Path | str): The path to the YAML file or the
|
||||
contents of the YAML file as a string.
|
||||
Returns:
|
||||
dict: The content of the YAML file as a dictionary.
|
||||
"""
|
||||
|
||||
@@ -15,8 +15,8 @@ from .renderer import (
|
||||
render_a_latex_file,
|
||||
render_a_latex_file_and_copy_theme_files,
|
||||
render_a_markdown_file,
|
||||
render_html_from_markdown,
|
||||
render_pdf_from_latex,
|
||||
render_an_html_from_markdown,
|
||||
render_a_pdf_from_latex,
|
||||
render_pngs_from_pdf,
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ __all__ = [
|
||||
"render_a_latex_file",
|
||||
"render_a_markdown_file",
|
||||
"render_a_latex_file_and_copy_theme_files",
|
||||
"render_pdf_from_latex",
|
||||
"render_a_pdf_from_latex",
|
||||
"render_pngs_from_pdf",
|
||||
"render_html_from_markdown",
|
||||
"render_an_html_from_markdown",
|
||||
]
|
||||
@@ -38,7 +38,7 @@ def copy_theme_files_to_output_directory(
|
||||
else:
|
||||
# Then it means the theme is a custom theme. If theme_directory is not given
|
||||
# as an argument, then look for the theme in the current working directory.
|
||||
theme_directory_path = pathlib.Path(pathlib.Path.cwd()) / theme_name
|
||||
theme_directory_path = pathlib.Path.cwd() / theme_name
|
||||
|
||||
if not theme_directory_path.is_dir():
|
||||
raise FileNotFoundError(
|
||||
@@ -51,7 +51,7 @@ def copy_theme_files_to_output_directory(
|
||||
# theme_file.suffix returns the latest part of the file name after the last dot.
|
||||
# But we need the latest part of the file name after the first dot:
|
||||
try:
|
||||
suffix = re.search(r"\..*", theme_file.name)[0]
|
||||
suffix = re.search(r"\..*", theme_file.name)[0] # type: ignore
|
||||
except TypeError:
|
||||
suffix = ""
|
||||
|
||||
@@ -145,7 +145,7 @@ def render_a_latex_file_and_copy_theme_files(
|
||||
return latex_file_path
|
||||
|
||||
|
||||
def render_pdf_from_latex(
|
||||
def render_a_pdf_from_latex(
|
||||
latex_file_path: pathlib.Path, local_latex_command: Optional[str] = None
|
||||
) -> pathlib.Path:
|
||||
"""Run TinyTeX with the given $\\LaTeX$ file to render the PDF.
|
||||
@@ -272,15 +272,15 @@ def render_pngs_from_pdf(pdf_file_path: pathlib.Path) -> list[pathlib.Path]:
|
||||
png_files = []
|
||||
pdf = fitz.open(pdf_file_path) # open the PDF file
|
||||
for page in pdf: # iterate the pages
|
||||
image = page.get_pixmap(dpi=300)
|
||||
png_file_path = png_directory / f"{png_file_name}_{page.number+1}.png"
|
||||
image = page.get_pixmap(dpi=300) # type: ignore
|
||||
png_file_path = png_directory / f"{png_file_name}_{page.number+1}.png" # type: ignore
|
||||
image.save(png_file_path)
|
||||
png_files.append(png_file_path)
|
||||
|
||||
return png_files
|
||||
|
||||
|
||||
def render_html_from_markdown(markdown_file_path: pathlib.Path) -> pathlib.Path:
|
||||
def render_an_html_from_markdown(markdown_file_path: pathlib.Path) -> pathlib.Path:
|
||||
"""Render an HTML file from a Markdown file with the same name and in the same
|
||||
directory. It uses `rendercv/themes/main.j2.html` as the Jinja2 template.
|
||||
|
||||
|
||||
@@ -770,7 +770,7 @@ def setup_jinja2_environment() -> jinja2.Environment:
|
||||
jinja2.Environment: The theme environment.
|
||||
"""
|
||||
global jinja2_environment
|
||||
themes_directory = pathlib.Path(__file__).parent / "themes"
|
||||
themes_directory = pathlib.Path(__file__).parent.parent / "themes"
|
||||
|
||||
if jinja2_environment is None:
|
||||
# create a Jinja2 environment:
|
||||
|
||||
Reference in new issue
Block a user