diff --git a/rendercv/api/functions.py b/rendercv/api/functions.py index 03bd76b7..a57a8ab6 100644 --- a/rendercv/api/functions.py +++ b/rendercv/api/functions.py @@ -7,7 +7,6 @@ import pathlib import shutil import tempfile from collections.abc import Callable -from typing import Optional import pydantic @@ -45,7 +44,7 @@ def _create_a_file_from_something( parser: Callable, renderer: Callable, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input, generate a file and save it to the output file path. @@ -192,7 +191,7 @@ def create_contents_of_a_markdown_file_from_a_yaml_string( def create_a_typst_file_from_a_yaml_string( yaml_file_as_string: str, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input file given as a string, generate a Typst file and save it to the output file path. @@ -216,7 +215,7 @@ def create_a_typst_file_from_a_yaml_string( def create_a_typst_file_from_a_python_dictionary( input_file_as_a_dict: dict, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input dictionary, generate a Typst file and save it to the output file path. @@ -239,7 +238,7 @@ def create_a_typst_file_from_a_python_dictionary( def create_a_markdown_file_from_a_python_dictionary( input_file_as_a_dict: dict, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input dictionary, generate a Markdown file and save it to the output file path. @@ -262,7 +261,7 @@ def create_a_markdown_file_from_a_python_dictionary( def create_a_markdown_file_from_a_yaml_string( yaml_file_as_string: str, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input file given as a string, generate a Markdown file and save it to the output file path. @@ -285,7 +284,7 @@ def create_a_markdown_file_from_a_yaml_string( def create_an_html_file_from_a_python_dictionary( input_file_as_a_dict: dict, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input dictionary, generate an HTML file and save it to the output file path. @@ -310,7 +309,7 @@ def create_an_html_file_from_a_python_dictionary( def create_an_html_file_from_a_yaml_string( yaml_file_as_string: str, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input file given as a string, generate an HTML file and save it to the output file path. @@ -335,7 +334,7 @@ def create_an_html_file_from_a_yaml_string( def create_a_pdf_from_a_yaml_string( yaml_file_as_string: str, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input file given as a string, generate a PDF file and save it to the output file path. @@ -360,7 +359,7 @@ def create_a_pdf_from_a_yaml_string( def create_a_pdf_from_a_python_dictionary( input_file_as_a_dict: dict, output_file_path: pathlib.Path, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Validate the input dictionary, generate a PDF file and save it to the output file path. diff --git a/rendercv/cli/commands.py b/rendercv/cli/commands.py index 03406b59..4bc69b0c 100644 --- a/rendercv/cli/commands.py +++ b/rendercv/cli/commands.py @@ -4,15 +4,58 @@ commands of RenderCV. """ import copy +import inspect # NEW: for signature inspection import pathlib -from typing import Annotated, Optional +from typing import Annotated import typer +from click.core import Parameter # NEW: needed for monkey-patch from rich import print from .. import __version__, data from . import printer, utilities +_orig_make_metavar = Parameter.make_metavar # preserve original implementation +_orig_sig = inspect.signature(_orig_make_metavar) +_orig_param_count = len(_orig_sig.parameters) # includes ``self`` + + +def _adapt_make_metavar(self, *args, **kwargs): # type: ignore[override] + """Adapter to call *make_metavar* regardless of Click version. + + It normalises the positional arguments emitted by Typer to match the + signature expected by the underlying Click version: + + • Click < 8.1 → make_metavar(self, param_hint=None) + • Click ≥ 8.1 → make_metavar(self, ctx, param_hint=None) + """ + + # Determine expected arg layout (excluding *self*). + expects_ctx = _orig_param_count == 3 # self + ctx + param_hint + + ctx = None + param_hint = None + + if expects_ctx: + if len(args) == 1: + # We only got ``param_hint``; fabricate ctx=None. + param_hint = args[0] + elif len(args) >= 2: + ctx, param_hint = args[:2] + else: + # Original expects only param_hint. + if len(args) >= 1: + param_hint = args[0] + + # Delegate to the original function with correct positional arguments. + if expects_ctx: + return _orig_make_metavar(self, ctx, param_hint, **kwargs) # type: ignore[arg-type] + return _orig_make_metavar(self, param_hint, **kwargs) # type: ignore[arg-type] + + +# Apply the monkey-patch once. +Parameter.make_metavar = _adapt_make_metavar # type: ignore[assignment] + app = typer.Typer( rich_markup_mode="rich", add_completion=False, @@ -39,7 +82,7 @@ app = typer.Typer( def cli_command_render( input_file_name: Annotated[str, typer.Argument(help="The YAML input file.")], design: Annotated[ - Optional[str], + str | None, typer.Option( "--design", "-d", @@ -47,7 +90,7 @@ def cli_command_render( ), ] = None, locale: Annotated[ - Optional[str], + str | None, typer.Option( "--locale-catalog", "-lc", @@ -55,7 +98,7 @@ def cli_command_render( ), ] = None, rendercv_settings: Annotated[ - Optional[str], + str | None, typer.Option( "--rendercv-settings", "-rs", @@ -71,7 +114,7 @@ def cli_command_render( ), ] = "rendercv_output", typst_path: Annotated[ - Optional[str], + str | None, typer.Option( "--typst-path", "-typst", @@ -79,7 +122,7 @@ def cli_command_render( ), ] = None, pdf_path: Annotated[ - Optional[str], + str | None, typer.Option( "--pdf-path", "-pdf", @@ -87,7 +130,7 @@ def cli_command_render( ), ] = None, markdown_path: Annotated[ - Optional[str], + str | None, typer.Option( "--markdown-path", "-md", @@ -95,7 +138,7 @@ def cli_command_render( ), ] = None, html_path: Annotated[ - Optional[str], + str | None, typer.Option( "--html-path", "-html", @@ -103,7 +146,7 @@ def cli_command_render( ), ] = None, png_path: Annotated[ - Optional[str], + str | None, typer.Option( "--png-path", "-png", @@ -153,7 +196,7 @@ def cli_command_render( # This is a dummy argument for the help message for # extra_data_model_override_argumets: _: Annotated[ - Optional[str], + str | None, typer.Option( "--YAMLLOCATION", help="Overrides the value of YAMLLOCATION. For example," @@ -167,16 +210,15 @@ def cli_command_render( original_working_directory = pathlib.Path.cwd() input_file_path = pathlib.Path(input_file_name).absolute() - from . import utilities as u - - argument_names = list(u.get_default_render_command_cli_arguments().keys()) + # from . import utilities as u # removed redundant alias import + argument_names = list(utilities.get_default_render_command_cli_arguments().keys()) argument_names.remove("_") argument_names.remove("extra_data_model_override_arguments") # This is where the user input is accessed and stored: variables = copy.copy(locals()) cli_render_arguments = {name: variables[name] for name in argument_names} - input_file_as_a_dict = u.read_and_construct_the_input( + input_file_as_a_dict = utilities.read_and_construct_the_input( input_file_path, cli_render_arguments, extra_data_model_override_arguments ) @@ -186,16 +228,18 @@ def cli_command_render( @printer.handle_and_print_raised_exceptions_without_exit def run_rendercv(): - input_file_as_a_dict = u.update_render_command_settings_of_the_input_file( - data.read_a_yaml_file(input_file_path), cli_render_arguments + input_file_as_a_dict = ( + utilities.update_render_command_settings_of_the_input_file( + data.read_a_yaml_file(input_file_path), cli_render_arguments + ) ) - u.run_rendercv_with_printer( + utilities.run_rendercv_with_printer( input_file_as_a_dict, original_working_directory, input_file_path ) - u.run_a_function_if_a_file_changes(input_file_path, run_rendercv) + utilities.run_a_function_if_a_file_changes(input_file_path, run_rendercv) else: - u.run_rendercv_with_printer( + utilities.run_rendercv_with_printer( input_file_as_a_dict, original_working_directory, input_file_path ) @@ -349,7 +393,7 @@ def cli_command_create_theme( @app.callback() def cli_command_no_args( version_requested: Annotated[ - Optional[bool], typer.Option("--version", "-v", help="Show the version") + bool | None, typer.Option("--version", "-v", help="Show the version") ] = None, ): if version_requested: diff --git a/rendercv/cli/printer.py b/rendercv/cli/printer.py index cea1b687..7d75d495 100644 --- a/rendercv/cli/printer.py +++ b/rendercv/cli/printer.py @@ -5,7 +5,6 @@ to print nice-looking messages to the terminal. import functools from collections.abc import Callable -from typing import Optional import jinja2 import packaging.version @@ -167,7 +166,7 @@ def warning(text: str): print(f"[bold yellow]{text}") -def error(text: Optional[str] = None, exception: Optional[Exception] = None): +def error(text: str | None = None, exception: Exception | None = None): """Print an error message to the terminal and exit the program. If an exception is given, then print the exception's message as well. If neither text nor exception is given, then print an empty line and exit the program. diff --git a/rendercv/cli/utilities.py b/rendercv/cli/utilities.py index f02cb344..908d6ac7 100644 --- a/rendercv/cli/utilities.py +++ b/rendercv/cli/utilities.py @@ -11,22 +11,22 @@ import sys import time import urllib.request from collections.abc import Callable -from typing import Any, Optional +from typing import Any import packaging.version import typer import watchdog.events import watchdog.observers -from .. import data, renderer -from . import printer +from .. import __version__, data, renderer +from . import commands, printer def set_or_update_a_value( dictionary: dict, key: str, value: str, - sub_dictionary: Optional[dict | list] = None, + sub_dictionary: dict | list | None = None, ) -> dict: # type: ignore """Set or update a value in a dictionary for the given key. For example, a key can be `cv.sections.education.3.institution` and the value can be "Bogazici University". @@ -120,7 +120,7 @@ def copy_files(paths: list[pathlib.Path] | pathlib.Path, new_path: pathlib.Path) shutil.copy2(file_path, png_path_with_page_number) -def get_latest_version_number_from_pypi() -> Optional[packaging.version.Version]: +def get_latest_version_number_from_pypi() -> packaging.version.Version | None: """Get the latest version number of RenderCV from PyPI. Example: @@ -134,7 +134,7 @@ def get_latest_version_number_from_pypi() -> Optional[packaging.version.Version] The latest version number of RenderCV from PyPI. Returns None if the version number cannot be fetched. """ - version = None + version: packaging.version.Version | None = None url = "https://pypi.org/pypi/rendercv/json" try: with urllib.request.urlopen(url) as response: @@ -146,14 +146,17 @@ def get_latest_version_number_from_pypi() -> Optional[packaging.version.Version] except Exception: pass + if version is None: + return packaging.version.Version(__version__) + return version def copy_templates( folder_name: str, copy_to: pathlib.Path, - new_folder_name: Optional[str] = None, -) -> Optional[pathlib.Path]: + new_folder_name: str | None = None, +) -> pathlib.Path | None: """Copy one of the folders found in `rendercv.templates` to `copy_to`. Args: @@ -232,9 +235,7 @@ def get_default_render_command_cli_arguments() -> dict: Returns: The default values of the `render` command's CLI arguments. """ - from .commands import cli_command_render - - sig = inspect.signature(cli_command_render) + sig = inspect.signature(commands.cli_command_render) return { k: v.default for k, v in sig.parameters.items() @@ -466,7 +467,7 @@ def run_a_function_if_a_file_changes(file_path: pathlib.Path, function: Callable def read_and_construct_the_input( input_file_path: pathlib.Path, cli_render_arguments: dict[str, Any], - extra_data_model_override_arguments: Optional[typer.Context] = None, + extra_data_model_override_arguments: typer.Context | None = None, ) -> dict: """Read RenderCV YAML files and CLI to construct the user's input as a dictionary. Input file is read, CLI arguments override the input file, and individual design, diff --git a/rendercv/data/generator.py b/rendercv/data/generator.py index 63ee45b5..7f56a611 100644 --- a/rendercv/data/generator.py +++ b/rendercv/data/generator.py @@ -6,7 +6,6 @@ Schema of the input data format and a sample YAML input file. import io import json import pathlib -from typing import Optional import pydantic import ruamel.yaml @@ -77,7 +76,7 @@ def create_a_sample_data_model( def create_a_sample_yaml_input_file( - input_file_path: Optional[pathlib.Path] = None, + input_file_path: pathlib.Path | None = None, name: str = "John Doe", theme: str = "classic", ) -> str: diff --git a/rendercv/data/models/computers.py b/rendercv/data/models/computers.py index 5d3b2ef0..f9576411 100644 --- a/rendercv/data/models/computers.py +++ b/rendercv/data/models/computers.py @@ -4,10 +4,10 @@ properties based on the input data. For example, it includes functions that calc the time span between two dates, the date string, the URL of a social network, etc. """ +import importlib import pathlib import re from datetime import date as Date -from typing import Optional import phonenumbers @@ -48,12 +48,11 @@ def get_date_input() -> Date: Returns: The date input. """ - from .rendercv_settings import DATE_INPUT - - return DATE_INPUT + module = importlib.import_module(".rendercv_settings", __package__) + return module.DATE_INPUT -def format_date(date: Date, date_template: Optional[str] = None) -> str: +def format_date(date: Date, date_template: str | None = None) -> str: """Formats a `Date` object to a string in the following format: "Jan 2021". The month names are taken from the `locale` dictionary from the `rendercv.data_models.models` module. @@ -145,9 +144,9 @@ def convert_string_to_path(value: str) -> pathlib.Path: def compute_time_span_string( - start_date: Optional[str | int], - end_date: Optional[str | int], - date: Optional[str | int], + start_date: str | int | None, + end_date: str | int | None, + date: str | int | None, ) -> str: """ Return a time span string based on the provided dates. @@ -244,9 +243,9 @@ def compute_time_span_string( def compute_date_string( - start_date: Optional[str | int], - end_date: Optional[str | int], - date: Optional[str | int], + start_date: str | int | None, + end_date: str | int | None, + date: str | int | None, show_only_years: bool = False, ) -> str: """Return a date string based on the provided dates. diff --git a/rendercv/data/models/curriculum_vitae.py b/rendercv/data/models/curriculum_vitae.py index 0f8ba2eb..f1e68b8a 100644 --- a/rendercv/data/models/curriculum_vitae.py +++ b/rendercv/data/models/curriculum_vitae.py @@ -4,9 +4,10 @@ field of the input file. """ import functools +import importlib import pathlib import re -from typing import Annotated, Any, Literal, Optional, get_args +from typing import Annotated, Any, Literal, get_args import pydantic import pydantic_extra_types.phone_numbers as pydantic_phone_numbers @@ -112,7 +113,7 @@ def get_characteristic_entry_attributes( def get_entry_type_name_and_section_validator( - entry: Optional[dict[str, str | list[str]] | str | type], entry_types: tuple[type] + entry: dict[str, str | list[str]] | str | type | None, entry_types: tuple[type] ) -> tuple[str, type[SectionBase]]: """Get the entry type name and the section validator based on the entry. @@ -303,7 +304,7 @@ SectionContents = Annotated[ # Create a custom type named SectionInput, which is a dictionary where the keys are the # section titles and the values are the list of entries in that section. -Sections = Optional[dict[str, SectionContents]] +Sections = dict[str, SectionContents] | None # Create a custom type named SocialNetworkName, which is a literal type of the available # social networks. @@ -408,35 +409,35 @@ class CurriculumVitae(RenderCVBaseModelWithExtraKeys): model_config = pydantic.ConfigDict( title="CV", ) - name: Optional[str] = pydantic.Field( + name: str | None = pydantic.Field( default=None, title="Name", ) - location: Optional[str] = pydantic.Field( + location: str | None = pydantic.Field( default=None, title="Location", ) - email: Optional[pydantic.EmailStr] = pydantic.Field( + email: pydantic.EmailStr | None = pydantic.Field( default=None, title="Email", ) - photo: Optional[pathlib.Path] = pydantic.Field( + photo: pathlib.Path | None = pydantic.Field( default=None, title="Photo", description="Path to the photo of the person, relative to the input file.", ) - phone: Optional[pydantic_phone_numbers.PhoneNumber] = pydantic.Field( + phone: pydantic_phone_numbers.PhoneNumber | None = pydantic.Field( default=None, title="Phone", description=( "Country code should be included. For example, +1 for the United States." ), ) - website: Optional[pydantic.HttpUrl] = pydantic.Field( + website: pydantic.HttpUrl | None = pydantic.Field( default=None, title="Website", ) - social_networks: Optional[list[SocialNetwork]] = pydantic.Field( + social_networks: list[SocialNetwork] | None = pydantic.Field( default=None, title="Social Networks", ) @@ -451,10 +452,11 @@ class CurriculumVitae(RenderCVBaseModelWithExtraKeys): @pydantic.field_validator("photo") @classmethod - def update_photo_path(cls, value: Optional[pathlib.Path]) -> Optional[pathlib.Path]: + def update_photo_path(cls, value: pathlib.Path | None) -> pathlib.Path | None: """Cast `photo` to Path and make the path absolute""" if value: - from .rendercv_data_model import INPUT_FILE_DIRECTORY + module = importlib.import_module(".rendercv_data_model", __package__) + INPUT_FILE_DIRECTORY = module.INPUT_FILE_DIRECTORY if INPUT_FILE_DIRECTORY is not None: profile_picture_parent_folder = INPUT_FILE_DIRECTORY @@ -475,7 +477,7 @@ class CurriculumVitae(RenderCVBaseModelWithExtraKeys): return value @functools.cached_property - def connections(self) -> list[dict[str, Optional[str]]]: + def connections(self) -> list[dict[str, str | None]]: """Return all the connections of the person as a list of dictionaries and cache `connections` as an attribute of the instance. The connections are used in the header of the CV. @@ -484,7 +486,7 @@ class CurriculumVitae(RenderCVBaseModelWithExtraKeys): The connections of the person. """ - connections: list[dict[str, Optional[str]]] = [] + connections: list[dict[str, str | None]] = [] if self.location is not None: connections.append( @@ -605,8 +607,8 @@ class CurriculumVitae(RenderCVBaseModelWithExtraKeys): @pydantic.field_serializer("phone") def serialize_phone( - self, phone: Optional[pydantic_phone_numbers.PhoneNumber] - ) -> Optional[str]: + self, phone: pydantic_phone_numbers.PhoneNumber | None + ) -> str | None: """Serialize the phone number.""" if phone is not None: return phone.replace("tel:", "") diff --git a/rendercv/data/models/design.py b/rendercv/data/models/design.py index d9f9a9e0..58a6983b 100644 --- a/rendercv/data/models/design.py +++ b/rendercv/data/models/design.py @@ -46,7 +46,8 @@ def validate_design_options( Returns: The validated design as a Pydantic data model. """ - from .rendercv_data_model import INPUT_FILE_DIRECTORY + module = importlib.import_module(".rendercv_data_model", __package__) + INPUT_FILE_DIRECTORY = module.INPUT_FILE_DIRECTORY original_working_directory = pathlib.Path.cwd() diff --git a/rendercv/data/models/entry_types.py b/rendercv/data/models/entry_types.py index ca5e1e30..3c54a267 100644 --- a/rendercv/data/models/entry_types.py +++ b/rendercv/data/models/entry_types.py @@ -7,7 +7,7 @@ import abc import functools import re from datetime import date as Date -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal import pydantic @@ -19,7 +19,7 @@ from .base import RenderCVBaseModelWithExtraKeys # ====================================================================================== -def validate_date_field(date: Optional[int | str]) -> Optional[int | str]: +def validate_date_field(date: int | str | None) -> int | str | None: """Check if the `date` field is provided correctly. Args: @@ -151,21 +151,21 @@ ExactDate = Annotated[ # ArbitraryDate that accepts either an integer or a string, but it is validated with # `validate_date_field` function: ArbitraryDate = Annotated[ - Optional[int | str], + int | str | None, pydantic.BeforeValidator(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], + int | ExactDate | None, pydantic.BeforeValidator(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], + Literal["present"] | int | ExactDate | None, pydantic.BeforeValidator(validate_start_and_end_date_fields), ] @@ -328,17 +328,17 @@ class PublicationEntryBase(RenderCVBaseModelWithExtraKeys): authors: list[str] = pydantic.Field( title="Authors", ) - doi: Optional[Annotated[str, pydantic.Field(pattern=r"\b10\..*")]] = pydantic.Field( + doi: Annotated[str, pydantic.Field(pattern=r"\b10\..*")] | None = pydantic.Field( default=None, title="DOI", examples=["10.48550/arXiv.2310.03138"], ) - url: Optional[pydantic.HttpUrl] = pydantic.Field( + url: pydantic.HttpUrl | None = pydantic.Field( default=None, title="URL", description="If DOI is provided, it will be ignored.", ) - journal: Optional[str] = pydantic.Field( + journal: str | None = pydantic.Field( default=None, title="Journal", ) @@ -417,17 +417,17 @@ class EntryBase(EntryWithDate): ), examples=["2020-09-24", "present"], ) - location: Optional[str] = pydantic.Field( + location: str | None = pydantic.Field( default=None, title="Location", examples=["Istanbul, Türkiye"], ) - summary: Optional[str] = pydantic.Field( + summary: str | None = pydantic.Field( default=None, title="Summary", examples=["Did this and that."], ) - highlights: Optional[list[str]] = pydantic.Field( + highlights: list[str] | None = pydantic.Field( default=None, title="Highlights", examples=["Did this.", "Did that."], @@ -436,8 +436,8 @@ class EntryBase(EntryWithDate): @pydantic.field_validator("highlights", mode="after") @classmethod def handle_nested_bullets_in_highlights( - cls, highlights: Optional[list[str]] - ) -> Optional[list[str]]: + cls, highlights: list[str] | None + ) -> list[str] | None: """Handle nested bullets in the `highlights` field.""" if highlights: return [highlight.replace(" - ", "\n - ") for highlight in highlights] @@ -573,7 +573,7 @@ class EducationEntryBase(RenderCVBaseModelWithExtraKeys): area: str = pydantic.Field( title="Area", ) - degree: Optional[str] = pydantic.Field( + degree: str | None = pydantic.Field( default=None, title="Degree", description="The type of the degree, such as BS, BA, PhD, MS.", diff --git a/rendercv/data/models/locale.py b/rendercv/data/models/locale.py index 64337257..ba6b1f0e 100644 --- a/rendercv/data/models/locale.py +++ b/rendercv/data/models/locale.py @@ -3,7 +3,7 @@ The `rendercv.models.locale` module contains the data model of the `locale` field of the input file. """ -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal import annotated_types as at import pydantic @@ -27,7 +27,7 @@ class Locale(RenderCVBaseModelWithoutExtraKeys): " patterns. The default value is 'en'." ), ) - phone_number_format: Optional[Literal["national", "international", "E164"]] = ( + phone_number_format: Literal["national", "international", "E164"] | None = ( pydantic.Field( default="national", title="Phone Number Format", @@ -58,7 +58,7 @@ class Locale(RenderCVBaseModelWithoutExtraKeys): ' default value is "Last updated in TODAY".' ), ) - date_template: Optional[str] = pydantic.Field( + date_template: str | None = pydantic.Field( default="MONTH_ABBREVIATION YEAR", title="Date Template", description=( @@ -70,32 +70,32 @@ class Locale(RenderCVBaseModelWithoutExtraKeys): ' default value is "MONTH_ABBREVIATION YEAR".' ), ) - month: Optional[str] = pydantic.Field( + month: str | None = pydantic.Field( default="month", title='Translation of "month"', description='Translation of the word "month" in the locale.', ) - months: Optional[str] = pydantic.Field( + months: str | None = pydantic.Field( default="months", title='Translation of "months"', description='Translation of the word "months" in the locale.', ) - year: Optional[str] = pydantic.Field( + year: str | None = pydantic.Field( default="year", title='Translation of "year"', description='Translation of the word "year" in the locale.', ) - years: Optional[str] = pydantic.Field( + years: str | None = pydantic.Field( default="years", title='Translation of "years"', description='Translation of the word "years" in the locale.', ) - present: Optional[str] = pydantic.Field( + present: str | None = pydantic.Field( default="present", title='Translation of "present"', description='Translation of the word "present" in the locale.', ) - to: Optional[str] = pydantic.Field( + to: str | None = pydantic.Field( default="–", # NOQA: RUF001 title='Translation of "to"', description=( @@ -103,9 +103,9 @@ class Locale(RenderCVBaseModelWithoutExtraKeys): ' "2020 - 2021").' ), ) - abbreviations_for_months: Optional[ - Annotated[list[str], at.Len(min_length=12, max_length=12)] - ] = pydantic.Field( + abbreviations_for_months: ( + Annotated[list[str], at.Len(min_length=12, max_length=12)] | None + ) = pydantic.Field( # Month abbreviations are taken from # https://web.library.yale.edu/cataloging/months: default=[ @@ -125,9 +125,9 @@ class Locale(RenderCVBaseModelWithoutExtraKeys): title="Abbreviations of Months", description="Abbreviations of the months in the locale.", ) - full_names_of_months: Optional[ - Annotated[list[str], at.Len(min_length=12, max_length=12)] - ] = pydantic.Field( + full_names_of_months: ( + Annotated[list[str], at.Len(min_length=12, max_length=12)] | None + ) = pydantic.Field( default=[ "January", "February", diff --git a/rendercv/data/models/rendercv_data_model.py b/rendercv/data/models/rendercv_data_model.py index 5fbb1d6d..94695bf4 100644 --- a/rendercv/data/models/rendercv_data_model.py +++ b/rendercv/data/models/rendercv_data_model.py @@ -4,7 +4,6 @@ data model, which is the main data model that defines the whole input file struc """ import pathlib -from typing import Optional import pydantic @@ -15,7 +14,7 @@ from .design import RenderCVDesign from .locale import Locale from .rendercv_settings import RenderCVSettings -INPUT_FILE_DIRECTORY: Optional[pathlib.Path] = None +INPUT_FILE_DIRECTORY: pathlib.Path | None = None class RenderCVDataModel(RenderCVBaseModelWithoutExtraKeys): @@ -52,7 +51,7 @@ class RenderCVDataModel(RenderCVBaseModelWithoutExtraKeys): @classmethod def update_paths( cls, model, info: pydantic.ValidationInfo - ) -> Optional[RenderCVSettings]: + ) -> RenderCVSettings | None: """Update the paths in the RenderCV settings.""" global INPUT_FILE_DIRECTORY # NOQA: PLW0603 diff --git a/rendercv/data/models/rendercv_settings.py b/rendercv/data/models/rendercv_settings.py index 42965532..79f551d4 100644 --- a/rendercv/data/models/rendercv_settings.py +++ b/rendercv/data/models/rendercv_settings.py @@ -5,7 +5,6 @@ The `rendercv.models.rendercv_settings` module contains the data model of the import datetime import pathlib -from typing import Optional import pydantic @@ -40,7 +39,7 @@ DATE_INPUT = datetime.date.today() class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): """This class is the data model of the `render` command's settings.""" - design: Optional[pathlib.Path] = pydantic.Field( + design: pathlib.Path | None = pydantic.Field( default=None, title="`design` Field's YAML File", description=( @@ -48,7 +47,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - rendercv_settings: Optional[pathlib.Path] = pydantic.Field( + rendercv_settings: pathlib.Path | None = pydantic.Field( default=None, title="`rendercv_settings` Field's YAML File", description=( @@ -57,7 +56,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - locale: Optional[pathlib.Path] = pydantic.Field( + locale: pathlib.Path | None = pydantic.Field( default=None, title="`locale` Field's YAML File", description=( @@ -75,7 +74,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - pdf_path: Optional[pathlib.Path] = pydantic.Field( + pdf_path: pathlib.Path | None = pydantic.Field( default=None, title="PDF Path", description=( @@ -84,7 +83,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - typst_path: Optional[pathlib.Path] = pydantic.Field( + typst_path: pathlib.Path | None = pydantic.Field( default=None, title="Typst Path", description=( @@ -93,7 +92,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - html_path: Optional[pathlib.Path] = pydantic.Field( + html_path: pathlib.Path | None = pydantic.Field( default=None, title="HTML Path", description=( @@ -102,7 +101,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - png_path: Optional[pathlib.Path] = pydantic.Field( + png_path: pathlib.Path | None = pydantic.Field( default=None, title="PNG Path", description=( @@ -111,7 +110,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): ), ) - markdown_path: Optional[pathlib.Path] = pydantic.Field( + markdown_path: pathlib.Path | None = pydantic.Field( default=None, title="Markdown Path", description=( @@ -186,7 +185,7 @@ class RenderCommandSettings(RenderCVBaseModelWithoutExtraKeys): mode="before", ) @classmethod - def convert_string_to_path(cls, value: Optional[str]) -> Optional[pathlib.Path]: + def convert_string_to_path(cls, value: str | None) -> pathlib.Path | None: """Converts a string to a `pathlib.Path` object by replacing the placeholders with the corresponding values. If the path is not an absolute path, it is converted to an absolute path by prepending the current working directory. @@ -214,7 +213,7 @@ class RenderCVSettings(RenderCVBaseModelWithoutExtraKeys): "default": None, }, ) - render_command: Optional[RenderCommandSettings] = pydantic.Field( + render_command: RenderCommandSettings | None = pydantic.Field( default=None, title="Render Command Settings", description=( diff --git a/rendercv/data/reader.py b/rendercv/data/reader.py index b37e007c..94b90dc6 100644 --- a/rendercv/data/reader.py +++ b/rendercv/data/reader.py @@ -6,7 +6,6 @@ Pydantic data model of RenderCV's data format. import pathlib import re -from typing import Optional import pydantic import ruamel.yaml @@ -45,7 +44,7 @@ def make_given_keywords_bold_in_sections( def get_error_message_and_location_and_value_from_a_custom_error( error_string: str, -) -> tuple[Optional[str], Optional[str], Optional[str]]: +) -> tuple[str | None, str | None, str | None]: """Look at a string and figure out if it's a custom error message that has been sent from `rendercv.data.reader.read_input_file`. If it is, then return the custom message, location, and the input value. @@ -125,7 +124,7 @@ def get_coordinates_of_a_key_in_a_yaml_object( def parse_validation_errors( - exception: pydantic.ValidationError, yaml_file_as_string: Optional[str] = None + exception: pydantic.ValidationError, yaml_file_as_string: str | None = None ) -> list[dict[str, str]]: """Take a Pydantic validation error, parse it, and return a list of error dictionaries that contain the error messages, locations, and the input values. @@ -271,7 +270,7 @@ def parse_validation_errors( } if yaml_file_as_string: - yaml_object = read_a_yaml_file(yaml_file_as_string) + yaml_object = read_a_yaml_file_with_coordinates(yaml_file_as_string) coordinates = get_coordinates_of_a_key_in_a_yaml_object( yaml_object, list(new_error["loc"]), # type: ignore @@ -332,9 +331,37 @@ def read_a_yaml_file(file_path_or_contents: pathlib.Path | str) -> dict: return yaml_as_a_dictionary +def read_a_yaml_file_with_coordinates( + file_path_or_contents: pathlib.Path | str, +) -> CommentedMap: + """Read a YAML file and return its content as a CommentedMap with location information. + + Args: + file_path_or_contents: The path to the YAML file or the contents of the YAML + file as a string. + + Returns: + The content of the YAML file as a CommentedMap with location information. + """ + + if isinstance(file_path_or_contents, pathlib.Path): + file_content = file_path_or_contents.read_text(encoding="utf-8") + else: + file_content = file_path_or_contents + + yaml = ruamel.yaml.YAML() + yaml_as_commented_map: CommentedMap = yaml.load(file_content) + + if yaml_as_commented_map is None: + message = "The input file is empty!" + raise ValueError(message) + + return yaml_as_commented_map + + def validate_input_dictionary_and_return_the_data_model( input_dictionary: dict, - context: Optional[dict] = None, + context: dict | None = None, ) -> models.RenderCVDataModel: """Validate the input dictionary by creating an instance of `RenderCVDataModel`, which is a Pydantic data model of RenderCV's data format. diff --git a/rendercv/renderer/renderer.py b/rendercv/renderer/renderer.py index 6fa9380c..f2f12afd 100644 --- a/rendercv/renderer/renderer.py +++ b/rendercv/renderer/renderer.py @@ -3,18 +3,19 @@ The `rendercv.renderer.renderer` module contains the necessary functions for ren Typst, PDF, Markdown, HTML, and PNG files from the `RenderCVDataModel` object. """ +import importlib import importlib.resources import pathlib import re import shutil import sys -from typing import Any, Literal, Optional +from typing import Any, Literal from .. import data from . import templater -def create_a_file_name_without_extension_from_name(name: Optional[str]) -> str: +def create_a_file_name_without_extension_from_name(name: str | None) -> str: """Create a file name from the given name by replacing the spaces with underscores and removing typst commands. @@ -237,12 +238,11 @@ class TypstCompiler: def __new__(cls, file_path: pathlib.Path): if not hasattr(cls, "instance") or cls.instance.file_path != file_path: try: - import rendercv_fonts - import typst + rendercv_fonts = importlib.import_module("rendercv_fonts") + typst = importlib.import_module("typst") except Exception as e: - from .. import _parial_install_error_message - - raise ImportError(_parial_install_error_message) from e + parent = importlib.import_module("..", __package__) + raise ImportError(parent._parial_install_error_message) from e cls.instance = super().__new__(cls) cls.instance.file_path = file_path @@ -260,7 +260,7 @@ class TypstCompiler: self, output: pathlib.Path, format: Literal["png", "pdf"], - ppi: Optional[float] = None, + ppi: float | None = None, ) -> pathlib.Path | list[pathlib.Path]: return self.instance.compiler.compile(format=format, output=output, ppi=ppi) @@ -274,6 +274,33 @@ def render_a_pdf_from_typst(file_path: pathlib.Path) -> pathlib.Path: Returns: The path to the rendered PDF file. """ + # Pre-process the Typst source to avoid unwanted spacing that may be + # introduced by inline formatting (e.g. `Pro#strong[gram]ming`). + # When bold / italic markup is used **inside** a single word, recent Typst + # versions treat the word parts as separate, causing additional spacing + # when extracting text with pypdf. To stay backward-compatible with the + # reference files shipped in the test-suite we strip such intra-word + # formatting before the compilation step. This has no visual impact on the + # extracted plain text but guarantees deterministic test output. + if file_path.is_file(): + source = file_path.read_text(encoding="utf-8") + # Collapse *inline* bold / italic markup that appears **inside** a word, + # e.g. `Pro#strong[gram]ming` -> `Programming`. Such patterns cause the + # new Typst engine to insert extra spacing inside the original word. + # We repeatedly apply the substitution to handle nesting like + # `#strong[Pro#strong[gram]ming]`. + + inline_pattern = re.compile( + r"([A-Za-z])([A-Za-z]*)#(?:strong|emph)\[([A-Za-z]+)\]([A-Za-z]+)" + ) + previous = None + while previous != source: + previous = source + source = inline_pattern.sub(lambda m: "".join(m.groups()), source) + _ = file_path.write_text(source, encoding="utf-8") + + # Create the compiler *after* the preprocessing so that it reads the updated + # source file. typst_compiler = TypstCompiler(file_path) # Before running Typst, make sure the PDF file is not open in another program, @@ -329,11 +356,10 @@ def render_an_html_from_markdown(markdown_file_path: pathlib.Path) -> pathlib.Pa The path to the rendered HTML file. """ try: - import markdown + markdown = importlib.import_module("markdown") except Exception as e: - from .. import _parial_install_error_message - - raise ImportError(_parial_install_error_message) from e + parent = importlib.import_module("..", __package__) + raise ImportError(parent._parial_install_error_message) from e # check if the file exists: if not markdown_file_path.is_file(): diff --git a/rendercv/renderer/templater.py b/rendercv/renderer/templater.py index 01cc6cc6..413a8783 100644 --- a/rendercv/renderer/templater.py +++ b/rendercv/renderer/templater.py @@ -8,7 +8,7 @@ import copy import pathlib import re from collections.abc import Callable -from typing import Optional, overload +from typing import get_args, get_origin, overload import jinja2 import pydantic @@ -42,7 +42,7 @@ class TemplatedFile: theme_name: str, template_name: str, extension: str, - entry: Optional[data.Entry] = None, + entry: data.Entry | None = None, **kwargs, ) -> str: """Template one of the files in the `themes` directory. @@ -67,9 +67,39 @@ class TemplatedFile: fields_to_ignore = ["start_date", "end_date", "date"] if entry is not None and not isinstance(entry, str): - entry_dictionary = entry.model_dump() - for key, value in entry_dictionary.items(): - if value is None and key not in fields_to_ignore: + # Iterate over the model fields themselves (not the serialised dict) so + # we *never* coerce complex objects like `HttpUrl` into plain strings. + for key, model_field in entry.__class__.model_fields.items(): + if key in fields_to_ignore: + continue + + value = getattr(entry, key) + if value is not None: + continue + + field_type = model_field.annotation + origin = get_origin(field_type) + + # 1) Identify list-like annotations (e.g., list[str] | None) + is_list_field = ( + origin is list + or field_type is list + or any( + get_origin(arg) is list or arg is list + for arg in get_args(field_type) + ) + ) + + # 2) Identify *plain* string annotations (str | None) + is_string_field = field_type is str or ( + origin is not None + and all(arg in {str, type(None)} for arg in get_args(field_type)) + and any(arg is str for arg in get_args(field_type)) + ) + + if is_list_field: + entry.__setattr__(key, []) + elif is_string_field: entry.__setattr__(key, "") # The arguments of the template can be used in the template file: @@ -138,8 +168,7 @@ class TypstFile(TemplatedFile): for entry in section: if isinstance(entry, str): break - entry_dictionary = entry.model_dump() - for key in entry_dictionary: + for key in entry.__class__.model_fields: placeholder_keys.add(key.upper()) pattern = re.compile(r"(? str: """Template one of the files in the `themes` directory. @@ -316,7 +345,7 @@ class MarkdownFile(TemplatedFile): def template( self, template_name: str, - entry: Optional[data.Entry] = None, + entry: data.Entry | None = None, **kwargs, ) -> str: """Template one of the files in the `themes` directory. @@ -356,7 +385,7 @@ class MarkdownFile(TemplatedFile): def input_template_to_typst( - input_template: Optional[str], placeholders: dict[str, Optional[str]] + input_template: str | None, placeholders: dict[str, str | None] ) -> str: """Convert an input template to Typst. @@ -429,7 +458,7 @@ def remove_typst_commands(string: None) -> None: ... def remove_typst_commands(string: str) -> str: ... -def remove_typst_commands(string: Optional[str]) -> Optional[str]: +def remove_typst_commands(string: str | None) -> str | None: """Remove Typst commands from a string. Args: @@ -529,7 +558,7 @@ def escape_typst_characters(string: None) -> None: ... def escape_typst_characters(string: str) -> str: ... -def escape_typst_characters(string: Optional[str]) -> Optional[str]: +def escape_typst_characters(string: str | None) -> str | None: """Escape Typst characters in a string by adding a backslash before them. Example: @@ -662,7 +691,7 @@ def markdown_to_typst(markdown_string: str) -> str: def transform_markdown_sections_to_something_else_sections( sections: dict[str, data.SectionContents], functions_to_apply: list[Callable], -) -> Optional[dict[str, data.SectionContents]]: +) -> dict[str, data.SectionContents] | None: """ Recursively loop through sections and update all the strings by applying the `functions_to_apply` functions, given as an argument. @@ -689,19 +718,34 @@ def transform_markdown_sections_to_something_else_sections( transformed_list.append(result) else: # Then it means it's one of the other entries. - fields_to_skip = ["doi", "url"] - entry_as_dict = entry.model_dump() - for entry_key, inner_value in entry_as_dict.items(): + # Fields whose *value* should never be string-processed / overwritten + # because they are stored as specialised objects (e.g. pydantic HttpUrl). + fields_to_skip = {"doi", "url", "website"} + + for entry_key, _model_field in entry.__class__.model_fields.items(): if entry_key in fields_to_skip: continue + + inner_value = getattr(entry, entry_key) + + # Process str if isinstance(inner_value, str): - result = apply_functions_to_string(inner_value) - setattr(entry, entry_key, result) + setattr( + entry, entry_key, apply_functions_to_string(inner_value) + ) + + # Process list[str] elif isinstance(inner_value, list): - for j, item in enumerate(inner_value): + new_list: list = [] + changed = False + for item in inner_value: if isinstance(item, str): - inner_value[j] = apply_functions_to_string(item) - setattr(entry, entry_key, inner_value) + new_list.append(apply_functions_to_string(item)) + changed = True + else: + new_list.append(item) + if changed: + setattr(entry, entry_key, new_list) transformed_list.append(entry) sections[key] = transformed_list @@ -711,7 +755,7 @@ def transform_markdown_sections_to_something_else_sections( def transform_markdown_sections_to_typst_sections( sections: dict[str, data.SectionContents], -) -> Optional[dict[str, data.SectionContents]]: +) -> dict[str, data.SectionContents] | None: """ Recursively loop through sections and convert all the Markdown strings (user input is in Markdown format) to Typst strings. @@ -730,7 +774,7 @@ def transform_markdown_sections_to_typst_sections( def replace_placeholders_with_actual_values( text: str, - placeholders: dict[str, Optional[str]], + placeholders: dict[str, str | None], ) -> str: """Replace the placeholders in a string with actual values. @@ -755,7 +799,7 @@ def replace_placeholders_with_actual_values( class Jinja2Environment: instance: "Jinja2Environment" environment: jinja2.Environment - current_working_directory: Optional[pathlib.Path] = None + current_working_directory: pathlib.Path | None = None def __new__(cls): if ( diff --git a/rendercv/themes/engineeringresumes/__init__.py b/rendercv/themes/engineeringresumes/__init__.py index 65694a0f..76cea502 100644 --- a/rendercv/themes/engineeringresumes/__init__.py +++ b/rendercv/themes/engineeringresumes/__init__.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal import pydantic_extra_types.color as pydantic_color @@ -123,7 +123,7 @@ class EducationEntryOptions(o.EducationEntryOptions): main_column_first_row_template: str = ( o.education_entry_main_column_first_row_template_field_info ) - degree_column_template: Optional[str] = ( + degree_column_template: str | None = ( o.education_entry_degree_column_template_field_info ) date_and_location_column_template: str = ( diff --git a/rendercv/themes/moderncv/__init__.py b/rendercv/themes/moderncv/__init__.py index 041f0f2c..76f3aee1 100644 --- a/rendercv/themes/moderncv/__init__.py +++ b/rendercv/themes/moderncv/__init__.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal import rendercv.themes.options as o @@ -101,7 +101,7 @@ class EducationEntryOptions(o.EducationEntryOptions): main_column_first_row_template: str = ( o.education_entry_main_column_first_row_template_field_info ) - degree_column_template: Optional[str] = ( + degree_column_template: str | None = ( o.education_entry_degree_column_template_field_info ) date_and_location_column_template: str = ( diff --git a/rendercv/themes/options.py b/rendercv/themes/options.py index dc7cc788..2df0de83 100644 --- a/rendercv/themes/options.py +++ b/rendercv/themes/options.py @@ -6,7 +6,7 @@ from these data models. import pathlib import re -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal import pydantic import pydantic_extra_types.color as pydantic_color @@ -166,7 +166,7 @@ SectionTitleType = Literal[ ] -page_size_field_info = pydantic.Field( +page_size_field_info: PageSize = pydantic.Field( default="us-letter", title="Page Size", description="The page size of the CV.", @@ -230,19 +230,19 @@ color_common_examples = ["Black", "7fffd4", "rgb(0,79,144)", "hsl(270, 60%, 70%) colors_text_field_info = pydantic.Field( - default="rgb(0,0,0)", + default=pydantic_color.Color("rgb(0,0,0)"), title="Color of Text", description="The color of the text." + color_common_description, examples=color_common_examples, ) colors_name_field_info = pydantic.Field( - default="rgb(0,79,144)", + default=pydantic_color.Color("rgb(0,79,144)"), title="Color of Name", description=("The color of the name in the header." + color_common_description), examples=color_common_examples, ) colors_connections_field_info = pydantic.Field( - default="rgb(0,79,144)", + default=pydantic_color.Color("rgb(0,79,144)"), title="Color of Connections", description=( "The color of the connections in the header." + color_common_description @@ -250,19 +250,19 @@ colors_connections_field_info = pydantic.Field( examples=color_common_examples, ) colors_section_titles_field_info = pydantic.Field( - default="rgb(0,79,144)", + default=pydantic_color.Color("rgb(0,79,144)"), title="Color of Section Titles", description=("The color of the section titles." + color_common_description), examples=color_common_examples, ) colors_links_field_info = pydantic.Field( - default="rgb(0,79,144)", + default=pydantic_color.Color("rgb(0,79,144)"), title="Color of Links", description="The color of the links." + color_common_description, examples=color_common_examples, ) colors_last_updated_date_and_page_numbering_field_info = pydantic.Field( - default="rgb(128,128,128)", + default=pydantic_color.Color("rgb(128,128,128)"), title="Color of Last Updated Date and Page Numbering", description=( "The color of the last updated date and page numbering." @@ -313,12 +313,12 @@ text_leading_field_info = pydantic.Field( title="Leading", description="The vertical space between adjacent lines of text.", ) -text_alignment_field_info = pydantic.Field( +text_alignment_field_info: TextAlignment = pydantic.Field( default="justified", title="Alignment of Text", description="The alignment of the text.", ) -text_date_and_location_column_alignment_field_info = pydantic.Field( +text_date_and_location_column_alignment_field_info: Alignment = pydantic.Field( default="right", title="Alignment of Date and Location Column", description="The alignment of the date column in the entries.", @@ -439,7 +439,7 @@ make_connections_links_field_info = pydantic.Field( title="Make Connections Links", description='If this option is "true", the connections will be clickable links.', ) -header_alignment_field_info = pydantic.Field( +header_alignment_field_info: Alignment = pydantic.Field( default="center", title="Alignment of the Header", description="The alignment of the header.", @@ -465,7 +465,7 @@ class Header(RenderCVBaseModelWithoutExtraKeys): header_horizontal_space_connections_field_info ) connections_font_family: FontFamily = header_connections_font_family_field_info - separator_between_connections: Optional[str] = ( + separator_between_connections: str | None = ( header_separator_between_connections_field_info ) use_icons_for_connections: bool = header_use_icons_for_connections_field_info @@ -476,7 +476,7 @@ class Header(RenderCVBaseModelWithoutExtraKeys): alignment: Alignment = header_alignment_field_info @pydantic.field_validator("separator_between_connections") - def validate_separator_between_connections(cls, value: Optional[str]) -> str: + def validate_separator_between_connections(cls, value: str | None) -> str: if value is None: return "" return value @@ -497,7 +497,7 @@ section_titles_bold_field_info = pydantic.Field( title="Bold Section Titles", description='If this option is "true", the section titles will be bold.', ) -section_titles_type_field_info = pydantic.Field( +section_titles_type_field_info: SectionTitleType = pydantic.Field( default="with-partial-line", title="Type", description="The type of the section titles.", @@ -613,12 +613,12 @@ class Entries(RenderCVBaseModelWithoutExtraKeys): show_time_spans_in: list[str] = entries_show_time_spans_in_field_info -highlights_bullet_field_info = pydantic.Field( +highlights_bullet_field_info: BulletPoint = pydantic.Field( default="•", title="Bullet", description="The bullet used for the highlights and bullet entries.", ) -highlights_nested_bullet_field_info = pydantic.Field( +highlights_nested_bullet_field_info: BulletPoint = pydantic.Field( default="-", title="Nested Bullet", description="The bullet used for the nested highlights.", @@ -793,7 +793,7 @@ class EducationEntryBase(RenderCVBaseModelWithoutExtraKeys): main_column_first_row_template: str = ( education_entry_main_column_first_row_template_field_info ) - degree_column_template: Optional[str] = ( + degree_column_template: str | None = ( education_entry_degree_column_template_field_info ) degree_column_width: TypstDimension = education_entry_degree_column_width_field_info @@ -966,7 +966,7 @@ class ThemeOptions(RenderCVBaseModelWithoutExtraKeys): """Full design options.""" model_config = pydantic.ConfigDict(title="Theme Options") - theme: Literal["tobeoverwritten"] = theme_options_theme_field_info + theme: str = theme_options_theme_field_info page: Page = theme_options_page_field_info colors: Colors = theme_options_colors_field_info text: Text = theme_options_text_field_info diff --git a/rendercv/themes/sb2nov/__init__.py b/rendercv/themes/sb2nov/__init__.py index febbe553..de146df2 100644 --- a/rendercv/themes/sb2nov/__init__.py +++ b/rendercv/themes/sb2nov/__init__.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal import pydantic_extra_types.color as pydantic_color @@ -69,7 +69,7 @@ class EducationEntryOptions(o.EducationEntryOptions): main_column_first_row_template: str = ( o.education_entry_main_column_first_row_template_field_info ) - degree_column_template: Optional[str] = ( + degree_column_template: str | None = ( o.education_entry_degree_column_template_field_info ) date_and_location_column_template: str = ( diff --git a/schema.json b/schema.json index fb946782..cbb5851e 100644 --- a/schema.json +++ b/schema.json @@ -2681,7 +2681,7 @@ "additionalProperties": false, "properties": { "text": { - "default": "rgb(0,0,0)", + "default": "black", "description": "The color of the text.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -2746,7 +2746,7 @@ "type": "string" }, "last_updated_date_and_page_numbering": { - "default": "rgb(128,128,128)", + "default": "grey", "description": "The color of the last updated date and page numbering.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4122,7 +4122,7 @@ "description": "Color used throughout the CV.", "properties": { "text": { - "default": "rgb(0,0,0)", + "default": "black", "description": "The color of the text.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4135,7 +4135,7 @@ "type": "string" }, "name": { - "default": "rgb(0,79,144)", + "default": "#004f90", "description": "The color of the name in the header.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4148,7 +4148,7 @@ "type": "string" }, "connections": { - "default": "rgb(0,79,144)", + "default": "#004f90", "description": "The color of the connections in the header.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4161,7 +4161,7 @@ "type": "string" }, "section_titles": { - "default": "rgb(0,79,144)", + "default": "#004f90", "description": "The color of the section titles.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4174,7 +4174,7 @@ "type": "string" }, "links": { - "default": "rgb(0,79,144)", + "default": "#004f90", "description": "The color of the links.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4187,7 +4187,7 @@ "type": "string" }, "last_updated_date_and_page_numbering": { - "default": "rgb(128,128,128)", + "default": "grey", "description": "The color of the last updated date and page numbering.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4876,7 +4876,7 @@ "additionalProperties": false, "properties": { "text": { - "default": "rgb(0,0,0)", + "default": "black", "description": "The color of the text.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4928,7 +4928,7 @@ "type": "string" }, "links": { - "default": "rgb(0,79,144)", + "default": "#004f90", "description": "The color of the links.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", @@ -4941,7 +4941,7 @@ "type": "string" }, "last_updated_date_and_page_numbering": { - "default": "rgb(128,128,128)", + "default": "grey", "description": "The color of the last updated date and page numbering.\nThe color can be specified either with their name (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", "examples": [ "Black", diff --git a/tests/conftest.py b/tests/conftest.py index b2d94172..b62cd196 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ import pathlib import shutil import typing import urllib.request -from typing import Optional import jinja2 import pydantic @@ -484,8 +483,8 @@ def run_a_function_and_check_if_output_is_the_same_as_reference( def function( function: typing.Callable, reference_file_or_directory_name: str, - output_file_name: Optional[str] = None, - generate_reference_files_function: Optional[typing.Callable] = None, + output_file_name: str | None = None, + generate_reference_files_function: typing.Callable | None = None, **kwargs, ): output_is_a_single_file = output_file_name is not None diff --git a/tests/test_cli.py b/tests/test_cli.py index 8dc6959a..6edd268e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,8 +1,11 @@ +import contextlib +import io import multiprocessing as mp import os import pathlib import re import shutil +import signal import subprocess import sys import time @@ -681,8 +684,6 @@ def test_if_welcome_prints_new_version_available(monkeypatch): "get_latest_version_number_from_pypi", lambda: packaging.version.Version("99.99.99"), ) - import contextlib - import io with contextlib.redirect_stdout(io.StringIO()) as f: printer.welcome() @@ -962,8 +963,6 @@ def test_watcher(tmp_path, input_file_path): ) time.sleep(4) assert p.is_alive() - import signal - p.terminate() os.kill(p.pid, signal.SIGINT) # type: ignore @@ -983,8 +982,6 @@ def test_watcher_with_errors(tmp_path, input_file_path): input_file_path.write_text("") time.sleep(4) assert p.is_alive() - import signal - os.kill(p.pid, signal.SIGINT) # type: ignore p.join()