"""Shared Pydantic field types, used across both the ``View`` and
``DataSource`` ``Params`` hierarchies (see `todays_paper_web.widgets.view` and
`todays_paper_web.widgets.datasource` for those - a widget and a data source
are different enough that they don't share a base *model*, only these value
types).
"""
from __future__ import annotations
import webcolors
[docs]
class Color:
"""A CSS color name or ``#hex`` code, parsed into a ``webcolors.IntegerRGB``.
Same name-then-hex parsing as the pre-migration, now-removed
``View.decodeColor`` had, but with no separate "default on failure"
behavior: an unrecognized color raises a validation error rather than
silently falling back. Give the field itself the right default via
``Field(default_factory=...)``.
"""
@classmethod
def __get_validators__(cls):
yield cls.validate
[docs]
@classmethod
def validate(cls, value) -> webcolors.IntegerRGB:
if isinstance(value, webcolors.IntegerRGB):
return value
try:
return webcolors.name_to_rgb(value)
except ValueError:
pass
try:
return webcolors.hex_to_rgb(value)
except ValueError:
pass
raise ValueError(f"Unknown color {value!r}: expected a CSS color name or a #hex code")
[docs]
class Number:
"""An int or float, kept as whichever type it already was.
For a `DataSource`'s YAML `params:` (unlike a widget's XML attributes, already native Python
types, not strings), a plain ``float`` field would silently coerce a whole-number int like
``17`` to ``17.0`` - harmless on its own, but a naive ``Union[int, float]`` is actively unsafe
in Pydantic v1: it tries ``int`` first and *truncates* a real float's fractional part (e.g.
``55.64277`` silently becomes ``55``) rather than falling through to ``float``. This preserves
whatever numeric type was given, without either problem.
"""
@classmethod
def __get_validators__(cls):
yield cls.validate
[docs]
@classmethod
def validate(cls, value):
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise TypeError(f"expected a number, got {type(value).__name__}")
return value