plot-io 0.12.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/settings.py
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
"""Settings: declared, validated, inherited and resolved through one cascade.
|
|
2
|
+
|
|
3
|
+
Every object of the model (figure, plot, axis, series, …) declares its settings in a
|
|
4
|
+
``SETTINGS`` table of :class:`Setting`. Values are checked when they are set.
|
|
5
|
+
:func:`cascade` finds where a value comes from, in this order:
|
|
6
|
+
|
|
7
|
+
1. the value set on the object, then (plots' x and y axes) the value set on the figure's
|
|
8
|
+
axis of that role (``figure.x_axis``), then a rule of values given elsewhere
|
|
9
|
+
(:func:`given_rule`: a series' own color axis settings);
|
|
10
|
+
2. each style of the figure's chain (:class:`Theme`), nearest first: the key of the
|
|
11
|
+
object's class, then of its base classes (a base class's key is skipped once a more
|
|
12
|
+
specific class has a default for the setting), then the key of the setting's group
|
|
13
|
+
(``"Marker.size"``); a class rule (``RULES``: a default the class computes from the
|
|
14
|
+
model) comes after the keys of its class and of more specific ones, before the
|
|
15
|
+
others. For an inherited setting, the first style with its bare name is the last
|
|
16
|
+
one read here (the nearer style wins);
|
|
17
|
+
3. for inherited settings, the parent's value (series → plot → figure);
|
|
18
|
+
4. the default (:data:`plot_io.defaults.DEFAULTS`): the class's, along the classes as
|
|
19
|
+
style keys, then the group's; first when it refers to the parent's value
|
|
20
|
+
(``"calc(1.4 * inherit)"``);
|
|
21
|
+
5. for inherited settings, the bare name: the nearest style's, else ``DEFAULTS``'.
|
|
22
|
+
|
|
23
|
+
:meth:`Settings.get` returns the value as given, :meth:`Settings.resolve` the final value
|
|
24
|
+
(units, ``var()``, ``calc()`` and ``"auto"`` rules resolved; see :mod:`plot_io.units`),
|
|
25
|
+
:meth:`Settings.explain` where it comes from. While a figure is rendered, plain
|
|
26
|
+
resolutions read the render's :mod:`~plot_io.spec`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import contextlib
|
|
30
|
+
import contextvars
|
|
31
|
+
import difflib
|
|
32
|
+
from collections.abc import Callable, Generator, Iterable
|
|
33
|
+
from typing import Any, Self
|
|
34
|
+
|
|
35
|
+
from .defaults import DEFAULTS, default_of, inherited
|
|
36
|
+
from .units import (SettingError, Value, Var, guard, has_fixed, may_refer, needs_resolving, parse, refers_inherit, resolve_raw,
|
|
37
|
+
given)
|
|
38
|
+
from .validators import Validator, anything, as_validator, family, text
|
|
39
|
+
# The validators, imported from here by the modules that declare settings.
|
|
40
|
+
from .validators import ( # noqa: F401
|
|
41
|
+
boolean, custom_vars, dpi, dpi_or_bool, either, font_stretch, font_style, font_variations,
|
|
42
|
+
font_weight, line_height, literal, number, one_of, pair, simplify_value, time_zone)
|
|
43
|
+
from . import hints
|
|
44
|
+
from . import spec as _spec
|
|
45
|
+
|
|
46
|
+
_MISS = object()
|
|
47
|
+
|
|
48
|
+
#: Values in force for the output in progress in this thread or task (``write(simplify=…)``,
|
|
49
|
+
#: ``region=``): ``id(obj)`` → ``(obj, {name: value})``, read as if set on ``obj``.
|
|
50
|
+
_OVERRIDES: contextvars.ContextVar[dict[int, tuple[Any, dict[str, Any]]]] = \
|
|
51
|
+
contextvars.ContextVar("plot_io_overrides", default={})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _own(obj: "Settings", name: str) -> Any:
|
|
55
|
+
"""``obj``'s value of ``name``: an override in force, else its own (``_MISS``: none)."""
|
|
56
|
+
o = _OVERRIDES.get()
|
|
57
|
+
if o:
|
|
58
|
+
hit = o.get(id(obj))
|
|
59
|
+
if hit is not None and name in hit[1]:
|
|
60
|
+
return hit[1][name]
|
|
61
|
+
return obj._values.get(name, _MISS)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@contextlib.contextmanager
|
|
65
|
+
def overriding(obj: "Settings", **settings: Any) -> Generator[None]:
|
|
66
|
+
"""Settings of ``obj`` in force inside the block, in this thread or task only, as if
|
|
67
|
+
set on ``obj``; the object is not changed. ``None`` values are left out."""
|
|
68
|
+
settings = {k: v for k, v in canonical_kwargs(settings, obj.SETTINGS, obj.kind).items()
|
|
69
|
+
if v is not None}
|
|
70
|
+
if not settings:
|
|
71
|
+
yield
|
|
72
|
+
return
|
|
73
|
+
checked = {}
|
|
74
|
+
for name, value in settings.items():
|
|
75
|
+
s = obj.SETTINGS.get(name)
|
|
76
|
+
if s is None:
|
|
77
|
+
raise SettingError(unknown_message(name, obj.SETTINGS, obj.kind), obj, name, value)
|
|
78
|
+
checked[name] = obj._checked(s, name, value)
|
|
79
|
+
current = _OVERRIDES.get()
|
|
80
|
+
mine = dict(current[id(obj)][1]) if id(obj) in current else {}
|
|
81
|
+
mine.update(checked)
|
|
82
|
+
token = _OVERRIDES.set({**current, id(obj): (obj, mine)})
|
|
83
|
+
try:
|
|
84
|
+
yield
|
|
85
|
+
finally:
|
|
86
|
+
_OVERRIDES.reset(token)
|
|
87
|
+
#: Returned by a class rule that does not apply: the cascade goes on.
|
|
88
|
+
DECLINE = object()
|
|
89
|
+
|
|
90
|
+
__all__ = ["Setting", "Settings", "Theme", "BASE_THEME", "ALIASES", "MARKER_ALIASES", "DECLINE",
|
|
91
|
+
"route", "unknown_message", "canonical", "cascade", "overriding"]
|
|
92
|
+
|
|
93
|
+
#: The shorthands: of setting names (on every class that has the full setting; a setting
|
|
94
|
+
#: has at most one) and of marker names. docs/api.md's table of shorthands is made from them.
|
|
95
|
+
ALIASES = {"c": "color", "s": "size", "cmap": "color_map", "smap": "size_map"}
|
|
96
|
+
MARKER_ALIASES = {"o": "circle", "s": "square", "d": "diamond", "^": "triangle_up",
|
|
97
|
+
"v": "triangle_down", "<": "triangle_left", ">": "triangle_right",
|
|
98
|
+
"+": "plus", "x": "cross", "triangle": "triangle_up"}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def canonical(name: str, table: Any) -> str:
|
|
102
|
+
"""``name``, or the setting it is an alias of when ``table`` has that setting."""
|
|
103
|
+
if name not in table:
|
|
104
|
+
target = ALIASES.get(name)
|
|
105
|
+
if target is not None and target in table:
|
|
106
|
+
return target
|
|
107
|
+
return name
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def canonical_kwargs(kwargs: dict[str, Any], table: Any, what: str) -> dict[str, Any]:
|
|
111
|
+
"""``kwargs`` with aliases replaced; an alias given with its setting is an error."""
|
|
112
|
+
out: dict[str, Any] = {}
|
|
113
|
+
given_as: dict[str, str] = {}
|
|
114
|
+
for name, value in kwargs.items():
|
|
115
|
+
key = canonical(name, table)
|
|
116
|
+
if key in out:
|
|
117
|
+
raise ValueError(f"{what}: {given_as[key]!r} and {name!r} are the same setting; "
|
|
118
|
+
f"give one")
|
|
119
|
+
out[key] = value
|
|
120
|
+
given_as[key] = name
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Theme:
|
|
125
|
+
"""A figure's style chain: ``layers``, the styles other than ``"default"`` as
|
|
126
|
+
``(label, keys)``, nearest first; ``DEFAULTS`` comes after them. As a mapping, the
|
|
127
|
+
bare names (inherited settings) and custom variables (``--name``): the nearest
|
|
128
|
+
style's, else ``DEFAULTS``'."""
|
|
129
|
+
|
|
130
|
+
__slots__ = ("layers", "_flat")
|
|
131
|
+
|
|
132
|
+
def __init__(self, layers: Iterable[tuple[str, dict[str, Any]]] = ()) -> None:
|
|
133
|
+
self.layers = tuple(layers)
|
|
134
|
+
flat = inherited()
|
|
135
|
+
for _, keys in reversed(self.layers):
|
|
136
|
+
flat.update((k, v) for k, v in keys.items() if "." not in k)
|
|
137
|
+
self._flat = flat
|
|
138
|
+
|
|
139
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
140
|
+
return self._flat.get(key, default)
|
|
141
|
+
|
|
142
|
+
def __getitem__(self, key: str) -> Any:
|
|
143
|
+
return self._flat[key]
|
|
144
|
+
|
|
145
|
+
def __contains__(self, key: object) -> bool:
|
|
146
|
+
return key in self._flat
|
|
147
|
+
|
|
148
|
+
def label(self, key: str) -> str:
|
|
149
|
+
"""``style "name" key`` for the nearest style that has ``key``, else
|
|
150
|
+
``default key``."""
|
|
151
|
+
for label, keys in self.layers:
|
|
152
|
+
if key in keys:
|
|
153
|
+
return f"style {label} {key}"
|
|
154
|
+
return f"default {key}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
#: The theme of objects outside a figure, and of figures without a style.
|
|
158
|
+
BASE_THEME = Theme()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Setting:
|
|
162
|
+
"""One declared setting: ``check``, its :class:`~plot_io.validators.Validator` (a
|
|
163
|
+
function is wrapped in a :class:`~plot_io.validators.Custom`), validates and normalizes
|
|
164
|
+
a value (raising ``ValueError``). ``expr``: ``var()`` is accepted (default: unless the
|
|
165
|
+
setting is text). ``auto(obj)``: the final value of ``"auto"``, computed from the model
|
|
166
|
+
when the setting is resolved. ``group``, ``field``: the setting group it belongs to and
|
|
167
|
+
its member name there. Defaults are in :data:`plot_io.defaults.DEFAULTS`; ``default``
|
|
168
|
+
is the default of a class that has no ``DEFAULTS`` key for the setting (a plot type
|
|
169
|
+
added by a user)."""
|
|
170
|
+
|
|
171
|
+
__slots__ = ("name", "check", "inherited", "doc", "expr", "auto", "default", "group",
|
|
172
|
+
"field", "prefix")
|
|
173
|
+
|
|
174
|
+
def __init__(self, check: Validator | Callable[[Any], Any] | None = None, *,
|
|
175
|
+
inherited: bool = False, doc: str = "", expr: bool | None = None,
|
|
176
|
+
auto: Callable[[Any], Any] | None = None, default: Any = _MISS) -> None:
|
|
177
|
+
self.name = ""
|
|
178
|
+
self.check: Validator = as_validator(check)
|
|
179
|
+
self.inherited = inherited
|
|
180
|
+
self.doc = doc
|
|
181
|
+
self.expr = expr if expr is not None else check not in (text, str, family, anything,
|
|
182
|
+
None)
|
|
183
|
+
self.auto = auto
|
|
184
|
+
#: the default of a class outside ``DEFAULTS`` (a plot type a user adds), used when
|
|
185
|
+
#: no ``DEFAULTS`` key applies
|
|
186
|
+
self.default = default
|
|
187
|
+
self.group: Any = None
|
|
188
|
+
self.field: str | None = None
|
|
189
|
+
self.prefix = ""
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def kind(self) -> str | None:
|
|
193
|
+
"""``"length"``, ``"angle"``, ``"number"`` or ``None``."""
|
|
194
|
+
return self.check.kind
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def group_key(self) -> str | None:
|
|
198
|
+
"""The ``"Group.member"`` key of this setting's group member."""
|
|
199
|
+
g = self.group
|
|
200
|
+
return None if g is None else f"{g.__name__}.{self.field}"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def table(**settings: Setting) -> dict[str, Setting]:
|
|
204
|
+
"""A ``SETTINGS`` table, with each setting's name filled in."""
|
|
205
|
+
for name, s in settings.items():
|
|
206
|
+
s.name = name
|
|
207
|
+
return settings
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
#: Setting names suggested for a name of the same idea that a class lacks (Hist's
|
|
211
|
+
#: ``shape`` for ``kind``, Bar's ``y_error`` for ``error``, Area's ``y`` for ``ys``).
|
|
212
|
+
SAME_IDEA = {"kind": "shape", "error": "y_error", "ys": "y"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def unknown_message(name: str, candidates: Iterable[str], what: str,
|
|
216
|
+
own: Iterable[str] = ()) -> str:
|
|
217
|
+
"""The error of an unknown setting ``name`` of ``what`` among ``candidates``; ``own``:
|
|
218
|
+
the candidates of the class itself, preferred for a matplotlib name's hint."""
|
|
219
|
+
if name == "theme":
|
|
220
|
+
return ("unknown setting 'theme': use style= (a style name, or a dict of settings "
|
|
221
|
+
"with an optional \"base\")")
|
|
222
|
+
candidates = list(candidates)
|
|
223
|
+
if name == "label" and "name" in candidates:
|
|
224
|
+
return f"unknown {what} setting 'label': use name= (the legend entry)"
|
|
225
|
+
if name.endswith("alpha"):
|
|
226
|
+
want = name[:-5] + "opacity"
|
|
227
|
+
if want in candidates or name == "alpha":
|
|
228
|
+
return (f"unknown {what} setting {name!r}: use {want}= (an opacity; alpha is "
|
|
229
|
+
"the alpha channel of a color)")
|
|
230
|
+
if name in ("color_map", "cmap") and "color_map" not in candidates \
|
|
231
|
+
and "sequential_map" in candidates:
|
|
232
|
+
from .color.maps import COLOR_MAP_HINT
|
|
233
|
+
return f"unknown {what} setting {name!r}: {COLOR_MAP_HINT}"
|
|
234
|
+
use = hints.setting(name, own) or hints.setting(name, candidates)
|
|
235
|
+
if use is not None:
|
|
236
|
+
return f"unknown {what} setting {name!r}: use {use}"
|
|
237
|
+
axis = hints.axis_setting(name)
|
|
238
|
+
if axis is not None:
|
|
239
|
+
return f"unknown {what} setting {name!r}: use {axis}"
|
|
240
|
+
near = [SAME_IDEA[name]] if SAME_IDEA.get(name) in candidates else \
|
|
241
|
+
difflib.get_close_matches(name, candidates, n=1, cutoff=0.6)
|
|
242
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
243
|
+
return f"unknown {what} setting {name!r}{hint}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Plan:
|
|
247
|
+
"""Where a setting of one class is looked up (see :func:`cascade`), computed once per
|
|
248
|
+
class: its owner style keys, its default and the key holding it, its group style key,
|
|
249
|
+
whether it is inherited and whether its default refers to the inherited value, and the
|
|
250
|
+
class rule."""
|
|
251
|
+
|
|
252
|
+
__slots__ = ("setting", "style_keys", "default", "default_key", "group_key",
|
|
253
|
+
"inherited", "inherit_default", "rule", "rule_key", "rule_first", "rule_at",
|
|
254
|
+
"shared")
|
|
255
|
+
|
|
256
|
+
def __init__(self, obj: "Settings", name: str) -> None:
|
|
257
|
+
s = obj.SETTINGS[name]
|
|
258
|
+
self.setting = s
|
|
259
|
+
self.inherited = s.inherited
|
|
260
|
+
keys: list[str] = []
|
|
261
|
+
self.default: Any = _MISS
|
|
262
|
+
self.default_key: str | None = None
|
|
263
|
+
for level in obj._levels(name):
|
|
264
|
+
keys.extend(f"{c}.{name}" for c in level)
|
|
265
|
+
hit = next((f"{c}.{name}" for c in level if f"{c}.{name}" in DEFAULTS), None)
|
|
266
|
+
if hit is not None:
|
|
267
|
+
self.default_key = hit
|
|
268
|
+
break
|
|
269
|
+
self.style_keys = tuple(keys)
|
|
270
|
+
self.group_key = s.group_key
|
|
271
|
+
if self.default_key is None and self.group_key is not None \
|
|
272
|
+
and self.group_key in DEFAULTS:
|
|
273
|
+
self.default_key = self.group_key
|
|
274
|
+
if self.default_key is None and s.default is not _MISS:
|
|
275
|
+
self.default_key = f"{type(obj).__name__}.{name}" # declared: not in DEFAULTS
|
|
276
|
+
if self.default_key is not None:
|
|
277
|
+
self.default = default_of(s, self.default_key)
|
|
278
|
+
self.inherit_default = self.inherited and self.default is not _MISS \
|
|
279
|
+
and refers_inherit(self.default)
|
|
280
|
+
#: whether values set on another object (``obj._shared()``) come before the rest
|
|
281
|
+
self.shared = type(obj)._shared is not Settings._shared
|
|
282
|
+
self.rule = None
|
|
283
|
+
self.rule_key: str | None = None
|
|
284
|
+
#: a rule of given values runs before the styles; another after the style keys of
|
|
285
|
+
#: its class and of more specific ones (``style_keys[:rule_at]``)
|
|
286
|
+
self.rule_first = False
|
|
287
|
+
self.rule_at = 0
|
|
288
|
+
for c in type(obj).__mro__:
|
|
289
|
+
r = c.__dict__.get("RULES", {}).get(name)
|
|
290
|
+
if r is not None:
|
|
291
|
+
self.rule, self.rule_key = r, f"{c.__name__}.{name}"
|
|
292
|
+
self.rule_first = isinstance(r, _GivenRule)
|
|
293
|
+
keys_ = self.style_keys
|
|
294
|
+
self.rule_at = keys_.index(self.rule_key) + 1 if self.rule_key in keys_ \
|
|
295
|
+
else len(keys_)
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
_PLANS: dict[Any, dict[str, Plan]] = {}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def class_default(cls: Any, name: str, keys: tuple[str, ...] = (), role: Any = None) -> Any:
|
|
303
|
+
"""The ``DEFAULTS`` entry (as written) of setting ``name`` of objects of ``cls``, where
|
|
304
|
+
the cascade's :class:`Plan` finds it: the first of ``keys`` present, else along the
|
|
305
|
+
class's levels (``role``: an axis' ``"x"``, ``"y"`` or ``"c"``), else its group's.
|
|
306
|
+
Raises ``KeyError``."""
|
|
307
|
+
for k in keys:
|
|
308
|
+
if k in DEFAULTS:
|
|
309
|
+
return DEFAULTS[k]
|
|
310
|
+
obj: Any = object.__new__(cls) # the class's levels, without a model
|
|
311
|
+
if role is not None:
|
|
312
|
+
obj.role = role
|
|
313
|
+
obj.__dict__["_plans"] = {}
|
|
314
|
+
try:
|
|
315
|
+
plan = Plan(obj, name)
|
|
316
|
+
except AttributeError: # levels that need an instance's state
|
|
317
|
+
obj.__dict__["_levels"] = lambda _: _mro_levels(cls)
|
|
318
|
+
plan = Plan(obj, name)
|
|
319
|
+
if plan.default_key is None:
|
|
320
|
+
raise KeyError(f"no default for {cls.__name__}.{name}")
|
|
321
|
+
if plan.default_key not in DEFAULTS:
|
|
322
|
+
return plan.setting.default
|
|
323
|
+
return DEFAULTS[plan.default_key]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def cascade(obj: "Settings", name: str,
|
|
327
|
+
theme: "Theme | None" = None) -> tuple[Any, "Settings", str, str | None]:
|
|
328
|
+
"""Where the value of setting ``name`` of ``obj`` comes from: ``(raw, holder, how,
|
|
329
|
+
key)``. ``how`` is ``"own"``, ``"figure"`` (set on ``holder``, the figure's axis of the
|
|
330
|
+
role), ``"rule"``, ``"style"`` (``key``: the style key),
|
|
331
|
+
``"default"`` (``key``: the ``DEFAULTS`` key), ``"parent"`` (``holder``: the ancestor
|
|
332
|
+
whose value it is; ``raw`` unused), ``"theme"`` (the bare inherited name) or
|
|
333
|
+
``"unset"``. ``raw`` is the stored value, as checked. ``theme``: ``obj._theme()``,
|
|
334
|
+
when the caller has it."""
|
|
335
|
+
v = _own(obj, name)
|
|
336
|
+
if v is not _MISS:
|
|
337
|
+
return v, obj, "own", None
|
|
338
|
+
plan = obj._plan(name)
|
|
339
|
+
if plan.shared:
|
|
340
|
+
src = obj._shared()
|
|
341
|
+
if src is not None:
|
|
342
|
+
v = _own(src, name)
|
|
343
|
+
if v is not _MISS:
|
|
344
|
+
return v, src, "figure", None
|
|
345
|
+
rule = plan.rule
|
|
346
|
+
if rule is not None and plan.rule_first:
|
|
347
|
+
v = _rule_value(obj, plan)
|
|
348
|
+
if v is not DECLINE:
|
|
349
|
+
return v, obj, "rule", plan.rule_key
|
|
350
|
+
rule = None
|
|
351
|
+
if theme is None:
|
|
352
|
+
theme = obj._theme()
|
|
353
|
+
group = None if plan.inherited else plan.group_key
|
|
354
|
+
keys_ = plan.style_keys
|
|
355
|
+
layers = theme.layers
|
|
356
|
+
if plan.inherited: # a style's bare name ends the farther styles
|
|
357
|
+
for i, (_, keys) in enumerate(layers):
|
|
358
|
+
if name in keys:
|
|
359
|
+
layers = layers[:i + 1]
|
|
360
|
+
break
|
|
361
|
+
if rule is not None:
|
|
362
|
+
near = keys_[:plan.rule_at]
|
|
363
|
+
for _, keys in layers:
|
|
364
|
+
for k in near:
|
|
365
|
+
v = keys.get(k, _MISS)
|
|
366
|
+
if v is not _MISS:
|
|
367
|
+
return v, obj, "style", k
|
|
368
|
+
v = _rule_value(obj, plan)
|
|
369
|
+
if v is not DECLINE:
|
|
370
|
+
return v, obj, "rule", plan.rule_key
|
|
371
|
+
keys_ = keys_[plan.rule_at:]
|
|
372
|
+
for _, keys in layers:
|
|
373
|
+
for k in keys_:
|
|
374
|
+
v = keys.get(k, _MISS)
|
|
375
|
+
if v is not _MISS:
|
|
376
|
+
return v, obj, "style", k
|
|
377
|
+
if group is not None:
|
|
378
|
+
v = keys.get(group, _MISS)
|
|
379
|
+
if v is not _MISS:
|
|
380
|
+
return v, obj, "style", group
|
|
381
|
+
if plan.inherited:
|
|
382
|
+
if plan.inherit_default:
|
|
383
|
+
return plan.default, obj, "default", plan.default_key
|
|
384
|
+
p = obj._parent()
|
|
385
|
+
while p is not None:
|
|
386
|
+
if name in p.SETTINGS:
|
|
387
|
+
return None, p, "parent", None
|
|
388
|
+
p = p._parent()
|
|
389
|
+
if plan.default is not _MISS:
|
|
390
|
+
return plan.default, obj, "default", plan.default_key
|
|
391
|
+
if plan.inherited:
|
|
392
|
+
return theme.get(name), obj, "theme", name
|
|
393
|
+
return None, obj, "unset", None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
#: ``DEFAULTS``' bare values as checked, by (setting, value).
|
|
397
|
+
_CHECKED_DEFAULTS: dict[tuple[int, str], Any] = {}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _checked_default(s: Setting, raw: str) -> Any:
|
|
401
|
+
key = (id(s), raw)
|
|
402
|
+
checked = _CHECKED_DEFAULTS.get(key, _MISS)
|
|
403
|
+
if checked is _MISS:
|
|
404
|
+
try:
|
|
405
|
+
checked = check_value(s, raw)
|
|
406
|
+
except (TypeError, ValueError):
|
|
407
|
+
checked = raw
|
|
408
|
+
_CHECKED_DEFAULTS[key] = checked
|
|
409
|
+
return checked
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _variable_default(s: Setting, raw: str) -> bool:
|
|
413
|
+
"""Whether a bare default of ``DEFAULTS`` given as text (a named color, a color map
|
|
414
|
+
name) depends on the color variant."""
|
|
415
|
+
return has_fixed(_checked_default(s, raw))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _varied_default(obj: "Settings", s: Setting, raw: str) -> Any:
|
|
419
|
+
"""A bare default of ``DEFAULTS`` given as text in ``obj``'s color variant; ``None``
|
|
420
|
+
in the base variant (the text as given)."""
|
|
421
|
+
from .color.values import variant_of
|
|
422
|
+
if variant_of(obj) == "base":
|
|
423
|
+
return None
|
|
424
|
+
return _checked_default(s, raw).in_context(obj)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _rule_value(obj: "Settings", plan: Plan) -> Any:
|
|
428
|
+
assert plan.rule is not None
|
|
429
|
+
v = plan.rule(obj)
|
|
430
|
+
if v is not DECLINE and v is not None and not isinstance(v, Value):
|
|
431
|
+
v = check_value(plan.setting, v)
|
|
432
|
+
return v
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def given_rule(rule: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
|
436
|
+
"""Marks a class rule whose value the user gave elsewhere (a series' own setting): it
|
|
437
|
+
comes before the styles."""
|
|
438
|
+
return _GivenRule(rule)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class _GivenRule:
|
|
442
|
+
"""A class rule of values given elsewhere (see :func:`given_rule`)."""
|
|
443
|
+
|
|
444
|
+
__slots__ = ("rule",)
|
|
445
|
+
|
|
446
|
+
def __init__(self, rule: Callable[[Any], Any]) -> None:
|
|
447
|
+
self.rule = rule
|
|
448
|
+
|
|
449
|
+
def __call__(self, obj: Any) -> Any:
|
|
450
|
+
return self.rule(obj)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class Settings:
|
|
454
|
+
"""Base of every object with settings."""
|
|
455
|
+
|
|
456
|
+
SETTINGS: dict[str, Setting] = {}
|
|
457
|
+
#: Class rules: ``{name: rule(obj)}`` giving the value of a setting (as given) from
|
|
458
|
+
#: the model, a default after the style keys of the class (and of more specific ones);
|
|
459
|
+
#: ``DECLINE`` when the rule does not apply. A :func:`given_rule` comes before them.
|
|
460
|
+
RULES: dict[str, Callable[[Any], Any]] = {}
|
|
461
|
+
|
|
462
|
+
def __init__(self, **settings: Any) -> None:
|
|
463
|
+
self._values: dict[str, Any] = {}
|
|
464
|
+
self.set(**settings)
|
|
465
|
+
|
|
466
|
+
def _parent(self) -> "Settings | None":
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
def _shared(self) -> "Settings | None":
|
|
470
|
+
"""An object whose own values come right after this object's (see
|
|
471
|
+
:func:`cascade`); classes that have one override this."""
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
def _theme(self) -> Theme:
|
|
475
|
+
p = self._parent()
|
|
476
|
+
return p._theme() if p is not None else BASE_THEME
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def kind(self) -> str:
|
|
480
|
+
return type(self).__name__
|
|
481
|
+
|
|
482
|
+
def set(self, **settings: Any) -> Self:
|
|
483
|
+
"""Set settings; ``None`` goes back to the inherited or default value."""
|
|
484
|
+
settings = canonical_kwargs(settings, self.SETTINGS, self.kind)
|
|
485
|
+
checked = {}
|
|
486
|
+
for name, value in settings.items():
|
|
487
|
+
s = self.SETTINGS.get(name)
|
|
488
|
+
if s is None:
|
|
489
|
+
raise SettingError(unknown_message(name, self.SETTINGS, self.kind), self, name,
|
|
490
|
+
value)
|
|
491
|
+
checked[name] = None if value is None else self._checked(s, name, value)
|
|
492
|
+
for name, value in checked.items():
|
|
493
|
+
if value is None:
|
|
494
|
+
self._values.pop(name, None)
|
|
495
|
+
else:
|
|
496
|
+
self._values[name] = value
|
|
497
|
+
r = _spec.current()
|
|
498
|
+
if r is not None:
|
|
499
|
+
r.changed(self)
|
|
500
|
+
return self
|
|
501
|
+
|
|
502
|
+
def _checked(self, s: Setting, name: str, value: Any) -> Any:
|
|
503
|
+
"""``value`` checked for setting ``s`` (``name``) of this object; a ``var()`` of
|
|
504
|
+
another of its settings is checked against it."""
|
|
505
|
+
try:
|
|
506
|
+
out = check_value(s, value)
|
|
507
|
+
except (TypeError, ValueError) as e:
|
|
508
|
+
raise SettingError(f"{self.kind} setting {name!r}: {e}", self, name,
|
|
509
|
+
value) from None
|
|
510
|
+
if isinstance(out, Var) and out.name in (name, "inherit") and out.fallback is None \
|
|
511
|
+
and not self._inherits(name):
|
|
512
|
+
raise SettingError(f"{self.kind} setting {name!r}: {out.source}: no parent of "
|
|
513
|
+
f"{self.kind} and no style has {name!r}", self, name, value)
|
|
514
|
+
if isinstance(out, Var) and out.name in self.SETTINGS \
|
|
515
|
+
and out.name not in (name, "inherit"):
|
|
516
|
+
target = self.SETTINGS[out.name].check
|
|
517
|
+
if s.check.accepts(target) is False:
|
|
518
|
+
raise SettingError(f"{self.kind} setting {name!r}: {out.source} is "
|
|
519
|
+
f"{target.noun}, expected {s.check.noun}", self, name, value)
|
|
520
|
+
return out
|
|
521
|
+
|
|
522
|
+
def _levels(self, name: str) -> tuple[tuple[str, ...], ...]:
|
|
523
|
+
"""The classes whose style keys and defaults apply to setting ``name``, most
|
|
524
|
+
specific first, in levels: a level with a default ends the search."""
|
|
525
|
+
return _mro_levels(type(self))
|
|
526
|
+
|
|
527
|
+
def _level_key(self) -> Any:
|
|
528
|
+
"""What the levels depend on besides the class (an axis: its role)."""
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
def _plan(self, name: str) -> Plan:
|
|
532
|
+
table_ = self.__dict__.get("_plans")
|
|
533
|
+
if table_ is None:
|
|
534
|
+
key = (type(self), self._level_key())
|
|
535
|
+
table_ = _PLANS.get(key)
|
|
536
|
+
if table_ is None:
|
|
537
|
+
table_ = _PLANS[key] = {}
|
|
538
|
+
self.__dict__["_plans"] = table_
|
|
539
|
+
plan = table_.get(name)
|
|
540
|
+
if plan is None:
|
|
541
|
+
plan = table_[name] = Plan(self, name)
|
|
542
|
+
return plan
|
|
543
|
+
|
|
544
|
+
def _name(self, name: str) -> str:
|
|
545
|
+
"""The setting ``name`` refers to (a shorthand's full name); ``ValueError`` for an
|
|
546
|
+
unknown name."""
|
|
547
|
+
if name in self.SETTINGS:
|
|
548
|
+
return name
|
|
549
|
+
key = canonical(name, self.SETTINGS)
|
|
550
|
+
if key not in self.SETTINGS:
|
|
551
|
+
raise SettingError(unknown_message(name, self.SETTINGS, self.kind), self, name)
|
|
552
|
+
return key
|
|
553
|
+
|
|
554
|
+
def is_set(self, name: str) -> bool:
|
|
555
|
+
"""Whether setting ``name`` is set on this object."""
|
|
556
|
+
return _own(self, self._name(name)) is not _MISS
|
|
557
|
+
|
|
558
|
+
def _raw(self, name: str) -> Any:
|
|
559
|
+
"""The stored value of setting ``name`` (as checked, before resolving), wherever
|
|
560
|
+
the cascade finds it."""
|
|
561
|
+
raw, holder, how, _ = cascade(self, name)
|
|
562
|
+
return holder._raw(name) if how == "parent" else raw
|
|
563
|
+
|
|
564
|
+
def get(self, name: str) -> Any:
|
|
565
|
+
"""The value of a setting as given (``"4.5pt"``, ``"var(color)"``, ``6``): its own,
|
|
566
|
+
the style's, inherited or the default; see :meth:`resolve` for the final value."""
|
|
567
|
+
return given(self._raw(self._name(name)))
|
|
568
|
+
|
|
569
|
+
def resolve(self, name: str, *, of: Any = None, scale: Any = None, time: bool = False,
|
|
570
|
+
unit: str = "px") -> Any:
|
|
571
|
+
"""The final value of a setting: lengths in px (``unit="data"``: in data units),
|
|
572
|
+
angles in degrees, ``var()`` and ``calc()`` evaluated, ``"auto"`` resolved by the
|
|
573
|
+
setting's rule, scaling applied.
|
|
574
|
+
|
|
575
|
+
``of``: what ``%`` is of, for settings whose reference the caller knows; ``scale``:
|
|
576
|
+
px per data unit, for ``data`` units; ``time``: durations are accepted."""
|
|
577
|
+
if name not in self.SETTINGS:
|
|
578
|
+
name = self._name(name)
|
|
579
|
+
if of is None and scale is None and not time and unit == "px":
|
|
580
|
+
r = _spec._CURRENT.get()
|
|
581
|
+
if r is not None: # the value kept by the render, else resolved
|
|
582
|
+
d = r.values.get(id(self))
|
|
583
|
+
v = _MISS if d is None else d.get(name, _MISS)
|
|
584
|
+
if v is _MISS:
|
|
585
|
+
v = r.lookup(self, name)
|
|
586
|
+
if v is not _spec.LAZY:
|
|
587
|
+
return v
|
|
588
|
+
return self._resolve(name, of, scale, time, unit)
|
|
589
|
+
|
|
590
|
+
def _resolve(self, name: str, of: Any = None, scale: Any = None, time: bool = False,
|
|
591
|
+
unit: str = "px", theme: "Theme | None" = None) -> Any:
|
|
592
|
+
""":meth:`resolve` through the cascade, without the render's values."""
|
|
593
|
+
raw, holder, how, _ = cascade(self, name, theme)
|
|
594
|
+
s = self.SETTINGS[name]
|
|
595
|
+
if how == "parent":
|
|
596
|
+
# named colors and color maps: in this object's color variant
|
|
597
|
+
raw = holder._raw(name)
|
|
598
|
+
if has_fixed(raw):
|
|
599
|
+
return resolve_raw(self, name, s.check, raw, of, scale, time, unit)
|
|
600
|
+
if not (isinstance(raw, str) and _variable_default(s, raw)):
|
|
601
|
+
return holder.resolve(name)
|
|
602
|
+
how = "theme"
|
|
603
|
+
if how == "theme" and isinstance(raw, str) and _variable_default(s, raw):
|
|
604
|
+
varied = _varied_default(self, s, raw)
|
|
605
|
+
return raw if varied is None else varied
|
|
606
|
+
if raw is None or not needs_resolving(raw, s.check):
|
|
607
|
+
out = raw
|
|
608
|
+
elif may_refer(raw):
|
|
609
|
+
with guard(self, name):
|
|
610
|
+
out = resolve_raw(self, name, s.check, raw, of, scale, time, unit)
|
|
611
|
+
else:
|
|
612
|
+
out = resolve_raw(self, name, s.check, raw, of, scale, time, unit)
|
|
613
|
+
if s.auto is not None and isinstance(out, str) and out == "auto":
|
|
614
|
+
out = s.auto(self)
|
|
615
|
+
return out
|
|
616
|
+
|
|
617
|
+
def resolve_linear(self, name: str, **options: Any) -> tuple[float, float]:
|
|
618
|
+
"""``(a, b)`` such that the setting resolves to ``a + b × of`` (``%`` of ``of``):
|
|
619
|
+
for settings whose reference varies item by item."""
|
|
620
|
+
a = self.resolve(name, of=0.0, **options)
|
|
621
|
+
return a, self.resolve(name, of=1.0, **options) - a
|
|
622
|
+
|
|
623
|
+
def explain(self, name: str, as_dict: bool = False) -> Any:
|
|
624
|
+
"""Where the value of setting ``name`` comes from: its source (set here, a style
|
|
625
|
+
key, inherited, the default), the value as given, what it refers to (``var()``,
|
|
626
|
+
``%``, ``em``) and the final value. Text, or a dict with ``as_dict``."""
|
|
627
|
+
from .units import explain_tree
|
|
628
|
+
return explain_tree(self, self._name(name), as_dict)
|
|
629
|
+
|
|
630
|
+
def _source(self, name: str) -> tuple[str, "Settings"]:
|
|
631
|
+
"""Where ``name``'s stored value comes from, and the object holding it."""
|
|
632
|
+
raw, holder, how, key = cascade(self, name)
|
|
633
|
+
if how == "own":
|
|
634
|
+
return f"set on {self.kind}", self
|
|
635
|
+
if how == "figure":
|
|
636
|
+
return f"set on the figure's {getattr(self, 'role', '')} axis", holder
|
|
637
|
+
if how == "rule":
|
|
638
|
+
return f"rule {key}", self
|
|
639
|
+
if how == "style":
|
|
640
|
+
return self._theme().label(key or name), self
|
|
641
|
+
if how == "default":
|
|
642
|
+
return f"default {key}", self
|
|
643
|
+
if how == "parent":
|
|
644
|
+
src, owner = holder._source(name)
|
|
645
|
+
if src.startswith("inherited from "):
|
|
646
|
+
return src, owner
|
|
647
|
+
return f"inherited from {holder.kind}: {src}", owner
|
|
648
|
+
if how == "theme":
|
|
649
|
+
return self._theme().label(name) if name in self._theme() else "unset", self
|
|
650
|
+
return "unset", self
|
|
651
|
+
|
|
652
|
+
def settings(self) -> dict[str, Any]:
|
|
653
|
+
"""The settings set explicitly on this object, as given."""
|
|
654
|
+
return {k: given(v) for k, v in self._values.items()}
|
|
655
|
+
|
|
656
|
+
def _children(self) -> "list[Settings]":
|
|
657
|
+
"""The model objects below this one."""
|
|
658
|
+
return []
|
|
659
|
+
|
|
660
|
+
# -- what resolution needs ------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
def _ancestors(self) -> "list[Settings]":
|
|
663
|
+
out = []
|
|
664
|
+
p = self._parent()
|
|
665
|
+
while p is not None:
|
|
666
|
+
out.append(p)
|
|
667
|
+
p = p._parent()
|
|
668
|
+
return out
|
|
669
|
+
|
|
670
|
+
def _root(self) -> "Settings":
|
|
671
|
+
r: Settings = self
|
|
672
|
+
p = r._parent()
|
|
673
|
+
while p is not None:
|
|
674
|
+
r, p = p, p._parent()
|
|
675
|
+
return r
|
|
676
|
+
|
|
677
|
+
def _scaling(self) -> float:
|
|
678
|
+
root = self._root()
|
|
679
|
+
if "scaling" in root.SETTINGS:
|
|
680
|
+
return root.resolve("scaling")
|
|
681
|
+
return 1.0
|
|
682
|
+
|
|
683
|
+
def _font_size(self) -> float:
|
|
684
|
+
"""The font size ``em`` is of: this object's ``font_size``, else its parent's."""
|
|
685
|
+
if "font_size" in self.SETTINGS:
|
|
686
|
+
return self.resolve("font_size")
|
|
687
|
+
return self._parent_font_size()
|
|
688
|
+
|
|
689
|
+
def _parent_font_size(self) -> float:
|
|
690
|
+
for p in self._ancestors():
|
|
691
|
+
if "font_size" in p.SETTINGS:
|
|
692
|
+
return p.resolve("font_size")
|
|
693
|
+
return self._root_font_size()
|
|
694
|
+
|
|
695
|
+
def _root_font_size(self) -> float:
|
|
696
|
+
root = self._root()
|
|
697
|
+
if root is not self and "font_size" in root.SETTINGS:
|
|
698
|
+
return root.resolve("font_size")
|
|
699
|
+
v = self._theme().get("font_size", 11.0)
|
|
700
|
+
return float(v) * self._scaling() if isinstance(v, (int, float)) else 11.0
|
|
701
|
+
|
|
702
|
+
def _box(self, kind: str, name: str) -> float:
|
|
703
|
+
"""The size (px) of a layout box, while the figure is laid out."""
|
|
704
|
+
r = _spec.current()
|
|
705
|
+
if r is not None:
|
|
706
|
+
for o in [self] + self._ancestors():
|
|
707
|
+
boxes = r.boxes.get(id(o))
|
|
708
|
+
if boxes and kind in boxes:
|
|
709
|
+
return boxes[kind]
|
|
710
|
+
raise _spec.NeedsBox(f"{self.kind} setting {name!r}: % of the "
|
|
711
|
+
f"{kind.replace('_', ' ')} is known only while the figure is "
|
|
712
|
+
f"drawn")
|
|
713
|
+
|
|
714
|
+
def _custom_var(self, key: str) -> Any:
|
|
715
|
+
"""Custom variable ``--name``: from the nearest ``vars`` that has it, else the
|
|
716
|
+
style's."""
|
|
717
|
+
for o in [self] + self._ancestors():
|
|
718
|
+
if "vars" in o.SETTINGS and o.is_set("vars"):
|
|
719
|
+
d = o._values["vars"]
|
|
720
|
+
if key[2:] in d:
|
|
721
|
+
return d[key[2:]]
|
|
722
|
+
return self._theme().get(key)
|
|
723
|
+
|
|
724
|
+
def _inherits(self, name: str) -> bool:
|
|
725
|
+
"""Whether an ancestor or the style has ``name`` (what ``inherit`` reads)."""
|
|
726
|
+
return any(name in p.SETTINGS for p in self._ancestors()) or name in self._theme()
|
|
727
|
+
|
|
728
|
+
def _parent_value(self, name: str) -> Any:
|
|
729
|
+
"""``name`` as inherited, skipping this object (``inherit``, or ``var()`` of a
|
|
730
|
+
setting's own name)."""
|
|
731
|
+
for p in self._ancestors():
|
|
732
|
+
if name in p.SETTINGS:
|
|
733
|
+
return p.resolve(name)
|
|
734
|
+
theme = self._theme()
|
|
735
|
+
if name in theme:
|
|
736
|
+
s = self.SETTINGS.get(name)
|
|
737
|
+
raw = theme[name]
|
|
738
|
+
if s is not None and needs_resolving(raw, s.check):
|
|
739
|
+
return resolve_raw(self, name, s.check, raw)
|
|
740
|
+
return raw
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
def _resolve_any(self, name: str) -> Any:
|
|
744
|
+
"""``name`` of this object, or of the nearest ancestor that has it."""
|
|
745
|
+
for o in [self] + self._ancestors():
|
|
746
|
+
if name in o.SETTINGS:
|
|
747
|
+
return o.resolve(name)
|
|
748
|
+
if name in self._theme():
|
|
749
|
+
return self._theme()[name]
|
|
750
|
+
raise ValueError(f"var({name}): no setting {name!r} on {self.kind} or its parents")
|
|
751
|
+
|
|
752
|
+
def _check_of(self, name: str, own: bool = True) -> Validator | None:
|
|
753
|
+
"""The validator of setting ``name`` of this object (``own``) or of the nearest
|
|
754
|
+
ancestor that has it; without ``own``, this object's when no ancestor has it (the
|
|
755
|
+
style's bare value is resolved by it); ``None``: no such setting."""
|
|
756
|
+
for o in ([self] if own else []) + self._ancestors():
|
|
757
|
+
if name in o.SETTINGS:
|
|
758
|
+
return o.SETTINGS[name].check
|
|
759
|
+
if not own and name in self.SETTINGS:
|
|
760
|
+
return self.SETTINGS[name].check
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
_MRO_LEVELS: dict[type, tuple[tuple[str, ...], ...]] = {}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _mro_levels(cls: type) -> tuple[tuple[str, ...], ...]:
|
|
768
|
+
out = _MRO_LEVELS.get(cls)
|
|
769
|
+
if out is None:
|
|
770
|
+
out = _MRO_LEVELS[cls] = tuple((c.__name__,) for c in cls.__mro__
|
|
771
|
+
if issubclass(c, Settings) and c is not Settings)
|
|
772
|
+
return out
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def check_value(s: Setting, value: Any) -> Any:
|
|
776
|
+
"""``value`` checked for setting ``s``: ``var()`` text as a :class:`Var`, else through
|
|
777
|
+
the setting's validator."""
|
|
778
|
+
if isinstance(value, Value): # already checked (a style built on another)
|
|
779
|
+
return value
|
|
780
|
+
if s.expr and isinstance(value, str):
|
|
781
|
+
v = parse(value, calc=False)
|
|
782
|
+
if v is not None:
|
|
783
|
+
return v
|
|
784
|
+
return s.check(value)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def route(kwargs: dict[str, Any], levels: list[type[Settings]],
|
|
788
|
+
extra: dict[int, Iterable[str]] | None = None) -> list[dict[str, Any]]:
|
|
789
|
+
"""Split ``kwargs`` among ``levels`` (lowest first): each name goes to the lowest
|
|
790
|
+
level declaring it (``extra`` adds names a level accepts besides its settings)."""
|
|
791
|
+
out: list[dict[str, Any]] = [{} for _ in levels]
|
|
792
|
+
given_as: dict[tuple[int, str], str] = {}
|
|
793
|
+
for name, value in kwargs.items():
|
|
794
|
+
for i, cls in enumerate(levels):
|
|
795
|
+
key = canonical(name, cls.SETTINGS)
|
|
796
|
+
if key in cls.SETTINGS or (extra and name in extra.get(i, ())):
|
|
797
|
+
if key in out[i]:
|
|
798
|
+
raise ValueError(f"{cls.__name__}: {given_as[i, key]!r} and {name!r} are "
|
|
799
|
+
f"the same setting; give one")
|
|
800
|
+
out[i][key] = value
|
|
801
|
+
given_as[i, key] = name
|
|
802
|
+
break
|
|
803
|
+
else:
|
|
804
|
+
names = set().union(*(cls.SETTINGS for cls in levels))
|
|
805
|
+
raise ValueError(unknown_message(name, names, levels[0].__name__,
|
|
806
|
+
levels[0].SETTINGS))
|
|
807
|
+
return out
|