ggplot2-python 4.0.2.9000__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.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
ggplot2_py/_compat.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
R infrastructure shims for ggplot2.
|
|
3
|
+
|
|
4
|
+
Replaces functionality from rlang, cli, lifecycle, withr, and vctrs that
|
|
5
|
+
ggplot2 relies on, adapted for idiomatic Python usage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Any, NoReturn, Optional
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"cli_abort",
|
|
16
|
+
"cli_warn",
|
|
17
|
+
"cli_inform",
|
|
18
|
+
"is_string",
|
|
19
|
+
"is_bool",
|
|
20
|
+
"is_character",
|
|
21
|
+
"is_null",
|
|
22
|
+
"is_bare_list",
|
|
23
|
+
"is_true",
|
|
24
|
+
"is_false",
|
|
25
|
+
"is_scalar_character",
|
|
26
|
+
"is_scalar_logical",
|
|
27
|
+
"is_installed",
|
|
28
|
+
"check_installed",
|
|
29
|
+
"deprecate_warn",
|
|
30
|
+
"deprecate_soft",
|
|
31
|
+
"deprecate_stop",
|
|
32
|
+
"Waiver",
|
|
33
|
+
"is_waiver",
|
|
34
|
+
"waiver",
|
|
35
|
+
"caller_arg",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# CLI messaging helpers (rlang / cli replacements)
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def cli_abort(
|
|
44
|
+
message: str,
|
|
45
|
+
*,
|
|
46
|
+
call: Optional[str] = None,
|
|
47
|
+
cls: type = ValueError,
|
|
48
|
+
**kwargs: Any,
|
|
49
|
+
) -> NoReturn:
|
|
50
|
+
"""Raise an exception with a formatted message.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
message : str
|
|
55
|
+
Error message. May contain ``{name}``-style placeholders that
|
|
56
|
+
are filled from *kwargs*.
|
|
57
|
+
call : str, optional
|
|
58
|
+
Name of the calling function (for context in the message).
|
|
59
|
+
cls : type, optional
|
|
60
|
+
Exception class to raise. Defaults to ``ValueError``.
|
|
61
|
+
**kwargs : Any
|
|
62
|
+
Substitution values for placeholders in *message*.
|
|
63
|
+
|
|
64
|
+
Raises
|
|
65
|
+
------
|
|
66
|
+
Exception
|
|
67
|
+
An instance of *cls* with the formatted message.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
formatted = message.format(**kwargs) if kwargs else message
|
|
71
|
+
except (KeyError, IndexError):
|
|
72
|
+
formatted = message
|
|
73
|
+
raise cls(formatted)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cli_warn(
|
|
77
|
+
message: str,
|
|
78
|
+
*,
|
|
79
|
+
call: Optional[str] = None,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Issue a ``UserWarning`` with a formatted message.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
message : str
|
|
87
|
+
Warning message. May contain ``{name}``-style placeholders.
|
|
88
|
+
call : str, optional
|
|
89
|
+
Name of the calling function.
|
|
90
|
+
**kwargs : Any
|
|
91
|
+
Substitution values for placeholders in *message*.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
formatted = message.format(**kwargs) if kwargs else message
|
|
95
|
+
except (KeyError, IndexError):
|
|
96
|
+
formatted = message
|
|
97
|
+
warnings.warn(formatted, UserWarning, stacklevel=2)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cli_inform(
|
|
101
|
+
message: str,
|
|
102
|
+
*,
|
|
103
|
+
call: Optional[str] = None,
|
|
104
|
+
**kwargs: Any,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Emit an informational message (no-op by default).
|
|
107
|
+
|
|
108
|
+
In interactive sessions this could print; in batch mode it stays silent.
|
|
109
|
+
Override by monkey-patching if verbose output is desired.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
message : str
|
|
114
|
+
Informational message.
|
|
115
|
+
call : str, optional
|
|
116
|
+
Name of the calling function.
|
|
117
|
+
**kwargs : Any
|
|
118
|
+
Substitution values for placeholders in *message*.
|
|
119
|
+
"""
|
|
120
|
+
# Intentionally silent – mirrors rlang::inform() in non-interactive R.
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Type-checking predicates (rlang replacements)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def is_string(x: Any) -> bool:
|
|
129
|
+
"""Check whether *x* is a single string.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
x : Any
|
|
134
|
+
Object to test.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
bool
|
|
139
|
+
``True`` if *x* is an instance of ``str``.
|
|
140
|
+
"""
|
|
141
|
+
return isinstance(x, str)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_bool(x: Any) -> bool:
|
|
145
|
+
"""Check whether *x* is a Python ``bool``.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
x : Any
|
|
150
|
+
Object to test.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
bool
|
|
155
|
+
``True`` if *x* is an instance of ``bool``.
|
|
156
|
+
"""
|
|
157
|
+
return isinstance(x, bool)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_character(x: Any) -> bool:
|
|
161
|
+
"""Check whether *x* is a string or list of strings.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
x : Any
|
|
166
|
+
Object to test.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
bool
|
|
171
|
+
``True`` if *x* is a ``str`` or a ``list`` whose elements are all
|
|
172
|
+
strings.
|
|
173
|
+
"""
|
|
174
|
+
if isinstance(x, str):
|
|
175
|
+
return True
|
|
176
|
+
if isinstance(x, list):
|
|
177
|
+
return all(isinstance(el, str) for el in x)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def is_null(x: Any) -> bool:
|
|
182
|
+
"""Check whether *x* is ``None``.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
x : Any
|
|
187
|
+
Object to test.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
bool
|
|
192
|
+
``True`` if *x* is ``None``.
|
|
193
|
+
"""
|
|
194
|
+
return x is None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def is_bare_list(x: Any) -> bool:
|
|
198
|
+
"""Check whether *x* is a plain ``list`` (not a subclass).
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
x : Any
|
|
203
|
+
Object to test.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
bool
|
|
208
|
+
``True`` if ``type(x)`` is exactly ``list``.
|
|
209
|
+
"""
|
|
210
|
+
return type(x) is list
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_true(x: Any) -> bool:
|
|
214
|
+
"""Check whether *x* is literally ``True``.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
x : Any
|
|
219
|
+
Object to test.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
bool
|
|
224
|
+
"""
|
|
225
|
+
return x is True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_false(x: Any) -> bool:
|
|
229
|
+
"""Check whether *x* is literally ``False``.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
x : Any
|
|
234
|
+
Object to test.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
bool
|
|
239
|
+
"""
|
|
240
|
+
return x is False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def is_scalar_character(x: Any) -> bool:
|
|
244
|
+
"""Check whether *x* is a length-1 character value (a single string).
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
x : Any
|
|
249
|
+
Object to test.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
bool
|
|
254
|
+
"""
|
|
255
|
+
return isinstance(x, str)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def is_scalar_logical(x: Any) -> bool:
|
|
259
|
+
"""Check whether *x* is a length-1 logical value (a single bool).
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
x : Any
|
|
264
|
+
Object to test.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
bool
|
|
269
|
+
"""
|
|
270
|
+
return isinstance(x, bool)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Package availability (rlang replacements)
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def is_installed(pkg: str) -> bool:
|
|
278
|
+
"""Check whether a Python package is importable.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
pkg : str
|
|
283
|
+
Package name (dotted names are supported).
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
bool
|
|
288
|
+
``True`` if the package can be imported.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
importlib.import_module(pkg)
|
|
292
|
+
return True
|
|
293
|
+
except ImportError:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def check_installed(
|
|
298
|
+
pkg: str,
|
|
299
|
+
reason: Optional[str] = None,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Raise ``ImportError`` if a package is not available.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
pkg : str
|
|
306
|
+
Package name.
|
|
307
|
+
reason : str, optional
|
|
308
|
+
Human-readable reason the package is needed (appended to the
|
|
309
|
+
error message).
|
|
310
|
+
|
|
311
|
+
Raises
|
|
312
|
+
------
|
|
313
|
+
ImportError
|
|
314
|
+
If the package cannot be imported.
|
|
315
|
+
"""
|
|
316
|
+
if not is_installed(pkg):
|
|
317
|
+
msg = f"The '{pkg}' package is required"
|
|
318
|
+
if reason:
|
|
319
|
+
msg += f" {reason}"
|
|
320
|
+
msg += "."
|
|
321
|
+
raise ImportError(msg)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Deprecation helpers (lifecycle replacements)
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def deprecate_warn(
|
|
329
|
+
when: str,
|
|
330
|
+
what: str,
|
|
331
|
+
with_: Optional[str] = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Emit a deprecation warning.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
when : str
|
|
338
|
+
Version in which the feature was deprecated (e.g. ``"3.4.0"``).
|
|
339
|
+
what : str
|
|
340
|
+
Description of the deprecated feature.
|
|
341
|
+
with_ : str, optional
|
|
342
|
+
Replacement to suggest.
|
|
343
|
+
"""
|
|
344
|
+
msg = f"{what} was deprecated in version {when}."
|
|
345
|
+
if with_ is not None:
|
|
346
|
+
msg += f" Please use {with_} instead."
|
|
347
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def deprecate_soft(
|
|
351
|
+
when: str,
|
|
352
|
+
what: str,
|
|
353
|
+
with_: Optional[str] = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Emit a soft deprecation warning.
|
|
356
|
+
|
|
357
|
+
Soft deprecations are shown only when the deprecated code is called
|
|
358
|
+
from outside the package. In this Python port the distinction is
|
|
359
|
+
ignored and a normal ``DeprecationWarning`` is emitted.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
when : str
|
|
364
|
+
Version string.
|
|
365
|
+
what : str
|
|
366
|
+
Deprecated feature description.
|
|
367
|
+
with_ : str, optional
|
|
368
|
+
Replacement suggestion.
|
|
369
|
+
"""
|
|
370
|
+
deprecate_warn(when, what, with_=with_)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def deprecate_stop(
|
|
374
|
+
when: str,
|
|
375
|
+
what: str,
|
|
376
|
+
with_: Optional[str] = None,
|
|
377
|
+
) -> NoReturn:
|
|
378
|
+
"""Raise an error for a defunct (fully removed) feature.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
when : str
|
|
383
|
+
Version in which the feature was removed.
|
|
384
|
+
what : str
|
|
385
|
+
Description of the removed feature.
|
|
386
|
+
with_ : str, optional
|
|
387
|
+
Replacement to suggest.
|
|
388
|
+
|
|
389
|
+
Raises
|
|
390
|
+
------
|
|
391
|
+
RuntimeError
|
|
392
|
+
Always raised.
|
|
393
|
+
"""
|
|
394
|
+
msg = f"{what} was deprecated in version {when} and is now defunct."
|
|
395
|
+
if with_ is not None:
|
|
396
|
+
msg += f" Please use {with_} instead."
|
|
397
|
+
raise RuntimeError(msg)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# Waiver sentinel (ggplot2-specific)
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
class Waiver:
|
|
405
|
+
"""Sentinel class indicating "use the default value".
|
|
406
|
+
|
|
407
|
+
In R ggplot2, ``waiver()`` signals that a parameter should fall back to
|
|
408
|
+
the default computed by the plot. This Python equivalent works the
|
|
409
|
+
same way.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
_instance: Optional["Waiver"] = None
|
|
413
|
+
|
|
414
|
+
def __new__(cls) -> "Waiver":
|
|
415
|
+
# Singleton so that ``Waiver() is Waiver()`` holds.
|
|
416
|
+
if cls._instance is None:
|
|
417
|
+
cls._instance = super().__new__(cls)
|
|
418
|
+
return cls._instance
|
|
419
|
+
|
|
420
|
+
def __repr__(self) -> str:
|
|
421
|
+
return "waiver()"
|
|
422
|
+
|
|
423
|
+
def __bool__(self) -> bool:
|
|
424
|
+
# Prevent accidental truthiness tests.
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def waiver() -> Waiver:
|
|
429
|
+
"""Return a ``Waiver`` sentinel value.
|
|
430
|
+
|
|
431
|
+
Returns
|
|
432
|
+
-------
|
|
433
|
+
Waiver
|
|
434
|
+
The singleton waiver instance.
|
|
435
|
+
"""
|
|
436
|
+
return Waiver()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def is_waiver(x: Any) -> bool:
|
|
440
|
+
"""Check whether *x* is a ``Waiver`` sentinel.
|
|
441
|
+
|
|
442
|
+
Parameters
|
|
443
|
+
----------
|
|
444
|
+
x : Any
|
|
445
|
+
Object to test.
|
|
446
|
+
|
|
447
|
+
Returns
|
|
448
|
+
-------
|
|
449
|
+
bool
|
|
450
|
+
``True`` if *x* is a ``Waiver`` instance.
|
|
451
|
+
"""
|
|
452
|
+
return isinstance(x, Waiver)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Miscellaneous rlang helpers
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def caller_arg(arg: str) -> str:
|
|
460
|
+
"""Return a human-readable label for a function argument.
|
|
461
|
+
|
|
462
|
+
In R, ``rlang::caller_arg()`` inspects the call stack. Here we
|
|
463
|
+
simply return the argument name as-is.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
arg : str
|
|
468
|
+
Argument name.
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
str
|
|
473
|
+
The same string, suitable for inclusion in error messages.
|
|
474
|
+
"""
|
|
475
|
+
return arg
|
ggplot2_py/_plugins.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry-point-based plugin discovery for ggplot2_py.
|
|
3
|
+
|
|
4
|
+
This module scans ``importlib.metadata.entry_points`` for packages that
|
|
5
|
+
declare extensions under the following groups:
|
|
6
|
+
|
|
7
|
+
- ``ggplot2_py.geoms`` — custom Geom subclasses
|
|
8
|
+
- ``ggplot2_py.stats`` — custom Stat subclasses
|
|
9
|
+
- ``ggplot2_py.positions`` — custom Position subclasses
|
|
10
|
+
- ``ggplot2_py.scales`` — custom Scale subclasses
|
|
11
|
+
- ``ggplot2_py.coords`` — custom Coord subclasses
|
|
12
|
+
- ``ggplot2_py.facets`` — custom Facet subclasses
|
|
13
|
+
|
|
14
|
+
Extension packages declare entry points in their ``pyproject.toml``::
|
|
15
|
+
|
|
16
|
+
[project.entry-points."ggplot2_py.geoms"]
|
|
17
|
+
star = "my_ext.geoms:GeomStar"
|
|
18
|
+
|
|
19
|
+
[project.entry-points."ggplot2_py.stats"]
|
|
20
|
+
chull = "my_ext.stats:StatChull"
|
|
21
|
+
|
|
22
|
+
At ggplot2_py import time, :func:`discover_extensions` scans all installed
|
|
23
|
+
packages, loads the declared classes, and registers them in the
|
|
24
|
+
corresponding ``_registry`` dictionaries. If a plugin fails to load
|
|
25
|
+
(e.g. missing dependency), a warning is emitted but import is not blocked.
|
|
26
|
+
|
|
27
|
+
This is a **Python-exclusive** extension mechanism — R has no equivalent.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import importlib
|
|
33
|
+
import warnings
|
|
34
|
+
from importlib.metadata import entry_points
|
|
35
|
+
from typing import Any, Dict, List, Tuple
|
|
36
|
+
|
|
37
|
+
__all__ = ["discover_extensions", "list_extensions"]
|
|
38
|
+
|
|
39
|
+
# (entry_point_group, module_path, base_class_name)
|
|
40
|
+
_EXTENSION_GROUPS: List[Tuple[str, str, str]] = [
|
|
41
|
+
("ggplot2_py.geoms", "ggplot2_py.geom", "Geom"),
|
|
42
|
+
("ggplot2_py.stats", "ggplot2_py.stat", "Stat"),
|
|
43
|
+
("ggplot2_py.positions", "ggplot2_py.position", "Position"),
|
|
44
|
+
("ggplot2_py.scales", "ggplot2_py.scale", "Scale"),
|
|
45
|
+
("ggplot2_py.coords", "ggplot2_py.coord", "Coord"),
|
|
46
|
+
("ggplot2_py.facets", "ggplot2_py.facet", "Facet"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
_discovered: Dict[str, List[str]] = {} # group -> list of names
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def discover_extensions() -> Dict[str, List[str]]:
|
|
53
|
+
"""Scan installed packages for ggplot2_py entry-point extensions.
|
|
54
|
+
|
|
55
|
+
For each declared entry point, the class is loaded and registered in
|
|
56
|
+
the corresponding base class's ``_registry``. Classes that are
|
|
57
|
+
already registered (e.g. via ``__init_subclass__``) are skipped.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
dict
|
|
62
|
+
Mapping of ``{group: [name, ...]}`` for all discovered extensions.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
::
|
|
67
|
+
|
|
68
|
+
from ggplot2_py._plugins import discover_extensions
|
|
69
|
+
found = discover_extensions()
|
|
70
|
+
print(found)
|
|
71
|
+
# {'ggplot2_py.geoms': ['star'], 'ggplot2_py.stats': ['chull'], ...}
|
|
72
|
+
"""
|
|
73
|
+
global _discovered
|
|
74
|
+
result: Dict[str, List[str]] = {}
|
|
75
|
+
|
|
76
|
+
eps = entry_points()
|
|
77
|
+
|
|
78
|
+
for group, mod_path, base_name in _EXTENSION_GROUPS:
|
|
79
|
+
group_eps = list(eps.select(group=group))
|
|
80
|
+
if not group_eps:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Import the base module to access the registry
|
|
84
|
+
try:
|
|
85
|
+
mod = importlib.import_module(mod_path)
|
|
86
|
+
base_cls = getattr(mod, base_name)
|
|
87
|
+
registry = getattr(base_cls, "_registry", None)
|
|
88
|
+
except Exception:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
names: List[str] = []
|
|
92
|
+
for ep in group_eps:
|
|
93
|
+
try:
|
|
94
|
+
cls = ep.load()
|
|
95
|
+
# Register under both the entry-point name and CamelCase
|
|
96
|
+
if registry is not None:
|
|
97
|
+
if ep.name not in registry:
|
|
98
|
+
registry[ep.name] = cls
|
|
99
|
+
# Also register CamelCase form
|
|
100
|
+
camel = ep.name[0].upper() + ep.name[1:] if ep.name else ep.name
|
|
101
|
+
if camel not in registry:
|
|
102
|
+
registry[camel] = cls
|
|
103
|
+
names.append(ep.name)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
warnings.warn(
|
|
106
|
+
f"ggplot2_py: failed to load extension '{ep.name}' "
|
|
107
|
+
f"from group '{group}': {exc}",
|
|
108
|
+
stacklevel=2,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if names:
|
|
112
|
+
result[group] = names
|
|
113
|
+
|
|
114
|
+
_discovered = result
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def list_extensions() -> Dict[str, List[str]]:
|
|
119
|
+
"""Return previously discovered extensions (without re-scanning).
|
|
120
|
+
|
|
121
|
+
Call :func:`discover_extensions` first, or rely on the automatic
|
|
122
|
+
scan at ggplot2_py import time.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
dict
|
|
127
|
+
Mapping of ``{group: [name, ...]}`` for discovered extensions.
|
|
128
|
+
"""
|
|
129
|
+
return dict(_discovered)
|