mirror of
https://github.com/rendercv/rendercv.git
synced 2026-04-17 13:33:53 -04:00
Address code review feedback on Hypothesis and classic_theme changes
- Update hypothesis to latest version (>=6.151.9) - Remove pythonpath pytest config (was only needed for tests.strategies) - Consolidate classic_theme.py into single file with all design models - Move Hypothesis strategies from strategies.py into their test files - Add noqa: ARG001 to unused yaml_field_override CLI parameter - Fix lint and type errors across the codebase
This commit is contained in:
@@ -25,12 +25,13 @@ from rendercv.schema.models.cv.section import (
|
||||
from rendercv.schema.models.cv.social_network import available_social_networks
|
||||
from rendercv.schema.models.design.built_in_design import available_themes
|
||||
from rendercv.schema.models.design.classic_theme import (
|
||||
Alignment,
|
||||
BodyAlignment,
|
||||
Bullet,
|
||||
PageSize,
|
||||
PhoneNumberFormatType,
|
||||
SectionTitleType,
|
||||
)
|
||||
from rendercv.schema.models.design.header import PhoneNumberFormatType
|
||||
from rendercv.schema.models.design.typography import Alignment, BodyAlignment
|
||||
from rendercv.schema.models.design.font_family import available_font_families
|
||||
from rendercv.schema.models.locale.locale import available_locales
|
||||
from rendercv.schema.yaml_reader import read_yaml
|
||||
|
||||
@@ -87,7 +87,7 @@ rendercv = "rendercv.cli.entry_point:entry_point"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=26.3.1", # Format the code
|
||||
"hypothesis>=6.100.0", # Property-based testing
|
||||
"hypothesis>=6.151.9", # Property-based testing
|
||||
"prek>=0.3.6", # Run checks before committing (pre-commit alternative)
|
||||
"pytest>=9.0.2", # Run tests
|
||||
"pytest-cov>=7.0.0", # Coverage plugin for pytest with xdist support
|
||||
@@ -223,7 +223,6 @@ addopts = [
|
||||
"--numprocesses=auto", # Number of processes in parallel
|
||||
]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["."]
|
||||
|
||||
[tool.codespell]
|
||||
skip = "*.md"
|
||||
|
||||
4618
schema.json
4618
schema.json
File diff suppressed because it is too large
Load Diff
@@ -186,7 +186,7 @@ def cli_command_render(
|
||||
),
|
||||
] = False,
|
||||
# Dummy argument that only exists to show the override syntax in --help:
|
||||
yaml_field_override: Annotated[
|
||||
yaml_field_override: Annotated[ # noqa: ARG001
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--YAMLLOCATION",
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import date as Date
|
||||
from rendercv.exception import RenderCVInternalError
|
||||
from rendercv.schema.models.cv.entries.publication import PublicationEntry
|
||||
from rendercv.schema.models.cv.section import Entry
|
||||
from rendercv.schema.models.design.templates import Templates
|
||||
from rendercv.schema.models.design.classic_theme import Templates
|
||||
from rendercv.schema.models.locale.locale import Locale
|
||||
|
||||
from .date import compute_time_span_string, format_date_range, format_single_date
|
||||
@@ -450,10 +450,10 @@ def remove_not_provided_placeholders(
|
||||
"""
|
||||
# Remove the not provided placeholders from the templates, including characters
|
||||
# around them:
|
||||
used_placeholders_in_templates = set(
|
||||
used_placeholders_in_templates: set[str] = set(
|
||||
uppercase_word_pattern.findall(" ".join(entry_templates.values()))
|
||||
)
|
||||
not_provided_placeholders = used_placeholders_in_templates - set(
|
||||
not_provided_placeholders: set[str] = used_placeholders_in_templates - set(
|
||||
entry_fields.keys()
|
||||
)
|
||||
if not_provided_placeholders:
|
||||
@@ -474,7 +474,7 @@ def remove_not_provided_placeholders(
|
||||
# Sort longest-first so e.g. "AAA" matches before "AA":
|
||||
sorted_placeholders = sorted(not_provided_placeholders, key=len, reverse=True)
|
||||
not_provided_placeholders_pattern = re.compile(
|
||||
r"\S*\b(?:" + "|".join(sorted_placeholders) + r")\b\S*"
|
||||
r"\S*\b(?:" + "|".join(sorted_placeholders) + r")\b\S*" # ty: ignore[no-matching-overload]
|
||||
)
|
||||
entry_templates = {
|
||||
key: clean_trailing_parts(
|
||||
|
||||
@@ -158,7 +158,4 @@ def clean_url(url: str | pydantic.HttpUrl) -> str:
|
||||
Returns:
|
||||
Clean URL string.
|
||||
"""
|
||||
url = str(url).replace("https://", "").replace("http://", "")
|
||||
url = url.rstrip("/")
|
||||
|
||||
return url
|
||||
return str(url).replace("https://", "").replace("http://", "").rstrip("/")
|
||||
|
||||
@@ -4,35 +4,16 @@ import pydantic
|
||||
|
||||
from rendercv.schema.models.base import BaseModelWithoutExtraKeys
|
||||
from rendercv.schema.models.design.color import Color
|
||||
from rendercv.schema.models.design.header import (
|
||||
Header,
|
||||
Links,
|
||||
from rendercv.schema.models.design.font_family import FontFamily as FontFamilyType
|
||||
from rendercv.schema.models.design.typst_dimension import (
|
||||
TypstDimension,
|
||||
length_common_description,
|
||||
)
|
||||
from rendercv.schema.models.design.templates import (
|
||||
Templates,
|
||||
)
|
||||
from rendercv.schema.models.design.typography import (
|
||||
Typography,
|
||||
)
|
||||
from rendercv.schema.models.design.typst_dimension import TypstDimension
|
||||
|
||||
type Bullet = Literal["●", "•", "◦", "-", "◆", "★", "■", "—", "○"]
|
||||
type SectionTitleType = Literal[
|
||||
"with_partial_line",
|
||||
"with_full_line",
|
||||
"without_line",
|
||||
"moderncv",
|
||||
"centered_without_line",
|
||||
"centered_with_partial_line",
|
||||
"centered_with_centered_partial_line",
|
||||
"centered_with_full_line",
|
||||
]
|
||||
# Page
|
||||
|
||||
type PageSize = Literal["a4", "a5", "us-letter", "us-executive"]
|
||||
|
||||
length_common_description = (
|
||||
"It can be specified with units (cm, in, pt, mm, em). For example, `0.1cm`."
|
||||
)
|
||||
|
||||
|
||||
class Page(BaseModelWithoutExtraKeys):
|
||||
size: PageSize = pydantic.Field(
|
||||
@@ -73,6 +54,8 @@ class Page(BaseModelWithoutExtraKeys):
|
||||
)
|
||||
|
||||
|
||||
# Colors
|
||||
|
||||
color_common_description = (
|
||||
"The color can be specified either with their name"
|
||||
" (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB"
|
||||
@@ -133,6 +116,328 @@ class Colors(BaseModelWithoutExtraKeys):
|
||||
)
|
||||
|
||||
|
||||
# Typography
|
||||
|
||||
type BodyAlignment = Literal["left", "justified", "justified-with-no-hyphenation"]
|
||||
type Alignment = Literal["left", "center", "right"]
|
||||
|
||||
|
||||
class FontFamily(BaseModelWithoutExtraKeys):
|
||||
body: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for body text. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
name: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for the name. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
headline: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for the headline. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
connections: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for connections. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
section_titles: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for section titles. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FontSize(BaseModelWithoutExtraKeys):
|
||||
body: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for body text. The default value is `10pt`.",
|
||||
)
|
||||
name: TypstDimension = pydantic.Field(
|
||||
default="30pt",
|
||||
description="The font size for the name. The default value is `30pt`.",
|
||||
)
|
||||
headline: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for the headline. The default value is `10pt`.",
|
||||
)
|
||||
connections: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for connections. The default value is `10pt`.",
|
||||
)
|
||||
section_titles: TypstDimension = pydantic.Field(
|
||||
default="1.4em",
|
||||
description="The font size for section titles. The default value is `1.4em`.",
|
||||
)
|
||||
|
||||
|
||||
class SmallCaps(BaseModelWithoutExtraKeys):
|
||||
name: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for the name. The default value is `false`."
|
||||
),
|
||||
)
|
||||
headline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for the headline. The default value is `false`."
|
||||
),
|
||||
)
|
||||
connections: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for connections. The default value is `false`."
|
||||
),
|
||||
)
|
||||
section_titles: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for section titles. The default value is"
|
||||
" `false`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Bold(BaseModelWithoutExtraKeys):
|
||||
name: bool = pydantic.Field(
|
||||
default=True,
|
||||
description="Whether to make the name bold. The default value is `true`.",
|
||||
)
|
||||
headline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Whether to make the headline bold. The default value is `false`.",
|
||||
)
|
||||
connections: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Whether to make connections bold. The default value is `false`.",
|
||||
)
|
||||
section_titles: bool = pydantic.Field(
|
||||
default=True,
|
||||
description="Whether to make section titles bold. The default value is `true`.",
|
||||
)
|
||||
|
||||
|
||||
class Typography(BaseModelWithoutExtraKeys):
|
||||
line_spacing: TypstDimension = pydantic.Field(
|
||||
default="0.6em",
|
||||
description=(
|
||||
"Space between lines of text. Larger values create more vertical space. The"
|
||||
" default value is `0.6em`."
|
||||
),
|
||||
)
|
||||
alignment: Literal["left", "justified", "justified-with-no-hyphenation"] = (
|
||||
pydantic.Field(
|
||||
default="justified",
|
||||
description=(
|
||||
"Text alignment. Options: 'left', 'justified' (spreads text across full"
|
||||
" width), 'justified-with-no-hyphenation' (justified without word"
|
||||
" breaks). The default value is `justified`."
|
||||
),
|
||||
)
|
||||
)
|
||||
date_and_location_column_alignment: Alignment = pydantic.Field(
|
||||
default="right",
|
||||
description=(
|
||||
"Alignment for dates and locations in entries. Options: 'left', 'center',"
|
||||
" 'right'. The default value is `right`."
|
||||
),
|
||||
)
|
||||
font_family: FontFamily | FontFamilyType = pydantic.Field(
|
||||
default_factory=FontFamily,
|
||||
description=(
|
||||
"The font family. You can provide a single font name as a string (applies"
|
||||
" to all elements), or a dictionary with keys 'body', 'name', 'headline',"
|
||||
" 'connections', and 'section_titles' to customize each element. Any system"
|
||||
" font can be used."
|
||||
),
|
||||
)
|
||||
font_size: FontSize = pydantic.Field(
|
||||
default_factory=FontSize,
|
||||
description="Font sizes for different elements.",
|
||||
)
|
||||
small_caps: SmallCaps = pydantic.Field(
|
||||
default_factory=SmallCaps,
|
||||
description="Small caps styling for different elements.",
|
||||
)
|
||||
bold: Bold = pydantic.Field(
|
||||
default_factory=Bold,
|
||||
description="Bold styling for different elements.",
|
||||
)
|
||||
|
||||
@pydantic.field_validator(
|
||||
"font_family", mode="plain", json_schema_input_type=FontFamily | FontFamilyType
|
||||
)
|
||||
@classmethod
|
||||
def validate_font_family(
|
||||
cls, font_family: FontFamily | FontFamilyType
|
||||
) -> FontFamily:
|
||||
"""Convert string font to FontFamily object with uniform styling.
|
||||
|
||||
Why:
|
||||
Users can provide simple string "Latin Modern Roman" for all text,
|
||||
or specify per-element fonts via FontFamily dict. Validator accepts
|
||||
both, expanding strings to full FontFamily objects.
|
||||
|
||||
Args:
|
||||
font_family: String font name or FontFamily object.
|
||||
|
||||
Returns:
|
||||
FontFamily object with all fields populated.
|
||||
"""
|
||||
if isinstance(font_family, str):
|
||||
return FontFamily(
|
||||
body=font_family,
|
||||
name=font_family,
|
||||
headline=font_family,
|
||||
connections=font_family,
|
||||
section_titles=font_family,
|
||||
)
|
||||
|
||||
return FontFamily.model_validate(font_family)
|
||||
|
||||
|
||||
# Links
|
||||
|
||||
|
||||
class Links(BaseModelWithoutExtraKeys):
|
||||
underline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Underline hyperlinks. The default value is `false`.",
|
||||
)
|
||||
show_external_link_icon: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Show an external link icon next to URLs. The default value is `false`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Header
|
||||
|
||||
type PhoneNumberFormatType = Literal["national", "international", "E164"]
|
||||
|
||||
|
||||
class Connections(BaseModelWithoutExtraKeys):
|
||||
phone_number_format: PhoneNumberFormatType = pydantic.Field(
|
||||
default="national",
|
||||
description="Phone number format. The default value is `national`.",
|
||||
)
|
||||
hyperlink: bool = pydantic.Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Make contact information clickable in the PDF. The default value is"
|
||||
" `true`."
|
||||
),
|
||||
)
|
||||
show_icons: bool = pydantic.Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Show icons next to contact information. The default value is `true`."
|
||||
),
|
||||
)
|
||||
display_urls_instead_of_usernames: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Display full URLs instead of labels. The default value is `false`."
|
||||
),
|
||||
)
|
||||
separator: str = pydantic.Field(
|
||||
default="",
|
||||
description=(
|
||||
"Character(s) to separate contact items (e.g., '|' or '•'). Leave empty for"
|
||||
" no separator. The default value is `''`."
|
||||
),
|
||||
)
|
||||
space_between_connections: TypstDimension = pydantic.Field(
|
||||
default="0.5cm",
|
||||
description=(
|
||||
"Horizontal space between contact items. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.5cm`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Header(BaseModelWithoutExtraKeys):
|
||||
alignment: Alignment = pydantic.Field(
|
||||
default="center",
|
||||
description=(
|
||||
"Header alignment. Options: 'left', 'center', 'right'. The default value is"
|
||||
" `center`."
|
||||
),
|
||||
)
|
||||
photo_width: TypstDimension = pydantic.Field(
|
||||
default="3.5cm",
|
||||
description="Photo width. "
|
||||
+ length_common_description
|
||||
+ " The default value is `3.5cm`.",
|
||||
)
|
||||
photo_position: Literal["left", "right"] = pydantic.Field(
|
||||
default="left",
|
||||
description="Photo position (left or right). The default value is `left`.",
|
||||
)
|
||||
photo_space_left: TypstDimension = pydantic.Field(
|
||||
default="0.4cm",
|
||||
description=(
|
||||
"Space to the left of the photo. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.4cm`."
|
||||
),
|
||||
)
|
||||
photo_space_right: TypstDimension = pydantic.Field(
|
||||
default="0.4cm",
|
||||
description=(
|
||||
"Space to the right of the photo. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.4cm`."
|
||||
),
|
||||
)
|
||||
space_below_name: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below your name. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
space_below_headline: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below the headline. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
space_below_connections: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below contact information. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
connections: Connections = pydantic.Field(
|
||||
default_factory=Connections,
|
||||
description="Contact information settings.",
|
||||
)
|
||||
|
||||
|
||||
# Section Titles
|
||||
|
||||
type SectionTitleType = Literal[
|
||||
"with_partial_line",
|
||||
"with_full_line",
|
||||
"without_line",
|
||||
"moderncv",
|
||||
"centered_without_line",
|
||||
"centered_with_partial_line",
|
||||
"centered_with_centered_partial_line",
|
||||
"centered_with_full_line",
|
||||
]
|
||||
|
||||
|
||||
class SectionTitles(BaseModelWithoutExtraKeys):
|
||||
type: SectionTitleType = pydantic.Field(
|
||||
default="with_partial_line",
|
||||
@@ -161,6 +466,9 @@ class SectionTitles(BaseModelWithoutExtraKeys):
|
||||
)
|
||||
|
||||
|
||||
# Sections
|
||||
|
||||
|
||||
class Sections(BaseModelWithoutExtraKeys):
|
||||
allow_page_break: bool = pydantic.Field(
|
||||
default=True,
|
||||
@@ -200,6 +508,11 @@ class Sections(BaseModelWithoutExtraKeys):
|
||||
return [section_title.lower().replace(" ", "_") for section_title in value]
|
||||
|
||||
|
||||
# Entries
|
||||
|
||||
type Bullet = Literal["●", "•", "◦", "-", "◆", "★", "■", "—", "○"]
|
||||
|
||||
|
||||
class Summary(BaseModelWithoutExtraKeys):
|
||||
space_above: TypstDimension = pydantic.Field(
|
||||
default="0cm",
|
||||
@@ -319,6 +632,237 @@ class Entries(BaseModelWithoutExtraKeys):
|
||||
)
|
||||
|
||||
|
||||
# Templates
|
||||
|
||||
|
||||
class OneLineEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**LABEL:** DETAILS",
|
||||
description=(
|
||||
"Template for one-line entries. Available placeholders:\n- `LABEL`: The"
|
||||
' label text (e.g., "Languages", "Citizenship")\n- `DETAILS`: The details'
|
||||
' text (e.g., "English (native), Spanish (fluent)")\n\nYou can also add'
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `**LABEL:** DETAILS`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EducationEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**INSTITUTION**, AREA\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for education entry main column. Available placeholders:\n-"
|
||||
" `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
|
||||
" `DEGREE`: Degree type (e.g., BS, PhD)\n- `DEGREE_WITH_AREA`: Locale-aware"
|
||||
" phrase combining degree and area (e.g., 'BS in Computer Science')\n-"
|
||||
" `SUMMARY`: Summary text\n-"
|
||||
" `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
|
||||
" Formatted date or date range\n\nYou can also add arbitrary keys to"
|
||||
" entries and use them as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**INSTITUTION**, AREA\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
degree_column: str | None = pydantic.Field(
|
||||
default="**DEGREE**",
|
||||
description=(
|
||||
"Optional degree column template. If provided, displays degree in separate"
|
||||
" column. If `null`, no degree column is shown. Available placeholders:\n-"
|
||||
" `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
|
||||
" `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`: Summary text\n-"
|
||||
" `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
|
||||
" Formatted date or date range\n\nYou can also add arbitrary keys to"
|
||||
" entries and use them as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**DEGREE**`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for education entry date/location column. Available"
|
||||
" placeholders:\n- `INSTITUTION`: Institution name\n- `AREA`: Field of"
|
||||
" study/major\n- `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`:"
|
||||
" Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
|
||||
" text\n- `DATE`: Formatted date or date range\n\nYou can also add"
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NormalEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**NAME**\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for normal entry main column. Available placeholders:\n- `NAME`:"
|
||||
" Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet"
|
||||
" points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or"
|
||||
" date range\n\nYou can also add arbitrary keys to entries and use them as"
|
||||
" UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**NAME**\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for normal entry date/location column. Available placeholders:\n-"
|
||||
" `NAME`: Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`:"
|
||||
" Bullet points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date"
|
||||
" or date range\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ExperienceEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**COMPANY**, POSITION\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for experience entry main column. Available placeholders:\n-"
|
||||
" `COMPANY`: Company name\n- `POSITION`: Job title/position\n- `SUMMARY`:"
|
||||
" Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
|
||||
" text\n- `DATE`: Formatted date or date range\n\nYou can also add"
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `**COMPANY**, POSITION\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for experience entry date/location column. Available"
|
||||
" placeholders:\n- `COMPANY`: Company name\n- `POSITION`: Job"
|
||||
" title/position\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet points"
|
||||
" list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or date"
|
||||
" range\n\nYou can also add arbitrary keys to entries and use them as"
|
||||
" UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PublicationEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**TITLE**\nSUMMARY\nAUTHORS\nURL (JOURNAL)",
|
||||
description=(
|
||||
"Template for publication entry main column. Available placeholders:\n-"
|
||||
" `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
|
||||
" comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
|
||||
" Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
|
||||
" provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
|
||||
" Formatted date\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**TITLE**\\nSUMMARY\\nAUTHORS\\nURL (JOURNAL)`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="DATE",
|
||||
description=(
|
||||
"Template for publication entry date column. Available placeholders:\n-"
|
||||
" `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
|
||||
" comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
|
||||
" Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
|
||||
" provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
|
||||
" Formatted date\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is `DATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Templates(BaseModelWithoutExtraKeys):
|
||||
footer: str = pydantic.Field(
|
||||
default="*NAME -- PAGE_NUMBER/TOTAL_PAGES*",
|
||||
description=(
|
||||
"Template for the footer. Available placeholders:\n"
|
||||
"- `NAME`: The CV owner's name from `cv.name`\n"
|
||||
"- `PAGE_NUMBER`: Current page number\n"
|
||||
"- `TOTAL_PAGES`: Total number of pages\n"
|
||||
"- `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n"
|
||||
"- `MONTH_NAME`: Full month name (e.g., January)\n"
|
||||
"- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
|
||||
"- `MONTH`: Month number (e.g., 1)\n"
|
||||
"- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
|
||||
"- `DAY`: Day of the month (e.g., 5)\n"
|
||||
"- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
|
||||
"- `YEAR`: Full year (e.g., 2025)\n"
|
||||
"- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `*NAME -- PAGE_NUMBER/TOTAL_PAGES*`."
|
||||
),
|
||||
)
|
||||
top_note: str = pydantic.Field(
|
||||
default="*LAST_UPDATED CURRENT_DATE*",
|
||||
description=(
|
||||
"Template for the top note. Available placeholders:\n- `LAST_UPDATED`:"
|
||||
' Localized "last updated" text from `locale.last_updated`\n-'
|
||||
" `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n-"
|
||||
" `NAME`: The CV owner's name from `cv.name`\n- `MONTH_NAME`: Full month"
|
||||
" name (e.g., January)\n- `MONTH_ABBREVIATION`: Abbreviated month name"
|
||||
" (e.g., Jan)\n- `MONTH`: Month number (e.g., 1)\n- `MONTH_IN_TWO_DIGITS`:"
|
||||
" Zero-padded month (e.g., 01)\n- `DAY`: Day of the month (e.g., 5)\n-"
|
||||
" `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n- `YEAR`: Full year"
|
||||
" (e.g., 2025)\n- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `*LAST_UPDATED CURRENT_DATE*`."
|
||||
),
|
||||
)
|
||||
single_date: str = pydantic.Field(
|
||||
default="MONTH_ABBREVIATION YEAR",
|
||||
description=(
|
||||
"Template for single dates. Available placeholders:\n"
|
||||
"- `MONTH_NAME`: Full month name (e.g., January)\n"
|
||||
"- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
|
||||
"- `MONTH`: Month number (e.g., 1)\n"
|
||||
"- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
|
||||
"- `DAY`: Day of the month (e.g., 5)\n"
|
||||
"- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
|
||||
"- `YEAR`: Full year (e.g., 2025)\n"
|
||||
"- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `MONTH_ABBREVIATION YEAR`."
|
||||
),
|
||||
)
|
||||
date_range: str = pydantic.Field(
|
||||
default="START_DATE – END_DATE",
|
||||
description=(
|
||||
"Template for date ranges. Available placeholders:\n- `START_DATE`:"
|
||||
" Formatted start date based on `design.templates.single_date`\n-"
|
||||
" `END_DATE`: Formatted end date based on `design.templates.single_date`"
|
||||
' (or "present"/"ongoing" for current positions)\n\nThe default value is'
|
||||
" `START_DATE – END_DATE`."
|
||||
),
|
||||
)
|
||||
time_span: str = pydantic.Field(
|
||||
default="HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS",
|
||||
description=(
|
||||
"Template for time spans (duration calculations). Available"
|
||||
" placeholders:\n- `HOW_MANY_YEARS`: Number of years (e.g., 2)\n- `YEARS`:"
|
||||
' Localized word for "years" from `locale.years` (or singular "year")\n-'
|
||||
" `HOW_MANY_MONTHS`: Number of months (e.g., 3)\n- `MONTHS`: Localized word"
|
||||
' for "months" from `locale.months` (or singular "month")\n\nThe default'
|
||||
" value is `HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS`."
|
||||
),
|
||||
)
|
||||
one_line_entry: OneLineEntryTemplate = pydantic.Field(
|
||||
default_factory=OneLineEntryTemplate,
|
||||
description="Template for one-line entries.",
|
||||
)
|
||||
education_entry: EducationEntryTemplate = pydantic.Field(
|
||||
default_factory=EducationEntryTemplate,
|
||||
description="Template for education entries.",
|
||||
)
|
||||
normal_entry: NormalEntryTemplate = pydantic.Field(
|
||||
default_factory=NormalEntryTemplate,
|
||||
description="Template for normal entries.",
|
||||
)
|
||||
experience_entry: ExperienceEntryTemplate = pydantic.Field(
|
||||
default_factory=ExperienceEntryTemplate,
|
||||
description="Template for experience entries.",
|
||||
)
|
||||
publication_entry: PublicationEntryTemplate = pydantic.Field(
|
||||
default_factory=PublicationEntryTemplate,
|
||||
description="Template for publication entries.",
|
||||
)
|
||||
|
||||
|
||||
# ClassicTheme
|
||||
|
||||
|
||||
class ClassicTheme(BaseModelWithoutExtraKeys):
|
||||
theme: Literal["classic"] = "classic"
|
||||
page: Page = pydantic.Field(default_factory=Page)
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
from typing import Literal
|
||||
|
||||
import pydantic
|
||||
|
||||
from rendercv.schema.models.base import BaseModelWithoutExtraKeys
|
||||
from rendercv.schema.models.design.typography import Alignment
|
||||
from rendercv.schema.models.design.typst_dimension import TypstDimension
|
||||
|
||||
type PhoneNumberFormatType = Literal["national", "international", "E164"]
|
||||
|
||||
length_common_description = (
|
||||
"It can be specified with units (cm, in, pt, mm, em). For example, `0.1cm`."
|
||||
)
|
||||
|
||||
|
||||
class Links(BaseModelWithoutExtraKeys):
|
||||
underline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Underline hyperlinks. The default value is `false`.",
|
||||
)
|
||||
show_external_link_icon: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Show an external link icon next to URLs. The default value is `false`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Connections(BaseModelWithoutExtraKeys):
|
||||
phone_number_format: PhoneNumberFormatType = pydantic.Field(
|
||||
default="national",
|
||||
description="Phone number format. The default value is `national`.",
|
||||
)
|
||||
hyperlink: bool = pydantic.Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Make contact information clickable in the PDF. The default value is"
|
||||
" `true`."
|
||||
),
|
||||
)
|
||||
show_icons: bool = pydantic.Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Show icons next to contact information. The default value is `true`."
|
||||
),
|
||||
)
|
||||
display_urls_instead_of_usernames: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Display full URLs instead of labels. The default value is `false`."
|
||||
),
|
||||
)
|
||||
separator: str = pydantic.Field(
|
||||
default="",
|
||||
description=(
|
||||
"Character(s) to separate contact items (e.g., '|' or '•'). Leave empty for"
|
||||
" no separator. The default value is `''`."
|
||||
),
|
||||
)
|
||||
space_between_connections: TypstDimension = pydantic.Field(
|
||||
default="0.5cm",
|
||||
description=(
|
||||
"Horizontal space between contact items. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.5cm`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Header(BaseModelWithoutExtraKeys):
|
||||
alignment: Alignment = pydantic.Field(
|
||||
default="center",
|
||||
description=(
|
||||
"Header alignment. Options: 'left', 'center', 'right'. The default value is"
|
||||
" `center`."
|
||||
),
|
||||
)
|
||||
photo_width: TypstDimension = pydantic.Field(
|
||||
default="3.5cm",
|
||||
description="Photo width. "
|
||||
+ length_common_description
|
||||
+ " The default value is `3.5cm`.",
|
||||
)
|
||||
photo_position: Literal["left", "right"] = pydantic.Field(
|
||||
default="left",
|
||||
description="Photo position (left or right). The default value is `left`.",
|
||||
)
|
||||
photo_space_left: TypstDimension = pydantic.Field(
|
||||
default="0.4cm",
|
||||
description=(
|
||||
"Space to the left of the photo. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.4cm`."
|
||||
),
|
||||
)
|
||||
photo_space_right: TypstDimension = pydantic.Field(
|
||||
default="0.4cm",
|
||||
description=(
|
||||
"Space to the right of the photo. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.4cm`."
|
||||
),
|
||||
)
|
||||
space_below_name: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below your name. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
space_below_headline: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below the headline. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
space_below_connections: TypstDimension = pydantic.Field(
|
||||
default="0.7cm",
|
||||
description="Space below contact information. "
|
||||
+ length_common_description
|
||||
+ " The default value is `0.7cm`.",
|
||||
)
|
||||
connections: Connections = pydantic.Field(
|
||||
default_factory=Connections,
|
||||
description="Contact information settings.",
|
||||
)
|
||||
@@ -1,228 +0,0 @@
|
||||
import pydantic
|
||||
|
||||
from rendercv.schema.models.base import BaseModelWithoutExtraKeys
|
||||
|
||||
|
||||
class OneLineEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**LABEL:** DETAILS",
|
||||
description=(
|
||||
"Template for one-line entries. Available placeholders:\n- `LABEL`: The"
|
||||
' label text (e.g., "Languages", "Citizenship")\n- `DETAILS`: The details'
|
||||
' text (e.g., "English (native), Spanish (fluent)")\n\nYou can also add'
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `**LABEL:** DETAILS`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EducationEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**INSTITUTION**, AREA\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for education entry main column. Available placeholders:\n-"
|
||||
" `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
|
||||
" `DEGREE`: Degree type (e.g., BS, PhD)\n- `DEGREE_WITH_AREA`: Locale-aware"
|
||||
" phrase combining degree and area (e.g., 'BS in Computer Science')\n-"
|
||||
" `SUMMARY`: Summary text\n-"
|
||||
" `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
|
||||
" Formatted date or date range\n\nYou can also add arbitrary keys to"
|
||||
" entries and use them as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**INSTITUTION**, AREA\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
degree_column: str | None = pydantic.Field(
|
||||
default="**DEGREE**",
|
||||
description=(
|
||||
"Optional degree column template. If provided, displays degree in separate"
|
||||
" column. If `null`, no degree column is shown. Available placeholders:\n-"
|
||||
" `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
|
||||
" `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`: Summary text\n-"
|
||||
" `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
|
||||
" Formatted date or date range\n\nYou can also add arbitrary keys to"
|
||||
" entries and use them as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**DEGREE**`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for education entry date/location column. Available"
|
||||
" placeholders:\n- `INSTITUTION`: Institution name\n- `AREA`: Field of"
|
||||
" study/major\n- `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`:"
|
||||
" Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
|
||||
" text\n- `DATE`: Formatted date or date range\n\nYou can also add"
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NormalEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**NAME**\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for normal entry main column. Available placeholders:\n- `NAME`:"
|
||||
" Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet"
|
||||
" points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or"
|
||||
" date range\n\nYou can also add arbitrary keys to entries and use them as"
|
||||
" UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**NAME**\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for normal entry date/location column. Available placeholders:\n-"
|
||||
" `NAME`: Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`:"
|
||||
" Bullet points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date"
|
||||
" or date range\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ExperienceEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**COMPANY**, POSITION\nSUMMARY\nHIGHLIGHTS",
|
||||
description=(
|
||||
"Template for experience entry main column. Available placeholders:\n-"
|
||||
" `COMPANY`: Company name\n- `POSITION`: Job title/position\n- `SUMMARY`:"
|
||||
" Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
|
||||
" text\n- `DATE`: Formatted date or date range\n\nYou can also add"
|
||||
" arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
|
||||
" default value is `**COMPANY**, POSITION\\nSUMMARY\\nHIGHLIGHTS`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="LOCATION\nDATE",
|
||||
description=(
|
||||
"Template for experience entry date/location column. Available"
|
||||
" placeholders:\n- `COMPANY`: Company name\n- `POSITION`: Job"
|
||||
" title/position\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet points"
|
||||
" list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or date"
|
||||
" range\n\nYou can also add arbitrary keys to entries and use them as"
|
||||
" UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PublicationEntryTemplate(BaseModelWithoutExtraKeys):
|
||||
main_column: str = pydantic.Field(
|
||||
default="**TITLE**\nSUMMARY\nAUTHORS\nURL (JOURNAL)",
|
||||
description=(
|
||||
"Template for publication entry main column. Available placeholders:\n-"
|
||||
" `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
|
||||
" comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
|
||||
" Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
|
||||
" provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
|
||||
" Formatted date\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is"
|
||||
" `**TITLE**\\nSUMMARY\\nAUTHORS\\nURL (JOURNAL)`."
|
||||
),
|
||||
)
|
||||
date_and_location_column: str = pydantic.Field(
|
||||
default="DATE",
|
||||
description=(
|
||||
"Template for publication entry date column. Available placeholders:\n-"
|
||||
" `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
|
||||
" comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
|
||||
" Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
|
||||
" provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
|
||||
" Formatted date\n\nYou can also add arbitrary keys to entries and use them"
|
||||
" as UPPERCASE placeholders.\n\nThe default value is `DATE`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Templates(BaseModelWithoutExtraKeys):
|
||||
footer: str = pydantic.Field(
|
||||
default="*NAME -- PAGE_NUMBER/TOTAL_PAGES*",
|
||||
description=(
|
||||
"Template for the footer. Available placeholders:\n"
|
||||
"- `NAME`: The CV owner's name from `cv.name`\n"
|
||||
"- `PAGE_NUMBER`: Current page number\n"
|
||||
"- `TOTAL_PAGES`: Total number of pages\n"
|
||||
"- `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n"
|
||||
"- `MONTH_NAME`: Full month name (e.g., January)\n"
|
||||
"- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
|
||||
"- `MONTH`: Month number (e.g., 1)\n"
|
||||
"- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
|
||||
"- `DAY`: Day of the month (e.g., 5)\n"
|
||||
"- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
|
||||
"- `YEAR`: Full year (e.g., 2025)\n"
|
||||
"- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `*NAME -- PAGE_NUMBER/TOTAL_PAGES*`."
|
||||
),
|
||||
)
|
||||
top_note: str = pydantic.Field(
|
||||
default="*LAST_UPDATED CURRENT_DATE*",
|
||||
description=(
|
||||
"Template for the top note. Available placeholders:\n- `LAST_UPDATED`:"
|
||||
' Localized "last updated" text from `locale.last_updated`\n-'
|
||||
" `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n-"
|
||||
" `NAME`: The CV owner's name from `cv.name`\n- `MONTH_NAME`: Full month"
|
||||
" name (e.g., January)\n- `MONTH_ABBREVIATION`: Abbreviated month name"
|
||||
" (e.g., Jan)\n- `MONTH`: Month number (e.g., 1)\n- `MONTH_IN_TWO_DIGITS`:"
|
||||
" Zero-padded month (e.g., 01)\n- `DAY`: Day of the month (e.g., 5)\n-"
|
||||
" `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n- `YEAR`: Full year"
|
||||
" (e.g., 2025)\n- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `*LAST_UPDATED CURRENT_DATE*`."
|
||||
),
|
||||
)
|
||||
single_date: str = pydantic.Field(
|
||||
default="MONTH_ABBREVIATION YEAR",
|
||||
description=(
|
||||
"Template for single dates. Available placeholders:\n"
|
||||
"- `MONTH_NAME`: Full month name (e.g., January)\n"
|
||||
"- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
|
||||
"- `MONTH`: Month number (e.g., 1)\n"
|
||||
"- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
|
||||
"- `DAY`: Day of the month (e.g., 5)\n"
|
||||
"- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
|
||||
"- `YEAR`: Full year (e.g., 2025)\n"
|
||||
"- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
|
||||
"The default value is `MONTH_ABBREVIATION YEAR`."
|
||||
),
|
||||
)
|
||||
date_range: str = pydantic.Field(
|
||||
default="START_DATE – END_DATE",
|
||||
description=(
|
||||
"Template for date ranges. Available placeholders:\n- `START_DATE`:"
|
||||
" Formatted start date based on `design.templates.single_date`\n-"
|
||||
" `END_DATE`: Formatted end date based on `design.templates.single_date`"
|
||||
' (or "present"/"ongoing" for current positions)\n\nThe default value is'
|
||||
" `START_DATE – END_DATE`."
|
||||
),
|
||||
)
|
||||
time_span: str = pydantic.Field(
|
||||
default="HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS",
|
||||
description=(
|
||||
"Template for time spans (duration calculations). Available"
|
||||
" placeholders:\n- `HOW_MANY_YEARS`: Number of years (e.g., 2)\n- `YEARS`:"
|
||||
' Localized word for "years" from `locale.years` (or singular "year")\n-'
|
||||
" `HOW_MANY_MONTHS`: Number of months (e.g., 3)\n- `MONTHS`: Localized word"
|
||||
' for "months" from `locale.months` (or singular "month")\n\nThe default'
|
||||
" value is `HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS`."
|
||||
),
|
||||
)
|
||||
one_line_entry: OneLineEntryTemplate = pydantic.Field(
|
||||
default_factory=OneLineEntryTemplate,
|
||||
description="Template for one-line entries.",
|
||||
)
|
||||
education_entry: EducationEntryTemplate = pydantic.Field(
|
||||
default_factory=EducationEntryTemplate,
|
||||
description="Template for education entries.",
|
||||
)
|
||||
normal_entry: NormalEntryTemplate = pydantic.Field(
|
||||
default_factory=NormalEntryTemplate,
|
||||
description="Template for normal entries.",
|
||||
)
|
||||
experience_entry: ExperienceEntryTemplate = pydantic.Field(
|
||||
default_factory=ExperienceEntryTemplate,
|
||||
description="Template for experience entries.",
|
||||
)
|
||||
publication_entry: PublicationEntryTemplate = pydantic.Field(
|
||||
default_factory=PublicationEntryTemplate,
|
||||
description="Template for publication entries.",
|
||||
)
|
||||
@@ -1,192 +0,0 @@
|
||||
from typing import Literal
|
||||
|
||||
import pydantic
|
||||
|
||||
from rendercv.schema.models.base import BaseModelWithoutExtraKeys
|
||||
from rendercv.schema.models.design.font_family import FontFamily as FontFamilyType
|
||||
from rendercv.schema.models.design.typst_dimension import TypstDimension
|
||||
|
||||
type BodyAlignment = Literal["left", "justified", "justified-with-no-hyphenation"]
|
||||
type Alignment = Literal["left", "center", "right"]
|
||||
|
||||
|
||||
class FontFamily(BaseModelWithoutExtraKeys):
|
||||
body: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for body text. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
name: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for the name. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
headline: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for the headline. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
connections: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for connections. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
section_titles: FontFamilyType = pydantic.Field(
|
||||
default="Source Sans 3",
|
||||
description=(
|
||||
"The font family for section titles. The default value is `Source Sans 3`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FontSize(BaseModelWithoutExtraKeys):
|
||||
body: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for body text. The default value is `10pt`.",
|
||||
)
|
||||
name: TypstDimension = pydantic.Field(
|
||||
default="30pt",
|
||||
description="The font size for the name. The default value is `30pt`.",
|
||||
)
|
||||
headline: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for the headline. The default value is `10pt`.",
|
||||
)
|
||||
connections: TypstDimension = pydantic.Field(
|
||||
default="10pt",
|
||||
description="The font size for connections. The default value is `10pt`.",
|
||||
)
|
||||
section_titles: TypstDimension = pydantic.Field(
|
||||
default="1.4em",
|
||||
description="The font size for section titles. The default value is `1.4em`.",
|
||||
)
|
||||
|
||||
|
||||
class SmallCaps(BaseModelWithoutExtraKeys):
|
||||
name: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for the name. The default value is `false`."
|
||||
),
|
||||
)
|
||||
headline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for the headline. The default value is `false`."
|
||||
),
|
||||
)
|
||||
connections: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for connections. The default value is `false`."
|
||||
),
|
||||
)
|
||||
section_titles: bool = pydantic.Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Whether to use small caps for section titles. The default value is"
|
||||
" `false`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Bold(BaseModelWithoutExtraKeys):
|
||||
name: bool = pydantic.Field(
|
||||
default=True,
|
||||
description="Whether to make the name bold. The default value is `true`.",
|
||||
)
|
||||
headline: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Whether to make the headline bold. The default value is `false`.",
|
||||
)
|
||||
connections: bool = pydantic.Field(
|
||||
default=False,
|
||||
description="Whether to make connections bold. The default value is `false`.",
|
||||
)
|
||||
section_titles: bool = pydantic.Field(
|
||||
default=True,
|
||||
description="Whether to make section titles bold. The default value is `true`.",
|
||||
)
|
||||
|
||||
|
||||
class Typography(BaseModelWithoutExtraKeys):
|
||||
line_spacing: TypstDimension = pydantic.Field(
|
||||
default="0.6em",
|
||||
description=(
|
||||
"Space between lines of text. Larger values create more vertical space. The"
|
||||
" default value is `0.6em`."
|
||||
),
|
||||
)
|
||||
alignment: Literal["left", "justified", "justified-with-no-hyphenation"] = (
|
||||
pydantic.Field(
|
||||
default="justified",
|
||||
description=(
|
||||
"Text alignment. Options: 'left', 'justified' (spreads text across full"
|
||||
" width), 'justified-with-no-hyphenation' (justified without word"
|
||||
" breaks). The default value is `justified`."
|
||||
),
|
||||
)
|
||||
)
|
||||
date_and_location_column_alignment: Alignment = pydantic.Field(
|
||||
default="right",
|
||||
description=(
|
||||
"Alignment for dates and locations in entries. Options: 'left', 'center',"
|
||||
" 'right'. The default value is `right`."
|
||||
),
|
||||
)
|
||||
font_family: FontFamily | FontFamilyType = pydantic.Field(
|
||||
default_factory=FontFamily,
|
||||
description=(
|
||||
"The font family. You can provide a single font name as a string (applies"
|
||||
" to all elements), or a dictionary with keys 'body', 'name', 'headline',"
|
||||
" 'connections', and 'section_titles' to customize each element. Any system"
|
||||
" font can be used."
|
||||
),
|
||||
)
|
||||
font_size: FontSize = pydantic.Field(
|
||||
default_factory=FontSize,
|
||||
description="Font sizes for different elements.",
|
||||
)
|
||||
small_caps: SmallCaps = pydantic.Field(
|
||||
default_factory=SmallCaps,
|
||||
description="Small caps styling for different elements.",
|
||||
)
|
||||
bold: Bold = pydantic.Field(
|
||||
default_factory=Bold,
|
||||
description="Bold styling for different elements.",
|
||||
)
|
||||
|
||||
@pydantic.field_validator(
|
||||
"font_family", mode="plain", json_schema_input_type=FontFamily | FontFamilyType
|
||||
)
|
||||
@classmethod
|
||||
def validate_font_family(
|
||||
cls, font_family: FontFamily | FontFamilyType
|
||||
) -> FontFamily:
|
||||
"""Convert string font to FontFamily object with uniform styling.
|
||||
|
||||
Why:
|
||||
Users can provide simple string "Latin Modern Roman" for all text,
|
||||
or specify per-element fonts via FontFamily dict. Validator accepts
|
||||
both, expanding strings to full FontFamily objects.
|
||||
|
||||
Args:
|
||||
font_family: String font name or FontFamily object.
|
||||
|
||||
Returns:
|
||||
FontFamily object with all fields populated.
|
||||
"""
|
||||
if isinstance(font_family, str):
|
||||
return FontFamily(
|
||||
body=font_family,
|
||||
name=font_family,
|
||||
headline=font_family,
|
||||
connections=font_family,
|
||||
section_titles=font_family,
|
||||
)
|
||||
|
||||
return FontFamily.model_validate(font_family)
|
||||
@@ -31,3 +31,7 @@ def validate_typst_dimension(dimension: str) -> str:
|
||||
|
||||
|
||||
type TypstDimension = Annotated[str, pydantic.AfterValidator(validate_typst_dimension)]
|
||||
|
||||
length_common_description = (
|
||||
"It can be specified with units (cm, in, pt, mm, em). For example, `0.1cm`."
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ class ScannerNoAlias(RoundTripScanner):
|
||||
|
||||
|
||||
yaml = ruamel.yaml.YAML()
|
||||
yaml.Scanner = ScannerNoAlias # ty: ignore[invalid-assignment]
|
||||
yaml.Scanner = ScannerNoAlias
|
||||
|
||||
# Disable ISO date parsing, keep it as a string:
|
||||
yaml.constructor.yaml_constructors["tag:yaml.org,2002:timestamp"] = (
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestCliCommandRender:
|
||||
"dont_generate_png": False,
|
||||
"watch": False,
|
||||
"quiet": False,
|
||||
"_": None,
|
||||
"yaml_field_override": None,
|
||||
"extra_data_model_override_arguments": context,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ from rendercv.renderer.templater.connections import (
|
||||
from rendercv.schema.models.cv.custom_connection import CustomConnection
|
||||
from rendercv.schema.models.cv.cv import Cv
|
||||
from rendercv.schema.models.cv.social_network import SocialNetwork, SocialNetworkName
|
||||
from rendercv.schema.models.design.classic_theme import ClassicTheme
|
||||
from rendercv.schema.models.design.header import Connections, Header
|
||||
from rendercv.schema.models.design.classic_theme import (
|
||||
ClassicTheme,
|
||||
Connections,
|
||||
Header,
|
||||
)
|
||||
from rendercv.schema.models.locale.locale import EnglishLocale
|
||||
from rendercv.schema.models.rendercv_model import RenderCVModel
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import calendar
|
||||
import re
|
||||
from datetime import date as Date
|
||||
|
||||
@@ -16,7 +17,21 @@ from rendercv.schema.models.cv.entries.bases.entry_with_complex_fields import (
|
||||
get_date_object,
|
||||
)
|
||||
from rendercv.schema.models.locale.english_locale import EnglishLocale
|
||||
from tests.strategies import valid_date_strings
|
||||
|
||||
|
||||
@st.composite
|
||||
def valid_date_strings(draw: st.DrawFn) -> str:
|
||||
"""Generate date strings in YYYY-MM-DD, YYYY-MM, or YYYY format."""
|
||||
year = draw(st.integers(min_value=1, max_value=9999))
|
||||
fmt = draw(st.sampled_from(["year", "year_month", "year_month_day"]))
|
||||
if fmt == "year":
|
||||
return f"{year:04d}"
|
||||
month = draw(st.integers(min_value=1, max_value=12))
|
||||
if fmt == "year_month":
|
||||
return f"{year:04d}-{month:02d}"
|
||||
max_day = calendar.monthrange(year, month)[1]
|
||||
day = draw(st.integers(min_value=1, max_value=max_day))
|
||||
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||
|
||||
|
||||
class TestBuildDatePlaceholders:
|
||||
@@ -669,7 +684,7 @@ class TestComputeTimeSpanString:
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
start=valid_date_strings(),
|
||||
start=valid_date_strings(), # ty: ignore[missing-argument]
|
||||
delta_days=st.integers(min_value=0, max_value=36500),
|
||||
)
|
||||
def test_non_negative_duration(self, start: str, delta_days: int) -> None:
|
||||
@@ -718,7 +733,7 @@ class TestComputeTimeSpanString:
|
||||
|
||||
class TestGetDateObject:
|
||||
@settings(deadline=None)
|
||||
@given(date_str=valid_date_strings())
|
||||
@given(date_str=valid_date_strings()) # ty: ignore[missing-argument]
|
||||
def test_valid_strings_produce_date_objects(self, date_str: str) -> None:
|
||||
result = get_date_object(date_str)
|
||||
assert isinstance(result, Date)
|
||||
|
||||
@@ -23,7 +23,7 @@ from rendercv.schema.models.cv.entries.education import EducationEntry
|
||||
from rendercv.schema.models.cv.entries.experience import ExperienceEntry
|
||||
from rendercv.schema.models.cv.entries.normal import NormalEntry
|
||||
from rendercv.schema.models.cv.entries.publication import PublicationEntry
|
||||
from rendercv.schema.models.design.templates import (
|
||||
from rendercv.schema.models.design.classic_theme import (
|
||||
EducationEntryTemplate,
|
||||
NormalEntryTemplate,
|
||||
PublicationEntryTemplate,
|
||||
|
||||
@@ -9,7 +9,53 @@ from rendercv.renderer.templater.string_processor import (
|
||||
make_keywords_bold,
|
||||
substitute_placeholders,
|
||||
)
|
||||
from tests.strategies import keyword_lists, placeholder_dicts, urls
|
||||
|
||||
keyword_lists = st.lists(
|
||||
st.text(
|
||||
alphabet=st.characters(categories=("L", "N", "Zs")),
|
||||
min_size=1,
|
||||
max_size=30,
|
||||
).filter(lambda s: s.strip()),
|
||||
min_size=0,
|
||||
max_size=10,
|
||||
)
|
||||
|
||||
|
||||
@st.composite
|
||||
def placeholder_dicts(draw: st.DrawFn) -> dict[str, str]:
|
||||
"""Generate placeholder dicts with UPPERCASE keys."""
|
||||
keys = draw(
|
||||
st.lists(
|
||||
st.from_regex(r"[A-Z]{1,15}", fullmatch=True),
|
||||
min_size=0,
|
||||
max_size=5,
|
||||
unique=True,
|
||||
)
|
||||
)
|
||||
values = draw(
|
||||
st.lists(
|
||||
st.text(
|
||||
alphabet=st.characters(categories=("L", "N", "Zs")),
|
||||
min_size=0,
|
||||
max_size=20,
|
||||
),
|
||||
min_size=len(keys),
|
||||
max_size=len(keys),
|
||||
)
|
||||
)
|
||||
return dict(zip(keys, values, strict=True))
|
||||
|
||||
|
||||
@st.composite
|
||||
def urls(draw: st.DrawFn) -> str:
|
||||
"""Generate realistic URL strings with http/https protocol."""
|
||||
protocol = draw(st.sampled_from(["https://", "http://"]))
|
||||
domain = draw(st.from_regex(r"[a-z]{2,10}\.[a-z]{2,4}", fullmatch=True))
|
||||
path = draw(st.from_regex(r"[a-z0-9_-]{0,20}", fullmatch=True))
|
||||
trailing_slash = draw(st.sampled_from(["", "/"]))
|
||||
if path:
|
||||
return f"{protocol}{domain}/{path}{trailing_slash}"
|
||||
return f"{protocol}{domain}{trailing_slash}"
|
||||
|
||||
|
||||
class TestMakeKeywordsBold:
|
||||
@@ -98,7 +144,7 @@ class TestSubstitutePlaceholders:
|
||||
assert substitute_placeholders(text, {}) == text
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(placeholders=placeholder_dicts())
|
||||
@given(placeholders=placeholder_dicts()) # ty: ignore[missing-argument]
|
||||
def test_all_keys_absent_from_output(self, placeholders: dict[str, str]) -> None:
|
||||
assume(placeholders)
|
||||
keys = set(placeholders.keys())
|
||||
@@ -125,19 +171,19 @@ class TestCleanUrl:
|
||||
assert clean_url(url) == expected_clean_url
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(url=urls())
|
||||
@given(url=urls()) # ty: ignore[missing-argument]
|
||||
def test_is_idempotent(self, url: str) -> None:
|
||||
assert clean_url(clean_url(url)) == clean_url(url)
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(url=urls())
|
||||
@given(url=urls()) # ty: ignore[missing-argument]
|
||||
def test_removes_protocol(self, url: str) -> None:
|
||||
result = clean_url(url)
|
||||
assert "https://" not in result
|
||||
assert "http://" not in result
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(url=urls())
|
||||
@given(url=urls()) # ty: ignore[missing-argument]
|
||||
def test_removes_trailing_slashes(self, url: str) -> None:
|
||||
result = clean_url(url)
|
||||
if result:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import calendar
|
||||
from datetime import date as Date
|
||||
|
||||
import pydantic
|
||||
@@ -13,7 +14,21 @@ from rendercv.schema.models.cv.entries.bases.entry_with_complex_fields import (
|
||||
from rendercv.schema.models.cv.entries.bases.entry_with_date import (
|
||||
validate_arbitrary_date,
|
||||
)
|
||||
from tests.strategies import valid_date_strings
|
||||
|
||||
|
||||
@st.composite
|
||||
def valid_date_strings(draw: st.DrawFn) -> str:
|
||||
"""Generate date strings in YYYY-MM-DD, YYYY-MM, or YYYY format."""
|
||||
year = draw(st.integers(min_value=1, max_value=9999))
|
||||
fmt = draw(st.sampled_from(["year", "year_month", "year_month_day"]))
|
||||
if fmt == "year":
|
||||
return f"{year:04d}"
|
||||
month = draw(st.integers(min_value=1, max_value=12))
|
||||
if fmt == "year_month":
|
||||
return f"{year:04d}-{month:02d}"
|
||||
max_day = calendar.monthrange(year, month)[1]
|
||||
day = draw(st.integers(min_value=1, max_value=max_day))
|
||||
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||
|
||||
|
||||
class TestGetDateObject:
|
||||
@@ -69,7 +84,7 @@ class TestBaseEntryWithComplexFields:
|
||||
)
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(date=valid_date_strings())
|
||||
@given(date=valid_date_strings()) # ty: ignore[missing-argument]
|
||||
def test_date_only_clears_start_and_end(self, date: str) -> None:
|
||||
entry = BaseEntryWithComplexFields(
|
||||
date=date, start_date="2020-01", end_date="2021-01"
|
||||
@@ -88,7 +103,7 @@ class TestBaseEntryWithComplexFields:
|
||||
assert entry.end_date == "present"
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(end_date=valid_date_strings())
|
||||
@given(end_date=valid_date_strings()) # ty: ignore[missing-argument]
|
||||
def test_end_only_becomes_date(self, end_date: str) -> None:
|
||||
entry = BaseEntryWithComplexFields(end_date=end_date)
|
||||
assert entry.date == end_date
|
||||
@@ -98,7 +113,7 @@ class TestBaseEntryWithComplexFields:
|
||||
|
||||
class TestValidateArbitraryDate:
|
||||
@settings(deadline=None)
|
||||
@given(date_str=valid_date_strings())
|
||||
@given(date_str=valid_date_strings()) # ty: ignore[missing-argument]
|
||||
def test_valid_date_strings_pass_through(self, date_str: str) -> None:
|
||||
result = validate_arbitrary_date(date_str)
|
||||
assert result == date_str
|
||||
@@ -120,9 +135,9 @@ class TestValidateArbitraryDate:
|
||||
assert result == text
|
||||
|
||||
def test_invalid_month_raises(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="month must be in"):
|
||||
validate_arbitrary_date("2020-13-01")
|
||||
|
||||
def test_invalid_day_raises(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="day is out of range"):
|
||||
validate_arbitrary_date("2020-02-30")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pydantic
|
||||
import pytest
|
||||
from hypothesis import assume, given, settings
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
# They are called dynamically in the test with `eval(f"{entry_type}(**entry)")`.
|
||||
|
||||
@@ -147,7 +147,7 @@ class TestSocialNetwork:
|
||||
username=st.from_regex(r"[a-zA-Z0-9_-]{1,20}", fullmatch=True),
|
||||
)
|
||||
def test_valid_network_url_is_valid_http_url(
|
||||
self, network: str, username: str
|
||||
self, network: SocialNetworkName, username: str
|
||||
) -> None:
|
||||
sn = SocialNetwork(network=network, username=username)
|
||||
pydantic.TypeAdapter(pydantic.HttpUrl).validate_strings(sn.url)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from rendercv.schema.models.design.typography import FontFamily, Typography
|
||||
from rendercv.schema.models.design.classic_theme import FontFamily, Typography
|
||||
|
||||
|
||||
class TestTypography:
|
||||
|
||||
@@ -8,7 +8,19 @@ from rendercv.schema.models.design.typst_dimension import (
|
||||
TypstDimension,
|
||||
validate_typst_dimension,
|
||||
)
|
||||
from tests.strategies import typst_dimensions
|
||||
|
||||
|
||||
@st.composite
|
||||
def typst_dimensions(draw: st.DrawFn) -> str:
|
||||
"""Generate valid Typst dimension strings."""
|
||||
sign = draw(st.sampled_from(["", "-"]))
|
||||
integer_part = draw(st.integers(min_value=0, max_value=999))
|
||||
has_decimal = draw(st.booleans())
|
||||
decimal_part = ""
|
||||
if has_decimal:
|
||||
decimal_part = "." + str(draw(st.integers(min_value=0, max_value=99)))
|
||||
unit = draw(st.sampled_from(["cm", "in", "pt", "mm", "em"]))
|
||||
return f"{sign}{integer_part}{decimal_part}{unit}"
|
||||
|
||||
|
||||
class TestTypstDimension:
|
||||
@@ -65,7 +77,7 @@ class TestTypstDimension:
|
||||
assert result == dimension
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(dim=typst_dimensions())
|
||||
@given(dim=typst_dimensions()) # ty: ignore[missing-argument]
|
||||
def test_accepts_random_valid_dimensions(self, dim: str) -> None:
|
||||
assert validate_typst_dimension(dim) == dim
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import datetime
|
||||
|
||||
import pydantic
|
||||
import pytest
|
||||
from hypothesis import given, settings as hypothesis_settings
|
||||
from hypothesis import given
|
||||
from hypothesis import settings as hypothesis_settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from rendercv.schema.models.settings.settings import Settings
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
"""Reusable Hypothesis strategies for RenderCV property-based tests."""
|
||||
|
||||
import calendar
|
||||
|
||||
from hypothesis import strategies as st
|
||||
|
||||
|
||||
@st.composite
|
||||
def valid_date_strings(draw: st.DrawFn) -> str:
|
||||
"""Generate date strings in YYYY-MM-DD, YYYY-MM, or YYYY format."""
|
||||
year = draw(st.integers(min_value=1, max_value=9999))
|
||||
fmt = draw(st.sampled_from(["year", "year_month", "year_month_day"]))
|
||||
if fmt == "year":
|
||||
return f"{year:04d}"
|
||||
month = draw(st.integers(min_value=1, max_value=12))
|
||||
if fmt == "year_month":
|
||||
return f"{year:04d}-{month:02d}"
|
||||
max_day = calendar.monthrange(year, month)[1]
|
||||
day = draw(st.integers(min_value=1, max_value=max_day))
|
||||
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||
|
||||
|
||||
@st.composite
|
||||
def date_inputs(draw: st.DrawFn) -> str | int:
|
||||
"""Generate inputs accepted by get_date_object (excluding 'present')."""
|
||||
return draw(
|
||||
st.one_of(
|
||||
valid_date_strings(),
|
||||
st.integers(min_value=1, max_value=9999),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
keyword_lists = st.lists(
|
||||
st.text(
|
||||
alphabet=st.characters(categories=("L", "N", "Zs")),
|
||||
min_size=1,
|
||||
max_size=30,
|
||||
).filter(lambda s: s.strip()),
|
||||
min_size=0,
|
||||
max_size=10,
|
||||
)
|
||||
|
||||
|
||||
@st.composite
|
||||
def placeholder_dicts(draw: st.DrawFn) -> dict[str, str]:
|
||||
"""Generate placeholder dicts with UPPERCASE keys."""
|
||||
keys = draw(
|
||||
st.lists(
|
||||
st.from_regex(r"[A-Z]{1,15}", fullmatch=True),
|
||||
min_size=0,
|
||||
max_size=5,
|
||||
unique=True,
|
||||
)
|
||||
)
|
||||
values = draw(
|
||||
st.lists(
|
||||
st.text(
|
||||
alphabet=st.characters(categories=("L", "N", "Zs")),
|
||||
min_size=0,
|
||||
max_size=20,
|
||||
),
|
||||
min_size=len(keys),
|
||||
max_size=len(keys),
|
||||
)
|
||||
)
|
||||
return dict(zip(keys, values, strict=True))
|
||||
|
||||
|
||||
@st.composite
|
||||
def urls(draw: st.DrawFn) -> str:
|
||||
"""Generate realistic URL strings with http/https protocol."""
|
||||
protocol = draw(st.sampled_from(["https://", "http://"]))
|
||||
domain = draw(st.from_regex(r"[a-z]{2,10}\.[a-z]{2,4}", fullmatch=True))
|
||||
path = draw(st.from_regex(r"[a-z0-9_-]{0,20}", fullmatch=True))
|
||||
trailing_slash = draw(st.sampled_from(["", "/"]))
|
||||
if path:
|
||||
return f"{protocol}{domain}/{path}{trailing_slash}"
|
||||
return f"{protocol}{domain}{trailing_slash}"
|
||||
|
||||
|
||||
@st.composite
|
||||
def typst_dimensions(draw: st.DrawFn) -> str:
|
||||
"""Generate valid Typst dimension strings."""
|
||||
sign = draw(st.sampled_from(["", "-"]))
|
||||
integer_part = draw(st.integers(min_value=0, max_value=999))
|
||||
has_decimal = draw(st.booleans())
|
||||
decimal_part = ""
|
||||
if has_decimal:
|
||||
decimal_part = "." + str(draw(st.integers(min_value=0, max_value=99)))
|
||||
unit = draw(st.sampled_from(["cm", "in", "pt", "mm", "em"]))
|
||||
return f"{sign}{integer_part}{decimal_part}{unit}"
|
||||
2
uv.lock
generated
2
uv.lock
generated
@@ -1270,7 +1270,7 @@ provides-extras = ["full"]
|
||||
create-executable = [{ name = "pyinstaller", specifier = ">=6.17.0" }]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=26.3.1" },
|
||||
{ name = "hypothesis", specifier = ">=6.100.0" },
|
||||
{ name = "hypothesis", specifier = ">=6.151.9" },
|
||||
{ name = "prek", specifier = ">=0.3.6" },
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||
|
||||
Reference in New Issue
Block a user