from __future__ import annotations
import os
from pathlib import Path
from typing import BinaryIO, ClassVar, Iterator, Optional, Type
from PIL import Image
import xml.etree.ElementTree as ET
from pydantic import BaseModel, Extra, Field, validator
from todays_paper_web.storage.storage import Storage
from todays_paper_web.widgets import registerView, ImageType
from .datasource import DataSource
[docs]
class ViewParams(BaseModel):
"""Base ``Params`` for every widget - unlike ``DataSource``'s equivalent, this carries real
fields: ``x``/``y``/``align``/``valign``, read off *any* child by whichever container it's
placed in (``box``/``row``/``column``, see `todays_paper_web.widgets.core.layout`) to decide
where to place it - not owned by the child, its parent interprets it. A few widgets (e.g.
``line``) also happen to read their own ``x``/``y`` for unrelated, widget-specific reasons;
that's the same attribute doing double duty, not a coincidence. Every widget's ``Params``
inherits this, even one with no attributes of its own to add (e.g. ``<column>``'s).
The default for each field is ``None``, not e.g. ``"left"`` for `align`: a container decides
*whether* and *how* to apply its own default when a value wasn't given, which needs to be able
to tell "not given" apart from any real value, including a falsy one like ``x="0"`` - so this
reports presence faithfully rather than pre-applying any one container's default.
"""
class Config:
extra = Extra.ignore
x: Optional[int] = Field(None, description="Horizontal position, if given.")
y: Optional[int] = Field(None, description="Vertical position, if given.")
align: Optional[str] = Field(
None, description="Horizontal alignment (`left`/`center`/`right`), if given."
)
valign: Optional[str] = Field(
None, description="Vertical alignment (`top`/`middle`/`center`/`bottom`), if given."
)
@validator("x", "y", "align", "valign", pre=True)
def _blankToNone(cls, value): # pylint: disable=no-self-argument
# An empty-string attribute (`x=""`) is absent in every practical
# sense - without this, Pydantic would try (and fail) to parse "" as
# an int rather than treating it the same as `x` being unset.
return value or None
[docs]
class View:
#: Every widget's ``Params`` is a ``ViewParams`` subclass - there is no
#: widget without one, even ``<column>``, which has nothing to add on top
#: of ``ViewParams`` itself. Overridden per subclass; never unset.
Params: ClassVar[Type[ViewParams]] = ViewParams
def __init__(self, params):
self._element = None
self._source = None
self._params = params
self._parsedParams: Optional[ViewParams] = None
self._storage: Storage = None
self._config: str = ""
@property
def params(self) -> ViewParams:
"""The parsed ``Params`` model for this widget."""
if self._parsedParams is None:
self._parsedParams = self.Params.parse_obj(self._params)
return self._parsedParams
@property
def children(self) -> Iterator[View]:
for child in self._element:
view = View.get(child.tag, None, child.attrib, self._storage, self._config)
view.element = child
yield view
@property
def element(self) -> ET.Element:
return self._element
@element.setter
def element(self, element: ET.Element):
self._element = element
@property
def innerText(self):
return "".join(self._element.itertext())
[docs]
def getData(self):
if not self.source:
return {}
return self.source.fetch()
[docs]
def openFile(self, filename) -> BinaryIO:
return self._storage.getFileObject(self._config, filename)
[docs]
def render(self) -> ImageType:
return Image.new("RGB", (0, 0), "white")
@property
def source(self):
return self._source
@source.setter
def source(self, source):
self._source = source
[docs]
@staticmethod
def get(name, source, params, storage, config) -> View:
ViewCls = registerView.get(name, View) # pylint: disable=invalid-name
view = ViewCls(params)
view.source = source
view._storage = storage
view._config = config
return view
[docs]
@staticmethod
def paste(target: ImageType, source: ImageType, position: tuple[int, int]):
if source.mode == "RGBA":
target.paste(source, position, source)
else:
target.paste(source, position)