figrecipe 0.5.0__py3-none-any.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.
- figrecipe/__init__.py +1090 -0
- figrecipe/_recorder.py +435 -0
- figrecipe/_reproducer.py +358 -0
- figrecipe/_seaborn.py +305 -0
- figrecipe/_serializer.py +227 -0
- figrecipe/_signatures/__init__.py +7 -0
- figrecipe/_signatures/_loader.py +186 -0
- figrecipe/_utils/__init__.py +32 -0
- figrecipe/_utils/_crop.py +261 -0
- figrecipe/_utils/_diff.py +98 -0
- figrecipe/_utils/_image_diff.py +204 -0
- figrecipe/_utils/_numpy_io.py +204 -0
- figrecipe/_utils/_units.py +200 -0
- figrecipe/_validator.py +186 -0
- figrecipe/_wrappers/__init__.py +8 -0
- figrecipe/_wrappers/_axes.py +327 -0
- figrecipe/_wrappers/_figure.py +227 -0
- figrecipe/plt.py +12 -0
- figrecipe/pyplot.py +264 -0
- figrecipe/styles/__init__.py +50 -0
- figrecipe/styles/_style_applier.py +412 -0
- figrecipe/styles/_style_loader.py +450 -0
- figrecipe-0.5.0.dist-info/METADATA +336 -0
- figrecipe-0.5.0.dist-info/RECORD +26 -0
- figrecipe-0.5.0.dist-info/WHEEL +4 -0
- figrecipe-0.5.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Style loader for figrecipe.
|
|
4
|
+
|
|
5
|
+
Loads style configuration from YAML file and provides centralized access
|
|
6
|
+
to all style parameters.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from figrecipe.styles import load_style, get_style, STYLE
|
|
10
|
+
|
|
11
|
+
# Load default style
|
|
12
|
+
style = load_style()
|
|
13
|
+
fig, ax = ps.subplots(**style.to_subplots_kwargs())
|
|
14
|
+
|
|
15
|
+
# Access individual style parameters
|
|
16
|
+
line_width = STYLE.lines.trace_mm
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"load_style",
|
|
21
|
+
"unload_style",
|
|
22
|
+
"get_style",
|
|
23
|
+
"reload_style",
|
|
24
|
+
"list_presets",
|
|
25
|
+
"STYLE",
|
|
26
|
+
"to_subplots_kwargs",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, List, Optional, Union
|
|
31
|
+
|
|
32
|
+
from ruamel.yaml import YAML
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Path to presets directory
|
|
36
|
+
_PRESETS_DIR = Path(__file__).parent / "presets"
|
|
37
|
+
|
|
38
|
+
# Path to default style file (for backwards compatibility)
|
|
39
|
+
_DEFAULT_STYLE_PATH = Path(__file__).parent / "FIGRECIPE_STYLE.yaml"
|
|
40
|
+
|
|
41
|
+
# Preset aliases (for branding - FigRecipe is part of SciTeX ecosystem)
|
|
42
|
+
_PRESET_ALIASES = {
|
|
43
|
+
"FIGRECIPE": "SCITEX",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Global style cache
|
|
47
|
+
_STYLE_CACHE: Optional["DotDict"] = None
|
|
48
|
+
_CURRENT_STYLE_NAME: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def list_presets() -> List[str]:
|
|
52
|
+
"""List available style presets.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list of str
|
|
57
|
+
Names of available presets.
|
|
58
|
+
Use `dark=True` parameter for dark variants.
|
|
59
|
+
|
|
60
|
+
Examples
|
|
61
|
+
--------
|
|
62
|
+
>>> fr.list_presets()
|
|
63
|
+
['MATPLOTLIB', 'SCITEX']
|
|
64
|
+
|
|
65
|
+
>>> fr.load_style("SCITEX") # Scientific publication style
|
|
66
|
+
>>> fr.load_style("SCITEX", dark=True) # Dark variant
|
|
67
|
+
>>> fr.load_style("MATPLOTLIB") # Vanilla matplotlib
|
|
68
|
+
"""
|
|
69
|
+
# Show only user-facing presets (not internal file names)
|
|
70
|
+
return ["MATPLOTLIB", "SCITEX"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DotDict(dict):
|
|
74
|
+
"""Dictionary with dot-notation access to nested keys.
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
>>> d = DotDict({"axes": {"width_mm": 40}})
|
|
79
|
+
>>> d.axes.width_mm
|
|
80
|
+
40
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, key: str) -> Any:
|
|
84
|
+
# Handle special methods first
|
|
85
|
+
if key == 'to_subplots_kwargs':
|
|
86
|
+
return lambda: to_subplots_kwargs(self)
|
|
87
|
+
try:
|
|
88
|
+
value = self[key]
|
|
89
|
+
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
90
|
+
value = DotDict(value)
|
|
91
|
+
self[key] = value
|
|
92
|
+
return value
|
|
93
|
+
except KeyError:
|
|
94
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
95
|
+
|
|
96
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
97
|
+
self[key] = value
|
|
98
|
+
|
|
99
|
+
def __delattr__(self, key: str) -> None:
|
|
100
|
+
try:
|
|
101
|
+
del self[key]
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
104
|
+
|
|
105
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
106
|
+
"""Get value with default, supporting nested keys with dots."""
|
|
107
|
+
if "." in key:
|
|
108
|
+
parts = key.split(".")
|
|
109
|
+
value = self
|
|
110
|
+
for part in parts:
|
|
111
|
+
if isinstance(value, dict) and part in value:
|
|
112
|
+
value = value[part]
|
|
113
|
+
else:
|
|
114
|
+
return default
|
|
115
|
+
return value
|
|
116
|
+
return super().get(key, default)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _deep_merge(base: Dict, override: Dict) -> Dict:
|
|
120
|
+
"""Deep merge two dictionaries, with override taking precedence."""
|
|
121
|
+
result = base.copy()
|
|
122
|
+
for key, value in override.items():
|
|
123
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
124
|
+
result[key] = _deep_merge(result[key], value)
|
|
125
|
+
else:
|
|
126
|
+
result[key] = value
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_yaml(path: Union[str, Path]) -> Dict:
|
|
131
|
+
"""Load YAML file and return as dictionary."""
|
|
132
|
+
yaml = YAML()
|
|
133
|
+
yaml.preserve_quotes = True
|
|
134
|
+
with open(path, "r") as f:
|
|
135
|
+
return dict(yaml.load(f))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def reload_style(style: Optional[Union[str, Path]] = None) -> DotDict:
|
|
139
|
+
"""Reload style from YAML file (clears cache).
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
style : str or Path, optional
|
|
144
|
+
Style preset name (e.g., "SCIENTIFIC") or path to YAML file.
|
|
145
|
+
If None, uses default SCIENTIFIC preset.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
DotDict
|
|
150
|
+
Style configuration as DotDict for dot-access
|
|
151
|
+
"""
|
|
152
|
+
global _STYLE_CACHE, _CURRENT_STYLE_NAME
|
|
153
|
+
_STYLE_CACHE = None
|
|
154
|
+
_CURRENT_STYLE_NAME = None
|
|
155
|
+
return load_style(style)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _apply_dark_theme(style_dict: Dict) -> Dict:
|
|
159
|
+
"""Apply dark theme transformation to a style dictionary.
|
|
160
|
+
|
|
161
|
+
Changes only UI elements (background, text, spines, ticks) -
|
|
162
|
+
NOT the data/figure colors for scientific integrity.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
style_dict : dict
|
|
167
|
+
Original style dictionary
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
dict
|
|
172
|
+
Style dictionary with dark theme applied
|
|
173
|
+
"""
|
|
174
|
+
import copy
|
|
175
|
+
result = copy.deepcopy(style_dict)
|
|
176
|
+
|
|
177
|
+
# Monaco/VS Code dark theme colors (from scitex-cloud UIUX.md)
|
|
178
|
+
dark_colors = {
|
|
179
|
+
"figure_bg": "#1e1e1e", # VS Code main background
|
|
180
|
+
"axes_bg": "#1e1e1e", # Same as figure background
|
|
181
|
+
"legend_bg": "#1e1e1e", # Same as figure background
|
|
182
|
+
"text": "#d4d4d4", # VS Code default text
|
|
183
|
+
"spine": "#3c3c3c", # Subtle border color
|
|
184
|
+
"tick": "#d4d4d4", # Match text
|
|
185
|
+
"grid": "#3a3a3a", # Subtle grid
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Update theme section
|
|
189
|
+
if "theme" not in result:
|
|
190
|
+
result["theme"] = {}
|
|
191
|
+
result["theme"]["mode"] = "dark"
|
|
192
|
+
result["theme"]["dark"] = dark_colors
|
|
193
|
+
|
|
194
|
+
# Update output to not be transparent (dark bg needs to show)
|
|
195
|
+
if "output" in result:
|
|
196
|
+
result["output"]["transparent"] = False
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def unload_style() -> None:
|
|
202
|
+
"""Unload the current style and reset to matplotlib defaults.
|
|
203
|
+
|
|
204
|
+
After calling this, subsequent `subplots()` calls will use vanilla
|
|
205
|
+
matplotlib behavior without FigRecipe styling.
|
|
206
|
+
|
|
207
|
+
Examples
|
|
208
|
+
--------
|
|
209
|
+
>>> import figrecipe as fr
|
|
210
|
+
>>> fr.load_style("SCITEX") # Apply scientific style
|
|
211
|
+
>>> fig, ax = fr.subplots() # Styled
|
|
212
|
+
>>> fr.unload_style() # Reset to matplotlib defaults
|
|
213
|
+
>>> fig, ax = fr.subplots() # Vanilla matplotlib
|
|
214
|
+
"""
|
|
215
|
+
global _STYLE_CACHE, _CURRENT_STYLE_NAME
|
|
216
|
+
_STYLE_CACHE = None
|
|
217
|
+
_CURRENT_STYLE_NAME = None
|
|
218
|
+
|
|
219
|
+
# Reset matplotlib rcParams to defaults
|
|
220
|
+
import matplotlib as mpl
|
|
221
|
+
mpl.rcParams.update(mpl.rcParamsDefault)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def load_style(style: Optional[Union[str, Path, bool]] = "SCITEX", dark: bool = False) -> Optional[DotDict]:
|
|
225
|
+
"""Load style configuration from preset or YAML file.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
style : str, Path, bool, or None
|
|
230
|
+
One of:
|
|
231
|
+
- "SCITEX" / "FIGRECIPE": Scientific publication style (default)
|
|
232
|
+
- "MATPLOTLIB": Vanilla matplotlib defaults
|
|
233
|
+
- Path to custom YAML file: "/path/to/my_style.yaml"
|
|
234
|
+
- None or False: Unload style (reset to matplotlib defaults)
|
|
235
|
+
dark : bool, optional
|
|
236
|
+
If True, apply dark theme transformation (default: False)
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
DotDict or None
|
|
241
|
+
Style configuration as DotDict for dot-access.
|
|
242
|
+
Returns None if style is unloaded.
|
|
243
|
+
|
|
244
|
+
Examples
|
|
245
|
+
--------
|
|
246
|
+
>>> import figrecipe as fr
|
|
247
|
+
|
|
248
|
+
>>> # Load scientific style (default)
|
|
249
|
+
>>> style = fr.load_style()
|
|
250
|
+
>>> style = fr.load_style("SCITEX") # explicit
|
|
251
|
+
|
|
252
|
+
>>> # Load dark variant
|
|
253
|
+
>>> fr.load_style("SCITEX_DARK")
|
|
254
|
+
>>> fr.load_style("SCITEX", dark=True) # equivalent
|
|
255
|
+
|
|
256
|
+
>>> # Reset to vanilla matplotlib
|
|
257
|
+
>>> fr.load_style(None) # unload
|
|
258
|
+
>>> fr.load_style(False) # unload
|
|
259
|
+
>>> fr.load_style("MATPLOTLIB") # explicit vanilla
|
|
260
|
+
|
|
261
|
+
>>> # Load custom YAML file
|
|
262
|
+
>>> fr.load_style("/path/to/my_style.yaml")
|
|
263
|
+
|
|
264
|
+
>>> # Access style values
|
|
265
|
+
>>> style = fr.load_style("SCITEX")
|
|
266
|
+
>>> style.fonts.axis_label_pt
|
|
267
|
+
7
|
|
268
|
+
"""
|
|
269
|
+
global _STYLE_CACHE, _CURRENT_STYLE_NAME
|
|
270
|
+
|
|
271
|
+
# Handle None or False as unload
|
|
272
|
+
if style is None or style is False:
|
|
273
|
+
unload_style()
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
# Handle _DARK suffix in style name
|
|
277
|
+
apply_dark = dark
|
|
278
|
+
base_style = style
|
|
279
|
+
if isinstance(style, str) and style.upper().endswith("_DARK"):
|
|
280
|
+
apply_dark = True
|
|
281
|
+
base_style = style[:-5] # Remove "_DARK" suffix
|
|
282
|
+
|
|
283
|
+
# Build cache key
|
|
284
|
+
cache_key = f"{base_style}{'_DARK' if apply_dark else ''}"
|
|
285
|
+
|
|
286
|
+
# Use cache if available and same style requested
|
|
287
|
+
if _STYLE_CACHE is not None and cache_key == _CURRENT_STYLE_NAME:
|
|
288
|
+
return _STYLE_CACHE
|
|
289
|
+
|
|
290
|
+
# Resolve aliases (e.g., SCITEX -> FIGRECIPE)
|
|
291
|
+
if isinstance(base_style, str):
|
|
292
|
+
resolved_style = _PRESET_ALIASES.get(base_style.upper(), base_style)
|
|
293
|
+
else:
|
|
294
|
+
resolved_style = base_style
|
|
295
|
+
|
|
296
|
+
# Determine the style path
|
|
297
|
+
if isinstance(resolved_style, Path) or (isinstance(resolved_style, str) and ("/" in resolved_style or "\\" in resolved_style or resolved_style.endswith(".yaml"))):
|
|
298
|
+
# Explicit file path
|
|
299
|
+
style_path = Path(resolved_style)
|
|
300
|
+
style_name = str(resolved_style)
|
|
301
|
+
else:
|
|
302
|
+
# Preset name (e.g., "FIGRECIPE", "MATPLOTLIB")
|
|
303
|
+
style_path = _PRESETS_DIR / f"{resolved_style.upper()}.yaml"
|
|
304
|
+
style_name = resolved_style.upper()
|
|
305
|
+
|
|
306
|
+
# Check if file exists
|
|
307
|
+
if not style_path.exists():
|
|
308
|
+
available = list_presets()
|
|
309
|
+
raise FileNotFoundError(
|
|
310
|
+
f"Style not found: {style}\n"
|
|
311
|
+
f"Available presets: {available}\n"
|
|
312
|
+
f"Or provide a path to a custom YAML file."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
style_dict = _load_yaml(style_path)
|
|
316
|
+
|
|
317
|
+
# Apply dark theme if requested
|
|
318
|
+
if apply_dark:
|
|
319
|
+
style_dict = _apply_dark_theme(style_dict)
|
|
320
|
+
style_name = f"{style_name}_DARK"
|
|
321
|
+
|
|
322
|
+
# Convert to DotDict for convenient access
|
|
323
|
+
result = DotDict(style_dict)
|
|
324
|
+
|
|
325
|
+
# Cache the style
|
|
326
|
+
_STYLE_CACHE = result
|
|
327
|
+
_CURRENT_STYLE_NAME = style_name
|
|
328
|
+
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_style() -> DotDict:
|
|
333
|
+
"""Get the current loaded style (loads default if not yet loaded).
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
DotDict
|
|
338
|
+
Current style configuration
|
|
339
|
+
"""
|
|
340
|
+
global _STYLE_CACHE
|
|
341
|
+
if _STYLE_CACHE is None:
|
|
342
|
+
return load_style()
|
|
343
|
+
return _STYLE_CACHE
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def to_subplots_kwargs(style: Optional[DotDict] = None) -> Dict[str, Any]:
|
|
347
|
+
"""Convert style DotDict to kwargs for ps.subplots().
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
style : DotDict, optional
|
|
352
|
+
Style configuration. If None, uses current loaded style.
|
|
353
|
+
|
|
354
|
+
Returns
|
|
355
|
+
-------
|
|
356
|
+
dict
|
|
357
|
+
Keyword arguments for ps.subplots()
|
|
358
|
+
|
|
359
|
+
Examples
|
|
360
|
+
--------
|
|
361
|
+
>>> style = load_style()
|
|
362
|
+
>>> kwargs = to_subplots_kwargs(style)
|
|
363
|
+
>>> fig, ax = ps.subplots(**kwargs)
|
|
364
|
+
"""
|
|
365
|
+
if style is None:
|
|
366
|
+
style = get_style()
|
|
367
|
+
|
|
368
|
+
result = {
|
|
369
|
+
# Axes dimensions
|
|
370
|
+
"axes_width_mm": style.axes.width_mm,
|
|
371
|
+
"axes_height_mm": style.axes.height_mm,
|
|
372
|
+
"axes_thickness_mm": style.axes.thickness_mm,
|
|
373
|
+
# Margins
|
|
374
|
+
"margin_left_mm": style.margins.left_mm,
|
|
375
|
+
"margin_right_mm": style.margins.right_mm,
|
|
376
|
+
"margin_bottom_mm": style.margins.bottom_mm,
|
|
377
|
+
"margin_top_mm": style.margins.top_mm,
|
|
378
|
+
# Spacing
|
|
379
|
+
"space_w_mm": style.spacing.horizontal_mm,
|
|
380
|
+
"space_h_mm": style.spacing.vertical_mm,
|
|
381
|
+
# Ticks
|
|
382
|
+
"tick_length_mm": style.ticks.length_mm,
|
|
383
|
+
"tick_thickness_mm": style.ticks.thickness_mm,
|
|
384
|
+
"n_ticks": style.ticks.n_ticks,
|
|
385
|
+
# Lines
|
|
386
|
+
"trace_thickness_mm": style.lines.trace_mm,
|
|
387
|
+
# Markers
|
|
388
|
+
"marker_size_mm": style.markers.size_mm,
|
|
389
|
+
# Fonts
|
|
390
|
+
"font_family": style.fonts.family,
|
|
391
|
+
"axis_font_size_pt": style.fonts.axis_label_pt,
|
|
392
|
+
"tick_font_size_pt": style.fonts.tick_label_pt,
|
|
393
|
+
"title_font_size_pt": style.fonts.title_pt,
|
|
394
|
+
"suptitle_font_size_pt": style.fonts.suptitle_pt,
|
|
395
|
+
"legend_font_size_pt": style.fonts.legend_pt,
|
|
396
|
+
# Padding
|
|
397
|
+
"label_pad_pt": style.padding.label_pt,
|
|
398
|
+
"tick_pad_pt": style.padding.tick_pt,
|
|
399
|
+
"title_pad_pt": style.padding.title_pt,
|
|
400
|
+
# Output
|
|
401
|
+
"dpi": style.output.dpi,
|
|
402
|
+
# Theme
|
|
403
|
+
"theme": style.theme.mode,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Add theme colors from preset if available
|
|
407
|
+
theme_mode = style.theme.mode
|
|
408
|
+
if "theme" in style and theme_mode in style.theme:
|
|
409
|
+
result["theme_colors"] = dict(style.theme[theme_mode])
|
|
410
|
+
|
|
411
|
+
# Add color palette if available
|
|
412
|
+
if "colors" in style and "palette" in style.colors:
|
|
413
|
+
result["color_palette"] = list(style.colors.palette)
|
|
414
|
+
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# Lazy-loaded global STYLE object
|
|
419
|
+
class _StyleProxy:
|
|
420
|
+
"""Proxy object that loads style on first access."""
|
|
421
|
+
|
|
422
|
+
def __getattr__(self, name: str) -> Any:
|
|
423
|
+
return getattr(get_style(), name)
|
|
424
|
+
|
|
425
|
+
def __repr__(self) -> str:
|
|
426
|
+
return repr(get_style())
|
|
427
|
+
|
|
428
|
+
def to_subplots_kwargs(self) -> Dict[str, Any]:
|
|
429
|
+
"""Convert style to subplots kwargs."""
|
|
430
|
+
return to_subplots_kwargs()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
STYLE = _StyleProxy()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
# Test loading
|
|
438
|
+
print("Loading default style...")
|
|
439
|
+
style = load_style()
|
|
440
|
+
print(f" axes.width_mm: {style.axes.width_mm}")
|
|
441
|
+
print(f" fonts.axis_label_pt: {style.fonts.axis_label_pt}")
|
|
442
|
+
print(f" lines.trace_mm: {style.lines.trace_mm}")
|
|
443
|
+
|
|
444
|
+
print("\nConverting to subplots kwargs...")
|
|
445
|
+
kwargs = to_subplots_kwargs()
|
|
446
|
+
for k, v in list(kwargs.items())[:5]:
|
|
447
|
+
print(f" {k}: {v}")
|
|
448
|
+
|
|
449
|
+
print("\nUsing STYLE proxy...")
|
|
450
|
+
print(f" STYLE.fonts.family: {STYLE.fonts.family}")
|