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.