python 3.9 support

This commit is contained in:
Eric Yu 2024-07-04 13:37:30 -07:00
parent 503c7133b7
commit ed5b0d48a4
3 changed files with 75 additions and 50 deletions

7
.gitignore vendored
View File

@ -175,10 +175,11 @@ cython_debug/
.vscode/ .vscode/
# Personal CVs # Personal CVs
*_CV.yaml *CV.yaml
*_cv.py *cv.py
*_CV.tex *CV.tex
rendercv_output/ rendercv_output/
results/
# Include reference files # Include reference files
!/tests/testdata/**/*.pdf !/tests/testdata/**/*.pdf

View File

@ -23,7 +23,7 @@ import pathlib
import re import re
import warnings import warnings
from datetime import date as Date from datetime import date as Date
from typing import Annotated, Any, Literal, Optional, Type, get_args from typing import Annotated, Any, Literal, Optional, Type, get_args, Union, Dict
import annotated_types as at import annotated_types as at
import pydantic import pydantic
@ -43,7 +43,7 @@ warnings.filterwarnings("ignore")
locale_catalog = {} locale_catalog = {}
def get_date_object(date: str | int) -> Date: def get_date_object(date: Union[str, int]) -> Date:
"""Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a """Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
`datetime.date` object. This function is used throughout the validation process of `datetime.date` object. This function is used throughout the validation process of
the data models. the data models.
@ -163,7 +163,7 @@ class BulletEntry(RenderCVBaseModel):
class EntryWithDate(RenderCVBaseModel): class EntryWithDate(RenderCVBaseModel):
date: Optional[int | str] = pydantic.Field( date: Optional[Union[int, str]] = pydantic.Field(
default=None, default=None,
title="Date", title="Date",
description=( description=(
@ -176,8 +176,8 @@ class EntryWithDate(RenderCVBaseModel):
@pydantic.field_validator("date", mode="before") @pydantic.field_validator("date", mode="before")
@classmethod @classmethod
def check_date( def check_date(
cls, date: Optional[int | RenderCVDate | str] cls, date: Optional[Union[int, RenderCVDate, str]]
) -> Optional[int | RenderCVDate | str]: ) -> Optional[Union[int, RenderCVDate, str]]:
"""Check if `date` is provided correctly.""" """Check if `date` is provided correctly."""
date_is_provided = date is not None date_is_provided = date is not None
@ -294,7 +294,7 @@ class EntryBase(EntryWithDate):
description="The location of the event.", description="The location of the event.",
examples=["Istanbul, Türkiye"], examples=["Istanbul, Türkiye"],
) )
start_date: Optional[int | RenderCVDate] = pydantic.Field( start_date: Optional[Union[int, RenderCVDate]] = pydantic.Field(
default=None, default=None,
title="Start Date", title="Start Date",
description=( description=(
@ -302,7 +302,7 @@ class EntryBase(EntryWithDate):
), ),
examples=["2020-09-24"], examples=["2020-09-24"],
) )
end_date: Optional[Literal["present"] | int | RenderCVDate] = pydantic.Field( end_date: Optional[Union[Literal["present"], int, RenderCVDate]] = pydantic.Field(
default=None, default=None,
title="End Date", title="End Date",
description=( description=(
@ -323,8 +323,8 @@ class EntryBase(EntryWithDate):
@classmethod @classmethod
def check_and_parse_dates( def check_and_parse_dates(
cls, cls,
date: Optional[Literal["present"] | int | RenderCVDate], date: Optional[Union[Literal["present"], int, RenderCVDate]],
) -> Optional[Literal["present"] | int | RenderCVDate]: ) -> Optional[Union[Literal["present"], int, RenderCVDate]]:
date_is_provided = date is not None date_is_provided = date is not None
if date_is_provided: if date_is_provided:
@ -615,23 +615,36 @@ class EducationEntry(EntryBase, EducationEntryBase):
# Create custom types named Entry and ListOfEntries: # Create custom types named Entry and ListOfEntries:
# Entry = (
# OneLineEntry
# | NormalEntry
# | ExperienceEntry
# | EducationEntry
# | PublicationEntry
# | BulletEntry
# | str
# )
Entry = ( Entry = (
Union[
OneLineEntry OneLineEntry
| NormalEntry , NormalEntry
| ExperienceEntry , ExperienceEntry
| EducationEntry , EducationEntry
| PublicationEntry , PublicationEntry
| BulletEntry , BulletEntry
| str , str
]
) )
ListOfEntries = list[ ListOfEntries = list[
Union[
OneLineEntry OneLineEntry
| NormalEntry , NormalEntry
| ExperienceEntry , ExperienceEntry
| EducationEntry , EducationEntry
| PublicationEntry , PublicationEntry
| BulletEntry , BulletEntry
| str , str
]
] ]
entry_types = Entry.__args__[:-1] # a tuple of all the entry types except str entry_types = Entry.__args__[:-1] # a tuple of all the entry types except str
entry_type_names = [entry_type.__name__ for entry_type in entry_types] + ["TextEntry"] entry_type_names = [entry_type.__name__ for entry_type in entry_types] + ["TextEntry"]
@ -691,7 +704,7 @@ def create_a_section_model(entry_type: Type[Entry]) -> Type[SectionBase]:
def get_entry_and_section_type( def get_entry_and_section_type(
entry: dict[str, Any] | Entry, entry: Union[dict[str, Any], Entry],
) -> tuple[ ) -> tuple[
str, str,
Type[SectionBase], Type[SectionBase],
@ -738,8 +751,8 @@ def get_entry_and_section_type(
def validate_section_input( def validate_section_input(
sections_input: SectionBase | list[Any], sections_input: Union[SectionBase, list[Any]],
) -> SectionBase | list[Any]: ) -> Union[SectionBase, list[Any]]:
"""Validate a `SectionInput` object and raise an error if it is not valid. """Validate a `SectionInput` object and raise an error if it is not valid.
Sections input is very complex. It is either a `Section` object or a list of Sections input is very complex. It is either a `Section` object or a list of
@ -1166,11 +1179,19 @@ LocaleCatalog() # Initialize the locale catalog with the default values
# It is a union of all the design options and the correct design option is determined by # It is a union of all the design options and the correct design option is determined by
# the theme field, thanks to Pydantic's discriminator feature. # the theme field, thanks to Pydantic's discriminator feature.
# See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information # See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information
# RenderCVDesign = Annotated[
# ClassicThemeOptions
# | ModerncvThemeOptions
# | Sb2novThemeOptions
# | EngineeringresumesThemeOptions,
# pydantic.Field(discriminator="theme"),
# ]
RenderCVDesign = Annotated[ RenderCVDesign = Annotated[
ClassicThemeOptions Union
| ModerncvThemeOptions [ClassicThemeOptions
| Sb2novThemeOptions , ModerncvThemeOptions
| EngineeringresumesThemeOptions, , Sb2novThemeOptions
, EngineeringresumesThemeOptions],
pydantic.Field(discriminator="theme"), pydantic.Field(discriminator="theme"),
] ]
rendercv_design_validator = pydantic.TypeAdapter(RenderCVDesign) rendercv_design_validator = pydantic.TypeAdapter(RenderCVDesign)
@ -1184,7 +1205,7 @@ class RenderCVDataModel(RenderCVBaseModel):
title="Curriculum Vitae", title="Curriculum Vitae",
description="The data of the CV.", description="The data of the CV.",
) )
design: pydantic.json_schema.SkipJsonSchema[Any] | RenderCVDesign = pydantic.Field( design: Union[pydantic.json_schema.SkipJsonSchema[Any], RenderCVDesign] = pydantic.Field(
default=ClassicThemeOptions(theme="classic"), default=ClassicThemeOptions(theme="classic"),
title="Design", title="Design",
description=( description=(
@ -1203,8 +1224,8 @@ class RenderCVDataModel(RenderCVBaseModel):
@pydantic.field_validator("design", mode="before") @pydantic.field_validator("design", mode="before")
@classmethod @classmethod
def initialize_if_custom_theme_is_used( def initialize_if_custom_theme_is_used(
cls, design: RenderCVDesign | Any cls, design: Union[RenderCVDesign, Any]
) -> RenderCVDesign | Any: ) -> Union[RenderCVDesign, Any]:
"""Initialize the custom theme if it is used and validate it. Otherwise, return """Initialize the custom theme if it is used and validate it. Otherwise, return
the built-in theme.""" the built-in theme."""
# `get_args` for an Annotated object returns the arguments when Annotated is # `get_args` for an Annotated object returns the arguments when Annotated is
@ -1212,7 +1233,10 @@ class RenderCVDataModel(RenderCVBaseModel):
# access the first argument to use isinstance function. # access the first argument to use isinstance function.
theme_data_model_types = get_args(RenderCVDesign)[0] theme_data_model_types = get_args(RenderCVDesign)[0]
if isinstance(design, theme_data_model_types): # Convert the arguments to a tuple
theme_data_model_types_tuple = tuple(theme_data_model_types.__args__) if hasattr(theme_data_model_types, '__args__') else (theme_data_model_types,)
if isinstance(design, theme_data_model_types_tuple):
# Then it means RenderCVDataModel is already initialized with a design, so # Then it means RenderCVDataModel is already initialized with a design, so
# return it as is: # return it as is:
return design return design
@ -1346,10 +1370,10 @@ def dictionary_key_to_proper_section_title(key: str) -> str:
def set_or_update_a_value( def set_or_update_a_value(
data_model: pydantic.BaseModel | dict | list, data_model: Union[pydantic.BaseModel, dict, list],
key: str, key: str,
value: str, value: str,
sub_model: pydantic.BaseModel | dict | list = None, sub_model: Union[pydantic.BaseModel, dict, list] = None,
): ):
"""Set or update a value in a data model for a specific key. For example, a key can """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". be `cv.sections.education.3.institution` and the value can be "Bogazici University".
@ -1419,7 +1443,7 @@ def set_or_update_a_value(
def read_input_file( def read_input_file(
file_path_or_contents: pathlib.Path | str, file_path_or_contents: Union[pathlib.Path, str],
) -> RenderCVDataModel: ) -> RenderCVDataModel:
"""Read the input file and return two instances of """Read the input file and return two instances of
[RenderCVDataModel][rendercv.data_models.RenderCVDataModel]. The first instance is [RenderCVDataModel][rendercv.data_models.RenderCVDataModel]. The first instance is
@ -1460,7 +1484,7 @@ def read_input_file(
else: else:
file_content = file_path_or_contents file_content = file_path_or_contents
input_as_dictionary: dict[str, Any] = ruamel.yaml.YAML().load(file_content) # type: ignore input_as_dictionary: Dict[str, Any] = ruamel.yaml.YAML().load(file_content) # type: ignore
# Validate the parsed dictionary by creating an instance of RenderCVDataModel: # Validate the parsed dictionary by creating an instance of RenderCVDataModel:
rendercv_data_model = RenderCVDataModel(**input_as_dictionary) rendercv_data_model = RenderCVDataModel(**input_as_dictionary)

View File

@ -1,4 +1,4 @@
from typing import Literal from typing import Literal, Union
import pydantic import pydantic
@ -23,16 +23,16 @@ class ModerncvThemeOptions(pydantic.BaseModel):
description='The page size of the CV. The default value is "letterpaper".', description='The page size of the CV. The default value is "letterpaper".',
examples=["a4paper", "letterpaper"], examples=["a4paper", "letterpaper"],
) )
color: ( color: Union[
Literal["blue"] Literal["blue"],
| Literal["black"] Literal["black"],
| Literal["burgundy"] Literal["burgundy"],
| Literal["green"] Literal["green"],
| Literal["grey"] Literal["grey"],
| Literal["orange"] Literal["orange"],
| Literal["purple"] Literal["purple"],
| Literal["red"] Literal["red"],
) = pydantic.Field( ]= pydantic.Field(
default="blue", default="blue",
validate_default=True, validate_default=True,
title="Primary Color", title="Primary Color",