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/units.py
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
"""Setting values: dimensions with units, ``var()`` and CSS math functions (``calc()``,
|
|
2
|
+
``min()``, …: vec_io's grammar, :func:`vec_io.units.parse_math`).
|
|
3
|
+
|
|
4
|
+
A validator parses a value once, when it is set, into a plain Python value or a
|
|
5
|
+
:class:`Value` (:class:`Dim`, :class:`Var`, :class:`Calc`); :meth:`Settings.get` returns it
|
|
6
|
+
as given and :meth:`Settings.resolve` turns it into px (lengths), degrees (angles) or a
|
|
7
|
+
plain value.
|
|
8
|
+
|
|
9
|
+
Units: absolute ``px pt pc in cm mm Q``; ``em`` (the object's ``font_size``; in a
|
|
10
|
+
``font_size`` setting, the parent's); ``rem`` (the figure's ``font_size``); ``%`` (of the
|
|
11
|
+
reference each setting declares, :data:`REFERENTS`); ``d`` or ``data`` (data units along an
|
|
12
|
+
axis; on a time axis ``d`` is a day) and durations; ``fr`` (figure widths and heights);
|
|
13
|
+
angles ``deg grad rad turn``. Bare numbers are px (degrees for angles); positions along
|
|
14
|
+
an axis (a decoration's ``min``/``max``) take bare numbers as data values.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import threading
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from vec_io.units import ANGLE_UNITS, PX_PER_UNIT, parse_math
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .validators import Validator
|
|
25
|
+
|
|
26
|
+
__all__ = ["Value", "Dim", "Fixed", "Var", "Calc", "parse", "given", "resolve_raw",
|
|
27
|
+
"SettingError", "NeedsContext", "NeedsScale", "explain_tree", "refers_inherit", "FONT_KEYWORDS",
|
|
28
|
+
"DURATIONS", "REFERENTS", "Expr", "var", "calc"]
|
|
29
|
+
|
|
30
|
+
ABSOLUTE = frozenset(PX_PER_UNIT)
|
|
31
|
+
DURATIONS = {"ms": 0.001, "s": 1.0, "min": 60.0, "h": 3600.0, "d": 86400.0, "w": 604800.0}
|
|
32
|
+
#: Font-size keywords, relative to the figure's font size (matplotlib's scale).
|
|
33
|
+
FONT_KEYWORDS = {"xx-small": 0.579, "x-small": 0.694, "small": 0.833, "medium": 1.0,
|
|
34
|
+
"large": 1.2, "x-large": 1.44, "xx-large": 1.728}
|
|
35
|
+
_RELATIVE_FONT = {"smaller": 1 / 1.2, "larger": 1.2}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Values
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class Value:
|
|
43
|
+
"""A setting value that needs resolving; ``source`` is the text as given."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("source",)
|
|
46
|
+
source: str
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return repr(self.source)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Dim(Value):
|
|
53
|
+
"""A number with a unit (``""``: bare), or a keyword (``number`` is ``None``)."""
|
|
54
|
+
|
|
55
|
+
__slots__ = ("number", "unit")
|
|
56
|
+
|
|
57
|
+
def __init__(self, source: str, number: float | None, unit: str) -> None:
|
|
58
|
+
self.source = source
|
|
59
|
+
self.number = number
|
|
60
|
+
self.unit = unit
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Fixed(Value):
|
|
64
|
+
"""A value whose final form is known when given (a named color's hex): ``source`` is
|
|
65
|
+
the text as given, ``value`` the final value."""
|
|
66
|
+
|
|
67
|
+
__slots__ = ("value",)
|
|
68
|
+
|
|
69
|
+
def __init__(self, source: str, value: Any) -> None:
|
|
70
|
+
self.source = source
|
|
71
|
+
self.value = value
|
|
72
|
+
|
|
73
|
+
def in_context(self, obj: Any) -> Any:
|
|
74
|
+
"""The final value for a setting of ``obj`` (a named color: in ``obj``'s color
|
|
75
|
+
variant)."""
|
|
76
|
+
return self.value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def varies(self) -> bool:
|
|
80
|
+
"""Whether the final value depends on the color variant."""
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def has_fixed(v: Any) -> bool:
|
|
85
|
+
"""Whether ``v`` is or holds (one level) a :class:`Fixed` value, whose final value may
|
|
86
|
+
depend on the object resolving it."""
|
|
87
|
+
return isinstance(v, Fixed) or _has_fixed(v)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _has_fixed(v: Any) -> bool:
|
|
91
|
+
if isinstance(v, dict):
|
|
92
|
+
v = v.values()
|
|
93
|
+
elif not isinstance(v, (list, tuple)):
|
|
94
|
+
return False
|
|
95
|
+
return any(isinstance(x, Fixed) for x in v)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _fixed_final(v: Any, obj: Any) -> Any:
|
|
99
|
+
"""``v`` with its :class:`Fixed` items replaced by their values for ``obj`` (one
|
|
100
|
+
level)."""
|
|
101
|
+
if isinstance(v, dict):
|
|
102
|
+
return {k: x.in_context(obj) if isinstance(x, Fixed) else x for k, x in v.items()}
|
|
103
|
+
out = [x.in_context(obj) if isinstance(x, Fixed) else x for x in v]
|
|
104
|
+
return tuple(out) if isinstance(v, tuple) else out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Var(Value):
|
|
108
|
+
"""``var(name[, fallback])``."""
|
|
109
|
+
|
|
110
|
+
__slots__ = ("name", "fallback")
|
|
111
|
+
|
|
112
|
+
def __init__(self, source: str, name: str, fallback: str | None) -> None:
|
|
113
|
+
self.source = source
|
|
114
|
+
self.name = name
|
|
115
|
+
self.fallback = fallback
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Calc(Value):
|
|
119
|
+
"""A CSS math function (``calc()``, ``min()``, ``clamp()``, …) parsed by vec_io's
|
|
120
|
+
:func:`~vec_io.units.parse_math`: ``math`` evaluates it; ``refs``: the dimensions and
|
|
121
|
+
``var()`` it holds, in order."""
|
|
122
|
+
|
|
123
|
+
__slots__ = ("math", "refs")
|
|
124
|
+
|
|
125
|
+
def __init__(self, source: str, math: Any, refs: tuple[Value, ...]) -> None:
|
|
126
|
+
self.source = source
|
|
127
|
+
self.math = math
|
|
128
|
+
self.refs = refs
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def given(v: Any) -> Any:
|
|
133
|
+
"""``v`` as it was given: the text of values, containers mapped (copies)."""
|
|
134
|
+
if isinstance(v, Value):
|
|
135
|
+
return v.source
|
|
136
|
+
if isinstance(v, tuple):
|
|
137
|
+
return tuple(given(x) for x in v)
|
|
138
|
+
if isinstance(v, list):
|
|
139
|
+
return [given(x) for x in v]
|
|
140
|
+
if isinstance(v, dict):
|
|
141
|
+
return {k: given(x) for k, x in v.items()}
|
|
142
|
+
return v
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Parsing
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
#: Units plot_io resolves inside math functions besides the CSS ones.
|
|
150
|
+
_MATH_UNITS = ("data", "fr", *DURATIONS)
|
|
151
|
+
_FUNCTION = re.compile(r"([A-Za-z][\w-]*)\s*\(")
|
|
152
|
+
_NAME = re.compile(r"\s*(--[\w-]+|[A-Za-z_][\w-]*)\s*")
|
|
153
|
+
_SCAN = re.compile(r"""
|
|
154
|
+
(?P<var>\bvar\s*\(\s*(?P<vname>--[\w-]+|[A-Za-z_][\w-]*))
|
|
155
|
+
| (?P<name>-*[A-Za-z_][\w-]*)
|
|
156
|
+
| (?P<num>(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)(?P<unit>[A-Za-z]+|%)?
|
|
157
|
+
""", re.X)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _parse_var(s: str, start: int) -> Var:
|
|
161
|
+
"""``var(name[, fallback])``, ``s[start:]`` following its ``(``."""
|
|
162
|
+
m = _NAME.match(s, start)
|
|
163
|
+
if m is None:
|
|
164
|
+
raise ValueError(f"{s}: expected a setting name at {start}")
|
|
165
|
+
name, pos, fallback = m.group(1), m.end(), None
|
|
166
|
+
if s.startswith(",", pos):
|
|
167
|
+
depth, i = 0, pos + 1
|
|
168
|
+
while i < len(s):
|
|
169
|
+
if s[i] == "(":
|
|
170
|
+
depth += 1
|
|
171
|
+
elif s[i] == ")":
|
|
172
|
+
if depth == 0:
|
|
173
|
+
break
|
|
174
|
+
depth -= 1
|
|
175
|
+
i += 1
|
|
176
|
+
fallback = s[pos + 1:i].strip()
|
|
177
|
+
if not fallback:
|
|
178
|
+
raise ValueError(f"{s}: expected a fallback at {i}")
|
|
179
|
+
pos = i
|
|
180
|
+
if not s.startswith(")", pos):
|
|
181
|
+
raise ValueError(f"{s}: expected ')' at {pos}")
|
|
182
|
+
if s[pos + 1:].strip():
|
|
183
|
+
raise ValueError(f"{s}: unexpected text after var() at {pos + 1}")
|
|
184
|
+
return Var(s, name, fallback)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
#: The longest math function text, and its deepest nesting of parentheses.
|
|
188
|
+
MAX_MATH_LENGTH, MAX_MATH_DEPTH = 1000, 32
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_math(s: str, units: frozenset[str] | None) -> Calc:
|
|
192
|
+
"""A math function; ``units``: the units allowed in it (``None``: all)."""
|
|
193
|
+
if len(s) > MAX_MATH_LENGTH:
|
|
194
|
+
raise ValueError(f"a math function of at most {MAX_MATH_LENGTH} characters, "
|
|
195
|
+
f"got {len(s)}")
|
|
196
|
+
depth = 0
|
|
197
|
+
for i, c in enumerate(s):
|
|
198
|
+
depth += (c == "(") - (c == ")")
|
|
199
|
+
if depth > MAX_MATH_DEPTH:
|
|
200
|
+
raise ValueError(f"{s[:20]}…: parentheses nested deeper than {MAX_MATH_DEPTH} "
|
|
201
|
+
f"at {i + 1}")
|
|
202
|
+
math = parse_math(s, units=_MATH_UNITS)
|
|
203
|
+
refs: list[Value] = []
|
|
204
|
+
for m in _SCAN.finditer(s):
|
|
205
|
+
if m["var"]:
|
|
206
|
+
refs.append(Var(f"var({m['vname']})", m["vname"], None))
|
|
207
|
+
elif m["name"]:
|
|
208
|
+
if m["name"].lower() == "inherit":
|
|
209
|
+
refs.append(Var("inherit", "inherit", None))
|
|
210
|
+
elif m["unit"]:
|
|
211
|
+
unit = m["unit"].lower()
|
|
212
|
+
if units is not None and unit not in units:
|
|
213
|
+
raise ValueError(f"{s}: unit {unit!r} not allowed here at {m.start()}")
|
|
214
|
+
refs.append(Dim(m.group(), float(m["num"]), unit))
|
|
215
|
+
return Calc(s, math, tuple(refs))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse(text: str, units: frozenset[str] | None = None, calc: bool = True) -> Value | None:
|
|
219
|
+
"""``var(…)`` or ``inherit`` (the parent's value of the same setting), or, when
|
|
220
|
+
``calc``, a CSS math function (``calc()``, ``min()``, …; vec_io's grammar), as a
|
|
221
|
+
:class:`Value`; ``None`` for other text. ``units``: the units allowed in a math
|
|
222
|
+
function. Raises ``ValueError`` naming the position of an error."""
|
|
223
|
+
s = text.strip()
|
|
224
|
+
if s.lower() == "inherit":
|
|
225
|
+
return Var(s, "inherit", None)
|
|
226
|
+
m = _FUNCTION.match(s)
|
|
227
|
+
if m is None:
|
|
228
|
+
return None
|
|
229
|
+
if m.group(1).lower() == "var":
|
|
230
|
+
return _parse_var(s, m.end())
|
|
231
|
+
return _parse_math(s, units) if calc else None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def refers_inherit(v: Any) -> bool:
|
|
236
|
+
"""Whether a stored value refers to ``inherit``."""
|
|
237
|
+
if isinstance(v, Var):
|
|
238
|
+
return v.name == "inherit"
|
|
239
|
+
if isinstance(v, Calc):
|
|
240
|
+
return "inherit" in v.math.variables
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# What % is of
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
#: What ``%`` of a length is of (``length(percent=…)``), besides another setting of the
|
|
250
|
+
#: object: ``{key: (meaning, how)}``. ``how``: ``"em"`` (the font size ``em`` is of),
|
|
251
|
+
#: ``"box"`` (a layout box, known while the figure is laid out) or ``"given"`` (known
|
|
252
|
+
#: where the setting is used, which passes it as ``of=``).
|
|
253
|
+
REFERENTS: dict[str, tuple[str, str]] = {
|
|
254
|
+
"em": ("the font size", "em"),
|
|
255
|
+
"axes_width": ("the axes width", "box"),
|
|
256
|
+
"axes_height": ("the axes height", "box"),
|
|
257
|
+
"across": ("the axes side across the axis", "box"),
|
|
258
|
+
"along": ("the axes side along the axis", "box"),
|
|
259
|
+
"axes": ("the axes width (across) or height (down)", "given"),
|
|
260
|
+
"axes_length": ("the axes length along the axis", "given"),
|
|
261
|
+
"data_range": ("the axis range", "given"),
|
|
262
|
+
"bar_across": ("the axes side across the colorbar", "given"),
|
|
263
|
+
"bar_thickness": ("the span along a key, the axes side across a bar", "given"),
|
|
264
|
+
"bar_along": ("the span along the colorbar (the axes side of an attached one)", "given"),
|
|
265
|
+
"figure": ("the figure width (across) or height (down)", "given"),
|
|
266
|
+
"room": ("the plots' room: the figure less its margins, spacing and lanes (and the "
|
|
267
|
+
"plots' margins with `align=\"axes\"`)", "given"),
|
|
268
|
+
"slot": ("the slot: one category, or the smallest gap between positions", "given"),
|
|
269
|
+
"slot_part": ("each series' part of a shared slot", "given"),
|
|
270
|
+
"box_width": ("the box width", "given"),
|
|
271
|
+
"radius": ("the radius", "given"),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def percent_meaning(ref: Any) -> str | None:
|
|
276
|
+
"""What ``%`` of a length with ``percent=ref`` is of, in words."""
|
|
277
|
+
if ref is None:
|
|
278
|
+
return None
|
|
279
|
+
if ref in REFERENTS:
|
|
280
|
+
return REFERENTS[ref][0]
|
|
281
|
+
return f"`{ref}`"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Resolution
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
_local = threading.local()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _stack() -> list[tuple[int, str, str]]:
|
|
292
|
+
s = getattr(_local, "stack", None)
|
|
293
|
+
if s is None:
|
|
294
|
+
s = _local.stack = []
|
|
295
|
+
return s
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class guard:
|
|
299
|
+
"""Detects reference loops while ``obj.name`` resolves."""
|
|
300
|
+
|
|
301
|
+
__slots__ = ("key",)
|
|
302
|
+
|
|
303
|
+
def __init__(self, obj: Any, name: str) -> None:
|
|
304
|
+
self.key = (id(obj), name, f"{obj.kind}.{name}")
|
|
305
|
+
|
|
306
|
+
def __enter__(self) -> None:
|
|
307
|
+
st = _stack()
|
|
308
|
+
if any(k[:2] == self.key[:2] for k in st):
|
|
309
|
+
chain = [k[2] for k in st[next(i for i, k in enumerate(st)
|
|
310
|
+
if k[:2] == self.key[:2]):]]
|
|
311
|
+
st.clear()
|
|
312
|
+
raise ValueError("reference loop: " + " → ".join(chain + [self.key[2]]))
|
|
313
|
+
st.append(self.key)
|
|
314
|
+
|
|
315
|
+
def __exit__(self, *exc: Any) -> None:
|
|
316
|
+
st = _stack()
|
|
317
|
+
if st and st[-1] == self.key:
|
|
318
|
+
st.pop()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class Expr(str):
|
|
322
|
+
"""A setting value written with ``var()`` or ``calc()`` (made by :func:`var` and
|
|
323
|
+
:func:`calc`): the type of such values in the typed API, where a plain string is
|
|
324
|
+
flagged for a number, a boolean or a choice."""
|
|
325
|
+
|
|
326
|
+
__slots__ = ()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def var(name: str, fallback: Any = None) -> Expr:
|
|
330
|
+
"""``var(name)``, or ``var(name, fallback)``: the value of setting ``name`` (or custom
|
|
331
|
+
variable ``--name``) of the object or its parents."""
|
|
332
|
+
return Expr(f"var({name})" if fallback is None else f"var({name}, {fallback})")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def calc(expression: Any) -> Expr:
|
|
336
|
+
"""``calc(expression)``: CSS math of numbers, lengths, angles, ``var()`` and
|
|
337
|
+
``inherit``."""
|
|
338
|
+
return Expr(f"calc({expression})")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class SettingError(ValueError):
|
|
342
|
+
"""A value a setting does not take, found when it is set or resolved: ``owner``, the
|
|
343
|
+
object whose setting it is; ``name``, the setting; ``value``, the value as given."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, message: str, owner: Any = None, name: str | None = None,
|
|
346
|
+
value: Any = None) -> None:
|
|
347
|
+
super().__init__(message)
|
|
348
|
+
self.owner = owner
|
|
349
|
+
self.name = name
|
|
350
|
+
self.value = value
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class NeedsContext(SettingError):
|
|
354
|
+
"""A value that needs what only its use provides: the reference of ``%``, a layout
|
|
355
|
+
box, an axis scale (it stays unresolved in a render's spec)."""
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class NeedsScale(NeedsContext):
|
|
359
|
+
"""A length in px resolved in data units before the axis scale is known."""
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class Context:
|
|
363
|
+
"""What one resolution needs besides the object: ``of`` (the reference of ``%``),
|
|
364
|
+
``scale`` (px per data unit), ``time`` (durations accepted), ``unit`` (``"px"`` or
|
|
365
|
+
``"data"``: the unit of the result)."""
|
|
366
|
+
|
|
367
|
+
__slots__ = ("obj", "name", "check", "of", "scale", "time", "unit", "raw")
|
|
368
|
+
|
|
369
|
+
def __init__(self, obj: Any, name: str, check: "Validator", of: Any, scale: Any,
|
|
370
|
+
time: bool, unit: str, raw: Any = None) -> None:
|
|
371
|
+
self.obj, self.name, self.check = obj, name, check
|
|
372
|
+
self.of, self.scale, self.time, self.unit = of, scale, time, unit
|
|
373
|
+
self.raw = raw
|
|
374
|
+
|
|
375
|
+
def error(self, msg: str, context: bool = False) -> SettingError:
|
|
376
|
+
cls = NeedsContext if context else SettingError
|
|
377
|
+
return cls(f"{self.obj.kind} setting {self.name!r}: {msg}", self.obj, self.name,
|
|
378
|
+
given(self.raw))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _scaling(ctx: Context) -> float:
|
|
382
|
+
if not ctx.check.scaled:
|
|
383
|
+
return 1.0
|
|
384
|
+
return ctx.obj._scaling()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _px_to_unit(px: float, ctx: Context) -> float:
|
|
388
|
+
if ctx.unit == "data":
|
|
389
|
+
if not ctx.scale:
|
|
390
|
+
raise NeedsScale(f"{ctx.obj.kind} setting {ctx.name!r}: a length in px needs "
|
|
391
|
+
f"the axis scale here")
|
|
392
|
+
return px / ctx.scale
|
|
393
|
+
return px
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _data_to_unit(d: float, ctx: Context) -> float:
|
|
397
|
+
if ctx.unit == "data":
|
|
398
|
+
return d
|
|
399
|
+
if ctx.scale is None:
|
|
400
|
+
raise ctx.error("data units are only valid along a linear or category axis", True)
|
|
401
|
+
return d * ctx.scale
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _percent_ref(ctx: Context) -> float:
|
|
405
|
+
if ctx.of is not None:
|
|
406
|
+
return float(ctx.of)
|
|
407
|
+
ref = ctx.check.percent
|
|
408
|
+
if ref is None:
|
|
409
|
+
raise ctx.error("% is not accepted")
|
|
410
|
+
if ref in REFERENTS and REFERENTS[ref][1] == "given":
|
|
411
|
+
raise ctx.error("% needs a reference here", True)
|
|
412
|
+
if ref == "em":
|
|
413
|
+
return _px_to_unit(_em(ctx), ctx)
|
|
414
|
+
if ref in REFERENTS and REFERENTS[ref][1] == "box":
|
|
415
|
+
return _px_to_unit(ctx.obj._box(ref, ctx.name), ctx)
|
|
416
|
+
return ctx.obj.resolve(ref)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _em(ctx: Context) -> float:
|
|
420
|
+
obj = ctx.obj
|
|
421
|
+
basis = ctx.check.em
|
|
422
|
+
if basis is not None:
|
|
423
|
+
return obj.resolve(basis)
|
|
424
|
+
if ctx.name == "font_size":
|
|
425
|
+
return obj._parent_font_size()
|
|
426
|
+
return obj._font_size()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _dim(d: Dim, ctx: Context) -> float:
|
|
430
|
+
n, u = d.number, d.unit
|
|
431
|
+
if n is None: # font keyword
|
|
432
|
+
if u in _RELATIVE_FONT:
|
|
433
|
+
return _px_to_unit(_em(ctx) * _RELATIVE_FONT[u], ctx)
|
|
434
|
+
return _px_to_unit(ctx.obj._root_font_size() * FONT_KEYWORDS[u], ctx)
|
|
435
|
+
if u in ABSOLUTE:
|
|
436
|
+
return _px_to_unit(n * PX_PER_UNIT[u] * _scaling(ctx), ctx)
|
|
437
|
+
if u == "em":
|
|
438
|
+
return _px_to_unit(n * _em(ctx), ctx)
|
|
439
|
+
if u == "rem":
|
|
440
|
+
return _px_to_unit(n * ctx.obj._root_font_size(), ctx)
|
|
441
|
+
if u == "%":
|
|
442
|
+
return n / 100.0 * _percent_ref(ctx)
|
|
443
|
+
if u == "data":
|
|
444
|
+
return _data_to_unit(n, ctx)
|
|
445
|
+
if u == "d" and not ctx.time: # data units (a day on a time axis)
|
|
446
|
+
return _data_to_unit(n, ctx)
|
|
447
|
+
if u in DURATIONS:
|
|
448
|
+
if not ctx.time:
|
|
449
|
+
raise ctx.error(f"a duration ({d.source!r}) needs a time axis", True)
|
|
450
|
+
return _data_to_unit(n * DURATIONS[u], ctx)
|
|
451
|
+
if u in ANGLE_UNITS:
|
|
452
|
+
return n * ANGLE_UNITS[u]
|
|
453
|
+
if u == "fr":
|
|
454
|
+
raise ctx.error("fr units are only valid in figure widths and heights")
|
|
455
|
+
raise ctx.error(f"unknown unit {u!r}")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _bare(n: float, ctx: Context) -> float:
|
|
459
|
+
if ctx.check.kind != "length":
|
|
460
|
+
return n
|
|
461
|
+
if ctx.check.bare == "data":
|
|
462
|
+
return _data_to_unit(n, ctx)
|
|
463
|
+
return _px_to_unit(n * _scaling(ctx), ctx)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _var(v: Var, ctx: Context, checked: bool = True) -> Any:
|
|
467
|
+
"""The value ``v`` refers to: as a value of this setting (``checked``), else as the
|
|
468
|
+
referred setting has it (a ``calc()`` operand)."""
|
|
469
|
+
obj = ctx.obj
|
|
470
|
+
if v.name.startswith("--"):
|
|
471
|
+
raw = obj._custom_var(v.name)
|
|
472
|
+
if raw is None:
|
|
473
|
+
if v.fallback is None:
|
|
474
|
+
raise ctx.error(f"unknown variable {v.name!r}")
|
|
475
|
+
raw = v.fallback
|
|
476
|
+
with guard(obj, v.name):
|
|
477
|
+
return _from_text(raw, ctx)
|
|
478
|
+
if v.name == ctx.name or v.name == "inherit":
|
|
479
|
+
if not obj._inherits(ctx.name):
|
|
480
|
+
if v.fallback is None:
|
|
481
|
+
raise ctx.error(f"{v.source}: no parent of {obj.kind} and no style has "
|
|
482
|
+
f"{ctx.name!r}")
|
|
483
|
+
return _from_text(v.fallback, ctx)
|
|
484
|
+
out = obj._parent_value(ctx.name)
|
|
485
|
+
target = obj._check_of(ctx.name, own=False)
|
|
486
|
+
else:
|
|
487
|
+
out = obj._resolve_any(v.name)
|
|
488
|
+
target = obj._check_of(v.name)
|
|
489
|
+
if out is None:
|
|
490
|
+
return None if v.fallback is None else _from_text(v.fallback, ctx)
|
|
491
|
+
return _take(v, out, target, ctx) if checked else out
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _take(v: Var, out: Any, target: "Validator | None", ctx: Context) -> Any:
|
|
495
|
+
"""``out``, the final value of the setting ``v`` refers to (checked by ``target``;
|
|
496
|
+
``None``: a style's bare value), as a value of this setting."""
|
|
497
|
+
check = ctx.check
|
|
498
|
+
ok = None if target is None else check.accepts(target)
|
|
499
|
+
if ok:
|
|
500
|
+
return out if target is check else check.finish(out, ctx)
|
|
501
|
+
if ok is False:
|
|
502
|
+
assert target is not None
|
|
503
|
+
raise ctx.error(f"{v.source} is {target.noun}, expected {check.noun}")
|
|
504
|
+
try:
|
|
505
|
+
raw = check(out)
|
|
506
|
+
except (TypeError, ValueError) as e:
|
|
507
|
+
raise ctx.error(f"{v.source} is {out!r}: {e}") from None
|
|
508
|
+
return resolve_value(raw, ctx)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _from_text(raw: Any, ctx: Context) -> Any:
|
|
512
|
+
"""A custom variable or fallback, checked and resolved as a value of this setting."""
|
|
513
|
+
try:
|
|
514
|
+
raw = ctx.check(raw)
|
|
515
|
+
except (TypeError, ValueError) as e:
|
|
516
|
+
raise ctx.error(str(e)) from None
|
|
517
|
+
return resolve_value(raw, ctx)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _finite(x: float, ctx: Context) -> float:
|
|
521
|
+
if x - x != 0: # inf or NaN
|
|
522
|
+
raise ctx.error(f"{given(ctx.raw)} gives {x} (not finite)")
|
|
523
|
+
return x
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
#: The unit of a ``var()`` value inside a math function: the setting's own unit.
|
|
527
|
+
_FINAL = "="
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _calc(c: "Calc", ctx: Context) -> tuple[float, bool]:
|
|
531
|
+
"""Value of a math function and whether it is a dimension (length or angle)."""
|
|
532
|
+
per: dict[str, float] = {}
|
|
533
|
+
|
|
534
|
+
def final(unit: str) -> float:
|
|
535
|
+
"""One ``unit`` in the setting's unit."""
|
|
536
|
+
if unit not in per:
|
|
537
|
+
per[unit] = 1.0 if unit == _FINAL else _dim(Dim(f"1{unit}", 1.0, unit), ctx)
|
|
538
|
+
return per[unit]
|
|
539
|
+
|
|
540
|
+
def px(unit: str) -> float:
|
|
541
|
+
return final(unit) / final("px")
|
|
542
|
+
|
|
543
|
+
def var(name: str, fallback: str | None) -> Any:
|
|
544
|
+
v = Var(name if name == "inherit" and fallback is None else
|
|
545
|
+
f"var({name})" if fallback is None else f"var({name}, {fallback})",
|
|
546
|
+
name, fallback)
|
|
547
|
+
out = _var(v, ctx, checked=False)
|
|
548
|
+
if isinstance(out, bool) or not isinstance(out, (int, float)):
|
|
549
|
+
raise ctx.error(f"{v.source} is not a number ({out!r})")
|
|
550
|
+
if name.startswith("--") or name in (ctx.name, "inherit"):
|
|
551
|
+
kind = ctx.check.kind
|
|
552
|
+
else:
|
|
553
|
+
target = ctx.obj._check_of(name)
|
|
554
|
+
kind = None if target is None else target.kind
|
|
555
|
+
x = _finite(float(out), ctx)
|
|
556
|
+
return {"deg": x} if kind == "angle" else {_FINAL: x} if kind == "length" else x
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
value = c.math.evaluate(px, var)
|
|
560
|
+
except SettingError:
|
|
561
|
+
raise
|
|
562
|
+
except (ValueError, ZeroDivisionError) as e:
|
|
563
|
+
raise ctx.error(str(e)) from None
|
|
564
|
+
for x in value.values():
|
|
565
|
+
_finite(x, ctx)
|
|
566
|
+
if "" in value:
|
|
567
|
+
if len(value) > 1:
|
|
568
|
+
raise ctx.error(f"{c.source} adds a dimension and a plain number")
|
|
569
|
+
return value[""], False
|
|
570
|
+
return _finite(sum(x * final(u) for u, x in value.items()), ctx), True
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def resolve_value(raw: Any, ctx: Context) -> Any:
|
|
574
|
+
"""The final value of ``raw`` (a stored value of ``ctx.name``)."""
|
|
575
|
+
check = ctx.check
|
|
576
|
+
kind = check.kind
|
|
577
|
+
if isinstance(raw, Fixed):
|
|
578
|
+
return raw.in_context(ctx.obj)
|
|
579
|
+
if isinstance(raw, Var):
|
|
580
|
+
return _var(raw, ctx)
|
|
581
|
+
if isinstance(raw, Calc):
|
|
582
|
+
x, is_dim = _calc(raw, ctx)
|
|
583
|
+
if kind == "length" and not is_dim:
|
|
584
|
+
x = _bare(x, ctx)
|
|
585
|
+
return check.finish(x, ctx)
|
|
586
|
+
if isinstance(raw, Dim):
|
|
587
|
+
return check.finish(_dim(raw, ctx), ctx)
|
|
588
|
+
if kind is not None and isinstance(raw, (int, float)) and not isinstance(raw, bool):
|
|
589
|
+
return _bare(float(raw), ctx)
|
|
590
|
+
inner = check.inner
|
|
591
|
+
if inner is not None and isinstance(raw, (tuple, list)):
|
|
592
|
+
sub = Context(ctx.obj, ctx.name, inner, ctx.of, ctx.scale, ctx.time, ctx.unit,
|
|
593
|
+
ctx.raw)
|
|
594
|
+
out = [resolve_value(x, sub) for x in raw]
|
|
595
|
+
return tuple(out) if isinstance(raw, tuple) else out
|
|
596
|
+
if _has_fixed(raw):
|
|
597
|
+
return _fixed_final(raw, ctx.obj)
|
|
598
|
+
return raw
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def resolve_raw(obj: Any, name: str, check: "Validator", raw: Any, of: Any = None,
|
|
602
|
+
scale: Any = None, time: bool = False, unit: str = "px") -> Any:
|
|
603
|
+
"""The final value of ``raw``, a stored value of setting ``name`` of ``obj``."""
|
|
604
|
+
return resolve_value(raw, Context(obj, name, check, of, scale, time, unit, raw))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def may_refer(raw: Any) -> bool:
|
|
608
|
+
"""Whether resolving ``raw`` may read other settings (and so loop back): a ``var()``, a
|
|
609
|
+
``calc()``, ``%`` or a relative unit."""
|
|
610
|
+
if isinstance(raw, Dim):
|
|
611
|
+
return raw.number is None or raw.unit in ("%", "em", "rem")
|
|
612
|
+
return isinstance(raw, (Var, Calc)) or isinstance(raw, (tuple, list))
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def needs_resolving(raw: Any, check: "Validator") -> bool:
|
|
616
|
+
"""Whether ``raw`` differs from its final value (fast path for plain values)."""
|
|
617
|
+
if isinstance(raw, Value):
|
|
618
|
+
return True
|
|
619
|
+
if check.kind == "length" and isinstance(raw, (int, float)):
|
|
620
|
+
return True
|
|
621
|
+
if check.inner is not None and isinstance(raw, (tuple, list)):
|
|
622
|
+
return True
|
|
623
|
+
return _has_fixed(raw)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# ---------------------------------------------------------------------------
|
|
627
|
+
# Explaining a value
|
|
628
|
+
# ---------------------------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
def _refs(raw: Any, out: list[Any]) -> list[Any]:
|
|
631
|
+
"""The ``var()`` names and dimensions a stored value refers to."""
|
|
632
|
+
if isinstance(raw, (Var, Dim)):
|
|
633
|
+
out.append(raw)
|
|
634
|
+
elif isinstance(raw, Calc):
|
|
635
|
+
out.extend(raw.refs)
|
|
636
|
+
elif isinstance(raw, (tuple, list)):
|
|
637
|
+
for x in raw:
|
|
638
|
+
_refs(x, out)
|
|
639
|
+
return out
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def explain_tree(obj: Any, name: str, as_dict: bool) -> Any:
|
|
643
|
+
"""See :meth:`Settings.explain`."""
|
|
644
|
+
tree = _explain(obj, name, 0, set())
|
|
645
|
+
return tree if as_dict else "\n".join(_lines(tree, 0))
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _final(obj: Any, name: str) -> Any:
|
|
649
|
+
try:
|
|
650
|
+
return obj.resolve(name)
|
|
651
|
+
except ValueError as e:
|
|
652
|
+
return f"<{e}>"
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _explain(obj: Any, name: str, depth: int, seen: set[Any]) -> dict[str, Any]:
|
|
656
|
+
source, owner = obj._source(name)
|
|
657
|
+
raw = owner._raw(name)
|
|
658
|
+
c = owner.SETTINGS[name].check if name in owner.SETTINGS else None
|
|
659
|
+
kind = None if c is None else c.kind or (c.inner.kind if c.inner is not None else None)
|
|
660
|
+
node: dict[str, Any] = {"name": f"{obj.kind}.{name}", "value": _final(obj, name),
|
|
661
|
+
"given": given(raw), "source": source, "refers": [],
|
|
662
|
+
"unit": {"length": "px", "angle": "°"}.get(kind or "", "")}
|
|
663
|
+
variant = _variant_used(obj, name, raw)
|
|
664
|
+
if variant is not None:
|
|
665
|
+
node["variant"] = variant
|
|
666
|
+
key = (id(owner), name)
|
|
667
|
+
if key in seen or depth > 8:
|
|
668
|
+
return node
|
|
669
|
+
seen = seen | {key}
|
|
670
|
+
check = None if c is None else c.inner or c # pairs, dash lists: their items
|
|
671
|
+
refs, keys = [], set()
|
|
672
|
+
for r in _refs(raw, []): # each reference once
|
|
673
|
+
k = (type(r).__name__, r.name if isinstance(r, Var) else None,
|
|
674
|
+
r.unit if isinstance(r, Dim) else None)
|
|
675
|
+
if k not in keys:
|
|
676
|
+
keys.add(k)
|
|
677
|
+
refs.append(r)
|
|
678
|
+
for r in refs:
|
|
679
|
+
if isinstance(r, Var):
|
|
680
|
+
if r.name.startswith("--"):
|
|
681
|
+
node["refers"].append({"name": r.name, "value": owner._custom_var(r.name),
|
|
682
|
+
"given": owner._custom_var(r.name),
|
|
683
|
+
"source": "custom variable", "refers": []})
|
|
684
|
+
continue
|
|
685
|
+
target = owner if r.name not in (name, "inherit") else None
|
|
686
|
+
if target is None:
|
|
687
|
+
parent = next((p for p in owner._ancestors() if name in p.SETTINGS), None)
|
|
688
|
+
if parent is not None:
|
|
689
|
+
node["refers"].append(_explain(parent, name, depth + 1, seen))
|
|
690
|
+
continue
|
|
691
|
+
holder = next((o for o in [owner] + owner._ancestors() if r.name in o.SETTINGS),
|
|
692
|
+
None)
|
|
693
|
+
if holder is not None:
|
|
694
|
+
node["refers"].append(_explain(holder, r.name, depth + 1, seen))
|
|
695
|
+
elif isinstance(r, Dim):
|
|
696
|
+
u = r.unit
|
|
697
|
+
if u == "%":
|
|
698
|
+
ref = None if check is None else check.percent
|
|
699
|
+
if isinstance(ref, str) and ref in owner.SETTINGS:
|
|
700
|
+
node["refers"].append(_explain(owner, ref, depth + 1, seen))
|
|
701
|
+
elif ref == "em" or (ref is None and name == "font_size"):
|
|
702
|
+
node["refers"].append(_font_node(owner, name, depth, seen, check))
|
|
703
|
+
else:
|
|
704
|
+
node["refers"].append({"name": f"% of {ref}", "value": None,
|
|
705
|
+
"given": None, "source": "layout", "refers": []})
|
|
706
|
+
elif u in ("em", "smaller", "larger") or (r.number is None and u in _RELATIVE_FONT):
|
|
707
|
+
node["refers"].append(_font_node(owner, name, depth, seen, check))
|
|
708
|
+
elif u == "rem" or (r.number is None):
|
|
709
|
+
root = owner._root()
|
|
710
|
+
if "font_size" in root.SETTINGS:
|
|
711
|
+
node["refers"].append(_explain(root, "font_size", depth + 1, seen))
|
|
712
|
+
elif u in ABSOLUTE and (check is None or check.scaled):
|
|
713
|
+
k = owner._scaling()
|
|
714
|
+
if k != 1.0:
|
|
715
|
+
node["refers"].append({"name": "scaling", "value": k, "given": k,
|
|
716
|
+
"source": "Figure.scaling", "refers": []})
|
|
717
|
+
return node
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _font_node(obj: Any, name: str, depth: int, seen: set[Any],
|
|
721
|
+
check: "Validator | None" = None) -> dict[str, Any]:
|
|
722
|
+
basis = None if check is None else check.em
|
|
723
|
+
if basis is not None:
|
|
724
|
+
return _explain(obj, basis, depth + 1, seen)
|
|
725
|
+
holders = obj._ancestors() if name == "font_size" else [obj] + obj._ancestors()
|
|
726
|
+
holder = next((o for o in holders if "font_size" in o.SETTINGS), None)
|
|
727
|
+
if holder is None:
|
|
728
|
+
return {"name": "font_size", "value": obj._root_font_size(), "given": None,
|
|
729
|
+
"source": "theme", "refers": []}
|
|
730
|
+
return _explain(holder, "font_size", depth + 1, seen)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _show(v: Any, unit: str = "") -> str:
|
|
734
|
+
if isinstance(v, float):
|
|
735
|
+
return f"{v:.6g}{unit}"
|
|
736
|
+
if isinstance(v, tuple) and all(isinstance(x, float) for x in v):
|
|
737
|
+
return "(" + ", ".join(f"{x:.6g}{unit}" for x in v) + ")"
|
|
738
|
+
return repr(v)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _variant_used(obj: Any, name: str, raw: Any) -> str | None:
|
|
742
|
+
"""The color variant a setting's named colors or color maps are resolved in, when not
|
|
743
|
+
base."""
|
|
744
|
+
from .settings import _variable_default
|
|
745
|
+
s = obj.SETTINGS.get(name)
|
|
746
|
+
items = raw.values() if isinstance(raw, dict) else raw if isinstance(raw, (list, tuple)) \
|
|
747
|
+
else (raw,)
|
|
748
|
+
if s is None or not (any(isinstance(x, Fixed) and x.varies for x in items)
|
|
749
|
+
or isinstance(raw, str) and _variable_default(s, raw)):
|
|
750
|
+
return None
|
|
751
|
+
from .color.values import variant_of
|
|
752
|
+
v = variant_of(obj)
|
|
753
|
+
return None if v == "base" else v
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _lines(node: dict[str, Any], depth: int) -> list[str]:
|
|
757
|
+
pad = " " * depth
|
|
758
|
+
given_ = node["given"]
|
|
759
|
+
head = f"{pad}{node['name']} = {_show(node['value'], node.get('unit', ''))}"
|
|
760
|
+
if given_ is not None and given_ != node["value"]:
|
|
761
|
+
head += f" ← {_show(given_)}"
|
|
762
|
+
variant = node.get("variant")
|
|
763
|
+
head += f" ({node['source']}; color_variant {variant})" if variant \
|
|
764
|
+
else f" ({node['source']})"
|
|
765
|
+
out = [head]
|
|
766
|
+
for r in node["refers"]:
|
|
767
|
+
out.extend(_lines(r, depth + 1))
|
|
768
|
+
return out
|