From dc876889805b226f489a5e8761f18319e7fee1f2 Mon Sep 17 00:00:00 2001 From: Sina Atalay Date: Fri, 5 Jul 2024 16:10:25 +0300 Subject: [PATCH] ensure type safety --- rendercv/__init__.py | 4 +- rendercv/cli/commands.py | 37 +++---- rendercv/cli/printer.py | 10 +- rendercv/cli/utilities.py | 35 +++--- rendercv/data/__init__.py | 2 + rendercv/data/models/__init__.py | 2 + rendercv/data/models/computers.py | 105 ++---------------- rendercv/data/models/curriculum_vitae.py | 112 +++++++++++++++++--- rendercv/data/models/design.py | 8 +- rendercv/data/models/entry_types.py | 16 +-- rendercv/data/models/locale_catalog.py | 2 +- rendercv/data/models/rendercv_data_model.py | 13 ++- rendercv/data/reader.py | 6 +- rendercv/renderer/__init__.py | 8 +- rendercv/renderer/renderer.py | 12 +-- rendercv/renderer/templater.py | 2 +- 16 files changed, 198 insertions(+), 176 deletions(-) diff --git a/rendercv/__init__.py b/rendercv/__init__.py index d5603f5d..527c9a93 100644 --- a/rendercv/__init__.py +++ b/rendercv/__init__.py @@ -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" diff --git a/rendercv/cli/commands.py b/rendercv/cli/commands.py index 840b601d..16e6660c 100644 --- a/rendercv/cli/commands.py +++ b/rendercv/cli/commands.py @@ -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: diff --git a/rendercv/cli/printer.py b/rendercv/cli/printer.py index 434aacaa..47a208f7 100644 --- a/rendercv/cli/printer.py +++ b/rendercv/cli/printer.py @@ -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 diff --git a/rendercv/cli/utilities.py b/rendercv/cli/utilities.py index 3da7a904..2bcce6df 100644 --- a/rendercv/cli/utilities.py +++ b/rendercv/cli/utilities.py @@ -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: diff --git a/rendercv/data/__init__.py b/rendercv/data/__init__.py index abc94849..acf6a321 100644 --- a/rendercv/data/__init__.py +++ b/rendercv/data/__init__.py @@ -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", ] diff --git a/rendercv/data/models/__init__.py b/rendercv/data/models/__init__.py index a7e36341..28ca0234 100644 --- a/rendercv/data/models/__init__.py +++ b/rendercv/data/models/__init__.py @@ -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", ] diff --git a/rendercv/data/models/computers.py b/rendercv/data/models/computers.py index 8bf90871..2d020a98 100644 --- a/rendercv/data/models/computers.py +++ b/rendercv/data/models/computers.py @@ -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. diff --git a/rendercv/data/models/curriculum_vitae.py b/rendercv/data/models/curriculum_vitae.py index 0527bddb..1dc63dc4 100644 --- a/rendercv/data/models/curriculum_vitae.py +++ b/rendercv/data/models/curriculum_vitae.py @@ -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: diff --git a/rendercv/data/models/design.py b/rendercv/data/models/design.py index bfb0cbe2..1f57a645 100644 --- a/rendercv/data/models/design.py +++ b/rendercv/data/models/design.py @@ -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, diff --git a/rendercv/data/models/entry_types.py b/rendercv/data/models/entry_types.py index 1621cc13..8aa4a086 100644 --- a/rendercv/data/models/entry_types.py +++ b/rendercv/data/models/entry_types.py @@ -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 diff --git a/rendercv/data/models/locale_catalog.py b/rendercv/data/models/locale_catalog.py index 1df5ae03..5295617f 100644 --- a/rendercv/data/models/locale_catalog.py +++ b/rendercv/data/models/locale_catalog.py @@ -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 diff --git a/rendercv/data/models/rendercv_data_model.py b/rendercv/data/models/rendercv_data_model.py index 088af9dd..40e27ab5 100644 --- a/rendercv/data/models/rendercv_data_model.py +++ b/rendercv/data/models/rendercv_data_model.py @@ -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 diff --git a/rendercv/data/reader.py b/rendercv/data/reader.py index ce3b073a..1edf87a1 100644 --- a/rendercv/data/reader.py +++ b/rendercv/data/reader.py @@ -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. """ diff --git a/rendercv/renderer/__init__.py b/rendercv/renderer/__init__.py index 67108af1..9cc7734d 100644 --- a/rendercv/renderer/__init__.py +++ b/rendercv/renderer/__init__.py @@ -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", ] diff --git a/rendercv/renderer/renderer.py b/rendercv/renderer/renderer.py index ebcf24ec..04d413bf 100644 --- a/rendercv/renderer/renderer.py +++ b/rendercv/renderer/renderer.py @@ -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. diff --git a/rendercv/renderer/templater.py b/rendercv/renderer/templater.py index a21f49be..a823d729 100644 --- a/rendercv/renderer/templater.py +++ b/rendercv/renderer/templater.py @@ -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: