Python API reference
This section is for extending the server itself — adding a new widget or data source — rather than for designing a screen with the existing ones. It’s generated from docstrings/signatures in the source, so it documents the Python interface of each class.
A widget’s or data source’s per-attribute reference (x, color, params.url, …) is generated by
declaring a typed Params model (see below); Sphinx pulls its properties table straight from the model
rather than it being hand-maintained. Every widget and data source has one, with no exceptions — including
<column>, which has no attributes of its own but still gets a real (if minimal) Params. There is
exactly one mechanism for typed attribute access — no separate plain-Python-property carve-out for
anything, not even x/y/align/valign.
Two separate base models sit under all of this: View’s ViewParams and DataSource’s
DataSourceParams. They aren’t unified into one shared base, deliberately: a widget and a data source
are different enough — a widget renders into an image and sits in a parent/child tree a container reads
generic positioning attributes off of; a data source just fetches data on demand — that a shared base
between them wouldn’t represent anything real, only that both happen to want the same extra=ignore
config. ViewParams carries actual shared fields (x/y/align/valign); DataSourceParams carries
none. Color/Number (see Shared field types) are different: genuine shared value types, meaningful on a
field of either hierarchy, which is why those two stay in their own small module rather than living next
to either base.
Adding a widget
Subclass View, override render() to return a PIL.Image,
and decorate the class with @registerView("your-tag-name"). Then import the module from
todays_paper_web/widgets/__init__.py — only modules imported there register themselves, via the
@registerView/@registerDataSource decorator running at import time.
Give it typed, self-documenting attributes instead of reading off the raw dict: declare a
ViewParams subclass (not pydantic.BaseModel directly —
every widget needs x/y/align/valign, even one with no other attributes of its own, like
todays_paper_web.widgets.core.layout.ColumnParams) with a pydantic.Field(..., description=...) per
extra attribute, and set it as the class’s Params. self.params.<field> then gives a validated value,
including self.params.x/self.params.y for the handful of widgets (line) that also happen to read
their own position for widget-specific reasons. View’s own default Params is already ViewParams
itself, so even an unrecognized tag name (which falls back to the inert base View) answers .params.x
correctly when some other widget’s container reads it. View has no dict fallback of any kind: an
attribute that isn’t a field on the widget’s own Params raises AttributeError, the same as accessing
any other undeclared Python attribute would. See todays_paper_web/widgets/core/rectangle.py for a
worked example, and pass the model to Sphinx with autopydantic_model plus :inherited-members: BaseModel (see that file’s raw source in Shapes: <line>, <rectangle>) so the inherited position fields show
up in the generated table too, instead of hand-writing a properties table.
If a widget shares real rendering logic with others but isn’t a tag itself — ImageBaseView
(todays_paper_web/widgets/core/image.py), subclassed by ImageView/GoComics/WeatherIconView, all
of which need the same resize/fit logic but load their source bitmap differently — give it a Params
too (ImageParams, sizing/fitting only) and simply don’t decorate it with @registerView; each concrete
subclass then sets its own, more specific Params (e.g. LocalImageParams(ImageParams) adding path)
and only needs to implement fetch_img().
Adding a data source
Subclass DataSource, override fetch() to return
whatever a template should receive, and decorate with @registerDataSource("your-source-name"). Same
import-registration rule apply; declare a
DataSourceParams subclass (not ViewParams — a data
source has no parent/child relationship for something else to read a position off it generically, so
there’s nothing to inherit there) and set it as Params, same as a widget. fetch() always reads
self.params.<field> explicitly. DataSource has no __getattr__ at all, so an undeclared attribute
raises AttributeError for free, via Python’s ordinary attribute lookup — no custom code required,
matching View exactly.
One difference from a widget: a data source’s params: come from YAML, already native Python types
rather than XML strings, which creates its own pitfall for a coordinate-like field that might be a whole
number in one config and a decimal in another (e.g. weather.yr’s altitude): a plain float field
silently turns 17 into 17.0, and Union[int, float] is actively unsafe in this project’s Pydantic v1
(it tries int first and truncates a real float’s fractional part). Use
Number for that case instead — it keeps whichever of
int/float was actually given.