Skip to content

Commit

Permalink
docs: Document and clear up theme code
Browse files Browse the repository at this point in the history
Introduce some type safety around date format strings
  • Loading branch information
alexpovel committed Feb 5, 2023
1 parent 920a759 commit 5229102
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 33 deletions.
10 changes: 9 additions & 1 deletion ancv/visualization/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,15 @@ def _format_date(self, date: date) -> str:
if is_last_doy and self.dec31_as_year:
format = self.theme.datefmt.year_only

return format_date(date, format=format, locale=self.locale)
# Code-wise, `format_date` could accept `DateTimePattern` as its `format`
# directly (version 2.10.3):
# https://github.com/python-babel/babel/blob/25e436016970443226d0ec19cf74ac8476369b33/babel/dates.py#L683
# However, the type annotation is wrong/more restrictive, and it only accepts
# `str`. The original stringly-typed date pattern hides in the `pattern`
# attribute:
pattern = format.pattern

return format_date(date, format=pattern, locale=self.locale)

@lru_cache(maxsize=1_000)
def _format_date_range(
Expand Down
60 changes: 47 additions & 13 deletions ancv/visualization/themes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from babel.dates import DateTimePattern, parse_pattern
from pydantic import BaseModel
from rich.style import Style


class Emphasis(BaseModel):
"""Emphasis styles for different levels of importance.
Using `rich.Style`, each can be styled arbitrarily.
"""

maximum: Style
strong: Style
medium: Style
Expand All @@ -13,17 +19,35 @@ class Config:


class DateFormat(BaseModel):
full: str
year_only: str
"""Date formats for different levels of detail.
The `full` format is as detailed as possible, while `year_only` should only contain
the year. Formats may otherwise be in whatever format you desire (ISO8601,
localized, months spelled out etc.). For more context, see:
https://babel.pocoo.org/en/latest/dates.html
Using `babel.dates.DateTimePattern` and forcing it here over `str` allows for
considerably better type safety (`str` is the worst offender in terms of typing) and
fast failure: at application startup, when a theme is loaded but `parse_pattern` (or
similar) fails, the program won't launch altogether, instead of failing at runtime.
"""

full: DateTimePattern
year_only: DateTimePattern

class Config:
arbitrary_types_allowed = True # No validator for `DateTimePattern`


class Theme(BaseModel):
emphasis: Emphasis
bullet: str
rulechar: str
sep: str
range_sep: str
datefmt: DateFormat
"""A theme, containing styles and other formatting options."""

emphasis: Emphasis # styles for different levels of importance
bullet: str # bullet character to use in (unordered) lists
rulechar: str # character for *horizontal* rules
sep: str # separator character for joined-together strings (e.g. "•" for "foo•bar")
range_sep: str # separator character for ranges (e.g. "..." for "2010...2020")
datefmt: DateFormat # date formats in different levels of detail


# See here for available colors:
Expand All @@ -41,7 +65,9 @@ class Theme(BaseModel):
sep="•",
range_sep="–",
rulechar="─",
datefmt=DateFormat(full="yyyy-MM", year_only="yyyy"),
datefmt=DateFormat(
full=parse_pattern("yyyy-MM"), year_only=parse_pattern("yyyy")
),
),
"grayscale": Theme(
emphasis=Emphasis(
Expand All @@ -54,7 +80,9 @@ class Theme(BaseModel):
sep="*",
range_sep="–",
rulechar="─",
datefmt=DateFormat(full="MMMM yyyy", year_only="yyyy"),
datefmt=DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
),
"basic": Theme(
emphasis=Emphasis(
Expand All @@ -67,7 +95,9 @@ class Theme(BaseModel):
sep="•",
range_sep="–",
rulechar="─",
datefmt=DateFormat(full="MMMM yyyy", year_only="yyyy"),
datefmt=DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
),
"lollipop": Theme(
emphasis=Emphasis(
Expand All @@ -80,7 +110,9 @@ class Theme(BaseModel):
sep="•",
range_sep="➔",
rulechar="─",
datefmt=DateFormat(full="MMMM yyyy", year_only="yyyy"),
datefmt=DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
),
"hendrix": Theme(
emphasis=Emphasis(
Expand All @@ -93,6 +125,8 @@ class Theme(BaseModel):
sep="•",
range_sep="➔",
rulechar="─",
datefmt=DateFormat(full="MMMM yyyy", year_only="yyyy"),
datefmt=DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
),
}
73 changes: 54 additions & 19 deletions tests/visualization/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from babel.core import Locale
from babel.dates import parse_pattern
from rich.console import NewLine, RenderableType
from rich.style import Style

Expand Down Expand Up @@ -57,7 +58,9 @@ def test_ensure_single_trailing_newline(
(
None,
None,
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"-",
"present",
Expand All @@ -68,7 +71,9 @@ def test_ensure_single_trailing_newline(
(
None,
date(2900, 3, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"-",
"present",
Expand All @@ -79,7 +84,9 @@ def test_ensure_single_trailing_newline(
(
date(163, 12, 1),
None,
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"-",
"present",
Expand All @@ -90,7 +97,9 @@ def test_ensure_single_trailing_newline(
(
date(2021, 1, 1),
None,
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"-",
"today",
Expand All @@ -101,7 +110,9 @@ def test_ensure_single_trailing_newline(
(
date(2021, 1, 1),
date(2021, 2, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"-",
"present",
Expand All @@ -112,7 +123,7 @@ def test_ensure_single_trailing_newline(
(
date(1999, 4, 1),
date(2018, 9, 1),
DateFormat(full="yyyy-MM", year_only="yyyy"),
DateFormat(full=parse_pattern("yyyy-MM"), year_only=parse_pattern("yyyy")),
Locale("en"),
"-",
"present",
Expand All @@ -123,7 +134,7 @@ def test_ensure_single_trailing_newline(
(
date(1999, 4, 1),
date(2018, 9, 1),
DateFormat(full="yyyy-MM", year_only="yyyy"),
DateFormat(full=parse_pattern("yyyy-MM"), year_only=parse_pattern("yyyy")),
Locale("en"),
"***",
"present",
Expand All @@ -134,7 +145,9 @@ def test_ensure_single_trailing_newline(
(
date(1999, 3, 1),
date(2018, 10, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("de"),
"***",
"heute",
Expand All @@ -145,7 +158,9 @@ def test_ensure_single_trailing_newline(
(
date(1999, 3, 1),
date(2018, 10, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("es"),
"***",
"heute",
Expand All @@ -156,7 +171,9 @@ def test_ensure_single_trailing_newline(
(
date(2018, 3, 1),
date(2018, 4, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"***",
"present",
Expand All @@ -167,7 +184,9 @@ def test_ensure_single_trailing_newline(
(
date(2018, 4, 1),
date(2018, 4, 1),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"***",
"present",
Expand All @@ -178,7 +197,9 @@ def test_ensure_single_trailing_newline(
(
None,
date(2000, 12, 31),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -189,7 +210,9 @@ def test_ensure_single_trailing_newline(
(
date(2000, 12, 31),
date(2000, 12, 31),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -200,7 +223,9 @@ def test_ensure_single_trailing_newline(
(
date(2000, 12, 31),
None,
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -211,7 +236,9 @@ def test_ensure_single_trailing_newline(
(
date(2000, 12, 31),
date(2002, 12, 31),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -222,7 +249,9 @@ def test_ensure_single_trailing_newline(
(
date(2000, 12, 30),
date(2002, 12, 30),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -233,7 +262,9 @@ def test_ensure_single_trailing_newline(
(
date(2000, 12, 31),
date(2002, 12, 31),
DateFormat(full="MMMM yyyy", year_only="yyyy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("yyyy")
),
Locale("en"),
"---",
"present",
Expand All @@ -244,7 +275,9 @@ def test_ensure_single_trailing_newline(
(
date(1995, 12, 31),
date(1999, 12, 31),
DateFormat(full="MMMM yyyy", year_only="''yy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("''yy")
),
Locale("en"),
"---",
"present",
Expand All @@ -255,7 +288,9 @@ def test_ensure_single_trailing_newline(
(
date(1995, 12, 31),
date(1999, 12, 31),
DateFormat(full="MMMM yyyy", year_only="'Anno' yy"),
DateFormat(
full=parse_pattern("MMMM yyyy"), year_only=parse_pattern("'Anno' yy")
),
Locale("en"),
"to",
"present",
Expand Down

0 comments on commit 5229102

Please sign in to comment.