figrecipe 0.5.0__py3-none-any.whl → 0.6.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.
Files changed (90) hide show
  1. figrecipe/__init__.py +361 -93
  2. figrecipe/_dev/__init__.py +120 -0
  3. figrecipe/_dev/demo_plotters/__init__.py +195 -0
  4. figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
  5. figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
  6. figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
  7. figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
  8. figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
  9. figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
  10. figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
  11. figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
  12. figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
  13. figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
  14. figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
  15. figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
  16. figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
  17. figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
  18. figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
  19. figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
  20. figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
  21. figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
  22. figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
  23. figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
  24. figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
  25. figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
  26. figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
  27. figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
  29. figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
  30. figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
  31. figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
  32. figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
  33. figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
  34. figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
  35. figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
  36. figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
  37. figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
  38. figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
  39. figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
  40. figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
  41. figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
  42. figrecipe/_dev/demo_plotters/plot_step.py +27 -0
  43. figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
  44. figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
  45. figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
  46. figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
  47. figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
  48. figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
  49. figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
  50. figrecipe/_editor/__init__.py +230 -0
  51. figrecipe/_editor/_bbox.py +978 -0
  52. figrecipe/_editor/_flask_app.py +1229 -0
  53. figrecipe/_editor/_hitmap.py +937 -0
  54. figrecipe/_editor/_overrides.py +318 -0
  55. figrecipe/_editor/_renderer.py +349 -0
  56. figrecipe/_editor/_templates/__init__.py +75 -0
  57. figrecipe/_editor/_templates/_html.py +406 -0
  58. figrecipe/_editor/_templates/_scripts.py +2778 -0
  59. figrecipe/_editor/_templates/_styles.py +1326 -0
  60. figrecipe/_params/_DECORATION_METHODS.py +27 -0
  61. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  62. figrecipe/_params/__init__.py +9 -0
  63. figrecipe/_recorder.py +126 -73
  64. figrecipe/_reproducer.py +658 -41
  65. figrecipe/_seaborn.py +14 -9
  66. figrecipe/_serializer.py +2 -2
  67. figrecipe/_signatures/README.md +68 -0
  68. figrecipe/_signatures/__init__.py +12 -2
  69. figrecipe/_signatures/_loader.py +515 -56
  70. figrecipe/_utils/__init__.py +6 -4
  71. figrecipe/_utils/_crop.py +10 -4
  72. figrecipe/_utils/_image_diff.py +37 -33
  73. figrecipe/_utils/_numpy_io.py +0 -1
  74. figrecipe/_utils/_units.py +11 -3
  75. figrecipe/_validator.py +12 -3
  76. figrecipe/_wrappers/_axes.py +860 -46
  77. figrecipe/_wrappers/_figure.py +115 -18
  78. figrecipe/plt.py +0 -1
  79. figrecipe/pyplot.py +2 -1
  80. figrecipe/styles/__init__.py +9 -10
  81. figrecipe/styles/_style_applier.py +332 -28
  82. figrecipe/styles/_style_loader.py +172 -44
  83. figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
  84. figrecipe/styles/presets/SCITEX.yaml +176 -0
  85. figrecipe-0.6.0.dist-info/METADATA +394 -0
  86. figrecipe-0.6.0.dist-info/RECORD +90 -0
  87. figrecipe-0.5.0.dist-info/METADATA +0 -336
  88. figrecipe-0.5.0.dist-info/RECORD +0 -26
  89. {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
  90. {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,32 +1,475 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """Load and query matplotlib function signatures."""
3
+ # Timestamp: "2025-12-23 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/figrecipe/src/figrecipe/_signatures/_loader.py
5
+
6
+ """Load and query matplotlib function signatures with deep inspection.
7
+
8
+ Parses *args/**kwargs from docstrings and expands them to actual parameters.
9
+ """
4
10
 
5
11
  import inspect
6
- from typing import Any, Dict, List, Optional, Set
12
+ import re
13
+ from typing import Any, Dict, List, Optional
7
14
 
8
15
  import matplotlib.pyplot as plt
9
16
 
10
-
11
17
  # Cache for signatures
12
18
  _SIGNATURE_CACHE: Dict[str, Dict[str, Any]] = {}
13
19
 
14
20
 
15
- def get_signature(method_name: str) -> Dict[str, Any]:
16
- """Get signature for a matplotlib Axes method.
21
+ # -----------------------------------------------------------------------------
22
+ # Docstring parsing
23
+ # -----------------------------------------------------------------------------
24
+ def _parse_parameter_types(docstring: Optional[str]) -> Dict[str, str]:
25
+ """Extract parameter types from NumPy-style docstring Parameters section."""
26
+ if not docstring:
27
+ return {}
28
+
29
+ types = {}
30
+
31
+ # Find Parameters section
32
+ params_match = re.search(
33
+ r"Parameters\s*[-]+\s*(.*?)(?:\n\s*Returns|\n\s*See Also|\n\s*Notes|\n\s*Examples|\n\s*Other Parameters|\Z)",
34
+ docstring,
35
+ re.DOTALL,
36
+ )
37
+ if not params_match:
38
+ return {}
39
+
40
+ params_text = params_match.group(1)
41
+
42
+ # Parse lines like "x, y : array-like or float" or "fmt : str, optional"
43
+ for match in re.finditer(
44
+ r"^(\w+(?:\s*,\s*\w+)*)\s*:\s*(.+?)(?=\n\s*\n|\n\w+\s*:|\Z)",
45
+ params_text,
46
+ re.MULTILINE | re.DOTALL,
47
+ ):
48
+ names_str = match.group(1)
49
+ type_str = match.group(2).split("\n")[0].strip() # First line only
50
+
51
+ # Clean up type string
52
+ type_str = re.sub(r",?\s*optional\s*$", "", type_str).strip()
53
+ type_str = re.sub(
54
+ r",?\s*default[^,]*$", "", type_str, flags=re.IGNORECASE
55
+ ).strip()
56
+
57
+ # Handle multiple names like "x, y"
58
+ for name in re.split(r"\s*,\s*", names_str):
59
+ name = name.strip()
60
+ if name:
61
+ types[name.lower()] = type_str
62
+
63
+ return types
64
+
65
+
66
+ def _parse_args_pattern(
67
+ args_str: str, param_types: Dict[str, str]
68
+ ) -> List[Dict[str, Any]]:
69
+ """Parse args pattern like '[x], y, [fmt]' into list of arg dicts."""
70
+ if not args_str:
71
+ return []
72
+
73
+ args = []
74
+ # Split by comma, handling brackets
75
+ parts = re.split(r",\s*", args_str)
76
+
77
+ for part in parts:
78
+ part = part.strip()
79
+ if not part or part == "/": # Skip empty or positional-only marker
80
+ continue
81
+
82
+ # Check if optional (wrapped in [])
83
+ optional = part.startswith("[") and part.endswith("]")
84
+ if optional:
85
+ name = part[1:-1].strip()
86
+ else:
87
+ # Handle cases like "[X, Y,] Z" where Z is required
88
+ name = part.strip("[]").strip()
89
+
90
+ if name and name not in ("...", "*"):
91
+ # Look up type from parsed parameters
92
+ type_str = param_types.get(name.lower())
93
+ args.append(
94
+ {
95
+ "name": name,
96
+ "type": type_str,
97
+ "optional": optional,
98
+ }
99
+ )
100
+
101
+ return args
102
+
103
+
104
+ # Manual *args patterns for functions without parseable call signatures
105
+ MANUAL_ARGS_PATTERNS = {
106
+ "fill": "[x], y, [color]",
107
+ "stackplot": "x, *ys",
108
+ "legend": "[handles], [labels]",
109
+ "stem": "[locs], heads",
110
+ "tricontour": "[triangulation], x, y, z, [levels]",
111
+ "tricontourf": "[triangulation], x, y, z, [levels]",
112
+ "triplot": "[triangulation], x, y, [triangles]",
113
+ "loglog": "[x], y, [fmt]",
114
+ "semilogx": "[x], y, [fmt]",
115
+ "semilogy": "[x], y, [fmt]",
116
+ "barbs": "[X], [Y], U, V, [C]",
117
+ "quiver": "[X], [Y], U, V, [C]",
118
+ "pcolor": "[X], [Y], C",
119
+ "pcolormesh": "[X], [Y], C",
120
+ "pcolorfast": "[X], [Y], C",
121
+ "acorr": "x",
122
+ "xcorr": "x, y",
123
+ "plot": "[x], y, [fmt]",
124
+ }
125
+
126
+
127
+ def _extract_args_from_docstring(
128
+ docstring: Optional[str], func_name: str = ""
129
+ ) -> List[Dict[str, Any]]:
130
+ """Extract *args as flattened list from docstring call signature."""
131
+ if not docstring:
132
+ return []
133
+
134
+ # First, parse parameter types
135
+ param_types = _parse_parameter_types(docstring)
136
+
137
+ # Check for manual pattern first
138
+ if func_name in MANUAL_ARGS_PATTERNS:
139
+ return _parse_args_pattern(MANUAL_ARGS_PATTERNS[func_name], param_types)
140
+
141
+ # Look for "Call signature:" patterns
142
+ patterns = [
143
+ r"Call signatures?::\s*\n\s*(.*?)(?:\n\n|\n[A-Z])",
144
+ r"^\s*(\w+\([^)]+\))\s*$",
145
+ ]
146
+
147
+ for pattern in patterns:
148
+ match = re.search(pattern, docstring, re.MULTILINE | re.DOTALL)
149
+ if match:
150
+ sig_text = match.group(1).strip()
151
+ # Extract first signature line
152
+ first_line = sig_text.split("\n")[0].strip()
153
+ # Parse the args from signature like "plot([x], y, [fmt], *, ...)"
154
+ inner_match = re.search(r"\(([^*]+?)(?:,\s*\*|,\s*data|\))", first_line)
155
+ if inner_match:
156
+ args_str = inner_match.group(1).strip().rstrip(",")
157
+ return _parse_args_pattern(args_str, param_types)
158
+ return []
159
+
160
+
161
+ # -----------------------------------------------------------------------------
162
+ # Kwargs expansion via set_* method introspection
163
+ # -----------------------------------------------------------------------------
164
+ def _get_setter_type(obj: Any, prop_name: str) -> Optional[str]:
165
+ """Get type from set_* method docstring."""
166
+ setter_name = f"set_{prop_name}"
167
+ if not hasattr(obj, setter_name):
168
+ return None
169
+
170
+ method = getattr(obj, setter_name)
171
+ if not method.__doc__:
172
+ return None
173
+
174
+ # Parse Parameters section
175
+ match = re.search(
176
+ r"Parameters\s*[-]+\s*\n\s*(\w+)\s*:\s*(.+?)(?:\n\s*\n|\Z)",
177
+ method.__doc__,
178
+ re.DOTALL,
179
+ )
180
+ if match:
181
+ type_str = match.group(2).split("\n")[0].strip()
182
+ return type_str
183
+ return None
184
+
185
+
186
+ def _build_kwargs_with_types() -> (
187
+ tuple[
188
+ List[Dict[str, Any]],
189
+ List[Dict[str, Any]],
190
+ List[Dict[str, Any]],
191
+ List[Dict[str, Any]],
192
+ ]
193
+ ):
194
+ """Build kwargs lists with types from matplotlib classes."""
195
+ from matplotlib.artist import Artist
196
+ from matplotlib.lines import Line2D
197
+ from matplotlib.patches import Patch
198
+ from matplotlib.text import Text
199
+
200
+ # Create instances for introspection
201
+ line = Line2D([0], [0])
202
+ patch = Patch()
203
+ text = Text()
204
+ artist = Artist()
205
+
206
+ def get_type(obj, name):
207
+ return _get_setter_type(obj, name)
208
+
209
+ ARTIST_KWARGS = [
210
+ {"name": "agg_filter", "type": get_type(artist, "agg_filter"), "default": None},
211
+ {"name": "alpha", "type": get_type(artist, "alpha"), "default": None},
212
+ {"name": "animated", "type": get_type(artist, "animated"), "default": False},
213
+ {"name": "clip_box", "type": get_type(artist, "clip_box"), "default": None},
214
+ {"name": "clip_on", "type": get_type(artist, "clip_on"), "default": True},
215
+ {"name": "clip_path", "type": get_type(artist, "clip_path"), "default": None},
216
+ {"name": "gid", "type": get_type(artist, "gid"), "default": None},
217
+ {"name": "label", "type": get_type(artist, "label"), "default": ""},
218
+ {
219
+ "name": "path_effects",
220
+ "type": get_type(artist, "path_effects"),
221
+ "default": None,
222
+ },
223
+ {"name": "picker", "type": get_type(artist, "picker"), "default": None},
224
+ {"name": "rasterized", "type": get_type(artist, "rasterized"), "default": None},
225
+ {
226
+ "name": "sketch_params",
227
+ "type": get_type(artist, "sketch_params"),
228
+ "default": None,
229
+ },
230
+ {"name": "snap", "type": get_type(artist, "snap"), "default": None},
231
+ {"name": "transform", "type": get_type(artist, "transform"), "default": None},
232
+ {"name": "url", "type": get_type(artist, "url"), "default": None},
233
+ {"name": "visible", "type": get_type(artist, "visible"), "default": True},
234
+ {"name": "zorder", "type": get_type(artist, "zorder"), "default": None},
235
+ ]
236
+
237
+ LINE2D_KWARGS = [
238
+ {"name": "color", "type": get_type(line, "color"), "default": None},
239
+ {"name": "linestyle", "type": get_type(line, "linestyle"), "default": "-"},
240
+ {"name": "linewidth", "type": get_type(line, "linewidth"), "default": None},
241
+ {"name": "marker", "type": get_type(line, "marker"), "default": ""},
242
+ {
243
+ "name": "markeredgecolor",
244
+ "type": get_type(line, "markeredgecolor"),
245
+ "default": None,
246
+ },
247
+ {
248
+ "name": "markeredgewidth",
249
+ "type": get_type(line, "markeredgewidth"),
250
+ "default": None,
251
+ },
252
+ {
253
+ "name": "markerfacecolor",
254
+ "type": get_type(line, "markerfacecolor"),
255
+ "default": None,
256
+ },
257
+ {"name": "markersize", "type": get_type(line, "markersize"), "default": None},
258
+ {"name": "antialiased", "type": get_type(line, "antialiased"), "default": True},
259
+ {
260
+ "name": "dash_capstyle",
261
+ "type": get_type(line, "dash_capstyle"),
262
+ "default": "butt",
263
+ },
264
+ {
265
+ "name": "dash_joinstyle",
266
+ "type": get_type(line, "dash_joinstyle"),
267
+ "default": "round",
268
+ },
269
+ {
270
+ "name": "solid_capstyle",
271
+ "type": get_type(line, "solid_capstyle"),
272
+ "default": "projecting",
273
+ },
274
+ {
275
+ "name": "solid_joinstyle",
276
+ "type": get_type(line, "solid_joinstyle"),
277
+ "default": "round",
278
+ },
279
+ {
280
+ "name": "drawstyle",
281
+ "type": get_type(line, "drawstyle"),
282
+ "default": "default",
283
+ },
284
+ {"name": "fillstyle", "type": get_type(line, "fillstyle"), "default": "full"},
285
+ ]
286
+
287
+ PATCH_KWARGS = [
288
+ {"name": "color", "type": get_type(patch, "color"), "default": None},
289
+ {"name": "edgecolor", "type": get_type(patch, "edgecolor"), "default": None},
290
+ {"name": "facecolor", "type": get_type(patch, "facecolor"), "default": None},
291
+ {"name": "fill", "type": get_type(patch, "fill"), "default": True},
292
+ {"name": "hatch", "type": get_type(patch, "hatch"), "default": None},
293
+ {"name": "linestyle", "type": get_type(patch, "linestyle"), "default": "-"},
294
+ {"name": "linewidth", "type": get_type(patch, "linewidth"), "default": None},
295
+ {
296
+ "name": "antialiased",
297
+ "type": get_type(patch, "antialiased"),
298
+ "default": None,
299
+ },
300
+ {"name": "capstyle", "type": get_type(patch, "capstyle"), "default": "butt"},
301
+ {"name": "joinstyle", "type": get_type(patch, "joinstyle"), "default": "miter"},
302
+ ]
303
+
304
+ TEXT_KWARGS = [
305
+ {"name": "color", "type": get_type(text, "color"), "default": "black"},
306
+ {"name": "fontfamily", "type": get_type(text, "fontfamily"), "default": None},
307
+ {"name": "fontsize", "type": get_type(text, "fontsize"), "default": None},
308
+ {"name": "fontstretch", "type": get_type(text, "fontstretch"), "default": None},
309
+ {"name": "fontstyle", "type": get_type(text, "fontstyle"), "default": "normal"},
310
+ {
311
+ "name": "fontvariant",
312
+ "type": get_type(text, "fontvariant"),
313
+ "default": "normal",
314
+ },
315
+ {
316
+ "name": "fontweight",
317
+ "type": get_type(text, "fontweight"),
318
+ "default": "normal",
319
+ },
320
+ {
321
+ "name": "horizontalalignment",
322
+ "type": get_type(text, "horizontalalignment"),
323
+ "default": "center",
324
+ },
325
+ {
326
+ "name": "verticalalignment",
327
+ "type": get_type(text, "verticalalignment"),
328
+ "default": "center",
329
+ },
330
+ {"name": "rotation", "type": get_type(text, "rotation"), "default": None},
331
+ {"name": "linespacing", "type": get_type(text, "linespacing"), "default": None},
332
+ {
333
+ "name": "multialignment",
334
+ "type": get_type(text, "multialignment"),
335
+ "default": None,
336
+ },
337
+ {"name": "wrap", "type": get_type(text, "wrap"), "default": False},
338
+ ]
339
+
340
+ return ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS
341
+
342
+
343
+ # Build kwargs with types (lazy initialization)
344
+ _KWARGS_CACHE: Optional[Dict[str, List[Dict[str, Any]]]] = None
345
+
346
+
347
+ def _get_kwargs_mapping() -> Dict[str, List[Dict[str, Any]]]:
348
+ """Get kwargs mapping, building it lazily on first call."""
349
+ global _KWARGS_CACHE
350
+ if _KWARGS_CACHE is not None:
351
+ return _KWARGS_CACHE
352
+
353
+ ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS = _build_kwargs_with_types()
354
+
355
+ _KWARGS_CACHE = {
356
+ "plot": LINE2D_KWARGS + ARTIST_KWARGS,
357
+ "scatter": ARTIST_KWARGS,
358
+ "bar": PATCH_KWARGS + ARTIST_KWARGS,
359
+ "barh": PATCH_KWARGS + ARTIST_KWARGS,
360
+ "fill": PATCH_KWARGS + ARTIST_KWARGS,
361
+ "fill_between": PATCH_KWARGS + ARTIST_KWARGS,
362
+ "fill_betweenx": PATCH_KWARGS + ARTIST_KWARGS,
363
+ "step": LINE2D_KWARGS + ARTIST_KWARGS,
364
+ "errorbar": LINE2D_KWARGS + ARTIST_KWARGS,
365
+ "hist": PATCH_KWARGS + ARTIST_KWARGS,
366
+ "hist2d": ARTIST_KWARGS,
367
+ "imshow": ARTIST_KWARGS,
368
+ "pcolor": ARTIST_KWARGS,
369
+ "pcolormesh": ARTIST_KWARGS,
370
+ "pcolorfast": ARTIST_KWARGS,
371
+ "contour": ARTIST_KWARGS,
372
+ "contourf": ARTIST_KWARGS,
373
+ "hexbin": ARTIST_KWARGS,
374
+ "quiver": ARTIST_KWARGS,
375
+ "barbs": ARTIST_KWARGS,
376
+ "specgram": ARTIST_KWARGS,
377
+ "psd": LINE2D_KWARGS + ARTIST_KWARGS,
378
+ "csd": LINE2D_KWARGS + ARTIST_KWARGS,
379
+ "cohere": LINE2D_KWARGS + ARTIST_KWARGS,
380
+ "acorr": LINE2D_KWARGS + ARTIST_KWARGS,
381
+ "xcorr": LINE2D_KWARGS + ARTIST_KWARGS,
382
+ "angle_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
383
+ "magnitude_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
384
+ "phase_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
385
+ "stackplot": PATCH_KWARGS + ARTIST_KWARGS,
386
+ "stairs": PATCH_KWARGS + ARTIST_KWARGS,
387
+ "eventplot": ARTIST_KWARGS,
388
+ "broken_barh": PATCH_KWARGS + ARTIST_KWARGS,
389
+ "loglog": LINE2D_KWARGS + ARTIST_KWARGS,
390
+ "semilogx": LINE2D_KWARGS + ARTIST_KWARGS,
391
+ "semilogy": LINE2D_KWARGS + ARTIST_KWARGS,
392
+ "annotate": TEXT_KWARGS + ARTIST_KWARGS,
393
+ "text": TEXT_KWARGS + ARTIST_KWARGS,
394
+ "arrow": PATCH_KWARGS + ARTIST_KWARGS,
395
+ "axhline": LINE2D_KWARGS + ARTIST_KWARGS,
396
+ "axvline": LINE2D_KWARGS + ARTIST_KWARGS,
397
+ "hlines": ARTIST_KWARGS,
398
+ "vlines": ARTIST_KWARGS,
399
+ "axhspan": PATCH_KWARGS + ARTIST_KWARGS,
400
+ "axvspan": PATCH_KWARGS + ARTIST_KWARGS,
401
+ "axline": LINE2D_KWARGS + ARTIST_KWARGS,
402
+ "legend": ARTIST_KWARGS,
403
+ "grid": LINE2D_KWARGS + ARTIST_KWARGS,
404
+ "table": ARTIST_KWARGS,
405
+ "clabel": TEXT_KWARGS + ARTIST_KWARGS,
406
+ "bar_label": TEXT_KWARGS + ARTIST_KWARGS,
407
+ "quiverkey": ARTIST_KWARGS,
408
+ "ecdf": LINE2D_KWARGS + ARTIST_KWARGS,
409
+ "tricontour": ARTIST_KWARGS,
410
+ "tricontourf": ARTIST_KWARGS,
411
+ "tripcolor": ARTIST_KWARGS,
412
+ "triplot": LINE2D_KWARGS + ARTIST_KWARGS,
413
+ "matshow": ARTIST_KWARGS,
414
+ "spy": ARTIST_KWARGS + LINE2D_KWARGS,
415
+ "boxplot": ARTIST_KWARGS,
416
+ "violinplot": ARTIST_KWARGS,
417
+ "pie": PATCH_KWARGS + ARTIST_KWARGS,
418
+ "stem": LINE2D_KWARGS + ARTIST_KWARGS,
419
+ }
420
+
421
+ return _KWARGS_CACHE
422
+
423
+
424
+ # -----------------------------------------------------------------------------
425
+ # Type helpers
426
+ # -----------------------------------------------------------------------------
427
+ def _get_type_str(annotation) -> Optional[str]:
428
+ """Convert annotation to string."""
429
+ if annotation is inspect.Parameter.empty:
430
+ return None
431
+ if hasattr(annotation, "__name__"):
432
+ return annotation.__name__
433
+ return str(annotation)
434
+
435
+
436
+ def _serialize_default(default) -> Any:
437
+ """Serialize default value."""
438
+ if default is inspect.Parameter.empty:
439
+ return None
440
+ if callable(default):
441
+ return f"<{type(default).__name__}>"
442
+ try:
443
+ import json
444
+
445
+ json.dumps(default)
446
+ return default
447
+ except (TypeError, ValueError):
448
+ return repr(default)
449
+
450
+
451
+ # -----------------------------------------------------------------------------
452
+ # Main API
453
+ # -----------------------------------------------------------------------------
454
+ def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any]:
455
+ """Get signature for a matplotlib Axes method with deep inspection.
17
456
 
18
457
  Parameters
19
458
  ----------
20
459
  method_name : str
21
460
  Name of the method (e.g., 'plot', 'scatter').
461
+ expand_kwargs : bool
462
+ If True, expand **kwargs to actual parameters.
22
463
 
23
464
  Returns
24
465
  -------
25
466
  dict
26
467
  Signature information with 'args' and 'kwargs' keys.
468
+ Args and kwargs are expanded from docstrings when possible.
27
469
  """
28
- if method_name in _SIGNATURE_CACHE:
29
- return _SIGNATURE_CACHE[method_name]
470
+ cache_key = f"{method_name}_{expand_kwargs}"
471
+ if cache_key in _SIGNATURE_CACHE:
472
+ return _SIGNATURE_CACHE[cache_key]
30
473
 
31
474
  # Create a temporary axes to introspect
32
475
  fig, ax = plt.subplots()
@@ -41,58 +484,76 @@ def get_signature(method_name: str) -> Dict[str, Any]:
41
484
  except (ValueError, TypeError):
42
485
  return {"args": [], "kwargs": {}}
43
486
 
487
+ # Parse parameter types from docstring
488
+ param_types = _parse_parameter_types(method.__doc__)
489
+
44
490
  args = []
45
491
  kwargs = {}
492
+ has_var_positional = False
493
+ has_var_keyword = False
46
494
 
47
495
  for name, param in sig.parameters.items():
48
496
  if name == "self":
49
497
  continue
50
498
 
51
499
  if param.kind == inspect.Parameter.VAR_POSITIONAL:
52
- args.append({"name": f"*{name}", "type": "*args"})
500
+ has_var_positional = True
53
501
  elif param.kind == inspect.Parameter.VAR_KEYWORD:
54
- kwargs["**kwargs"] = {"type": "**kwargs"}
55
- elif param.default is inspect.Parameter.empty:
56
- # Positional argument
57
- args.append({
58
- "name": name,
59
- "type": _get_type_str(param.annotation),
60
- })
502
+ has_var_keyword = True
503
+ else:
504
+ # Try annotation first, then docstring
505
+ typehint = _get_type_str(param.annotation)
506
+ if not typehint:
507
+ typehint = param_types.get(name.lower())
508
+
509
+ if param.default is inspect.Parameter.empty:
510
+ # Positional argument
511
+ args.append(
512
+ {
513
+ "name": name,
514
+ "type": typehint,
515
+ }
516
+ )
517
+ else:
518
+ # Keyword argument with default
519
+ kwargs[name] = {
520
+ "type": typehint,
521
+ "default": _serialize_default(param.default),
522
+ }
523
+
524
+ # Expand *args from docstring
525
+ if has_var_positional:
526
+ docstring_args = _extract_args_from_docstring(method.__doc__, method_name)
527
+ if docstring_args:
528
+ # Insert flattened args at the beginning
529
+ for i, arg in enumerate(docstring_args):
530
+ args.insert(i, arg)
531
+ else:
532
+ # No docstring info, keep generic *args
533
+ args.insert(0, {"name": "*args", "type": "*args"})
534
+
535
+ # Expand **kwargs based on function type
536
+ if has_var_keyword and expand_kwargs:
537
+ kwargs_mapping = _get_kwargs_mapping()
538
+ if method_name in kwargs_mapping:
539
+ expanded_kwargs = kwargs_mapping[method_name]
540
+ existing_names = {p["name"] for p in args} | set(kwargs.keys())
541
+ for kwarg in expanded_kwargs:
542
+ if kwarg["name"] not in existing_names:
543
+ kwargs[kwarg["name"]] = {
544
+ "type": kwarg["type"],
545
+ "default": kwarg["default"],
546
+ }
61
547
  else:
62
- # Keyword argument with default
63
- kwargs[name] = {
64
- "type": _get_type_str(param.annotation),
65
- "default": _serialize_default(param.default),
66
- }
548
+ kwargs["**kwargs"] = {"type": "**kwargs"}
549
+ elif has_var_keyword:
550
+ kwargs["**kwargs"] = {"type": "**kwargs"}
67
551
 
68
552
  result = {"args": args, "kwargs": kwargs}
69
- _SIGNATURE_CACHE[method_name] = result
553
+ _SIGNATURE_CACHE[cache_key] = result
70
554
  return result
71
555
 
72
556
 
73
- def _get_type_str(annotation) -> Optional[str]:
74
- """Convert annotation to string."""
75
- if annotation is inspect.Parameter.empty:
76
- return None
77
- if hasattr(annotation, "__name__"):
78
- return annotation.__name__
79
- return str(annotation)
80
-
81
-
82
- def _serialize_default(default) -> Any:
83
- """Serialize default value."""
84
- if default is inspect.Parameter.empty:
85
- return None
86
- if callable(default):
87
- return f"<{type(default).__name__}>"
88
- try:
89
- import json
90
- json.dumps(default)
91
- return default
92
- except (TypeError, ValueError):
93
- return repr(default)
94
-
95
-
96
557
  def get_defaults(method_name: str) -> Dict[str, Any]:
97
558
  """Get default values for a method's kwargs.
98
559
 
@@ -137,7 +598,7 @@ def validate_kwargs(
137
598
  sig = get_signature(method_name)
138
599
  known_kwargs = set(sig.get("kwargs", {}).keys()) - {"**kwargs"}
139
600
 
140
- # If method accepts **kwargs, all are valid
601
+ # If method accepts **kwargs and we haven't expanded it, all are valid
141
602
  if "**kwargs" in sig.get("kwargs", {}):
142
603
  return {
143
604
  "valid": list(kwargs.keys()),
@@ -164,23 +625,21 @@ def validate_kwargs(
164
625
  def list_plotting_methods() -> List[str]:
165
626
  """List all available plotting methods.
166
627
 
628
+ Uses _params.PLOTTING_METHODS as single source of truth,
629
+ filtered to methods that actually exist on the current matplotlib version.
630
+
167
631
  Returns
168
632
  -------
169
633
  list
170
634
  Names of plotting methods.
171
635
  """
636
+ from .._params import PLOTTING_METHODS
637
+
172
638
  fig, ax = plt.subplots()
173
639
  plt.close(fig)
174
640
 
175
- # Common plotting methods
176
- methods = [
177
- "plot", "scatter", "bar", "barh", "hist", "hist2d",
178
- "boxplot", "violinplot", "pie", "errorbar", "fill",
179
- "fill_between", "fill_betweenx", "stackplot", "stem",
180
- "step", "imshow", "pcolor", "pcolormesh", "contour",
181
- "contourf", "quiver", "barbs", "streamplot", "hexbin",
182
- "eventplot", "stairs", "ecdf",
183
- ]
641
+ # Use _params.PLOTTING_METHODS as single source of truth
642
+ return sorted([m for m in PLOTTING_METHODS if hasattr(ax, m)])
643
+
184
644
 
185
- # Filter to only those that exist
186
- return [m for m in methods if hasattr(ax, m)]
645
+ # EOF
@@ -2,9 +2,9 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Utility modules for figrecipe."""
4
4
 
5
- from ._numpy_io import load_array, save_array
6
5
  from ._diff import get_non_default_kwargs, is_default_value
7
- from ._units import mm_to_inch, inch_to_mm, mm_to_pt, pt_to_mm
6
+ from ._numpy_io import load_array, save_array
7
+ from ._units import inch_to_mm, mm_to_inch, mm_to_pt, pt_to_mm
8
8
 
9
9
  __all__ = [
10
10
  "save_array",
@@ -19,14 +19,16 @@ __all__ = [
19
19
 
20
20
  # Optional: image comparison (requires PIL)
21
21
  try:
22
- from ._image_diff import compare_images, create_comparison_figure
22
+ from ._image_diff import compare_images, create_comparison_figure # noqa: F401
23
+
23
24
  __all__.extend(["compare_images", "create_comparison_figure"])
24
25
  except ImportError:
25
26
  pass
26
27
 
27
28
  # Optional: crop utility (requires PIL)
28
29
  try:
29
- from ._crop import crop, find_content_area
30
+ from ._crop import crop, find_content_area # noqa: F401
31
+
30
32
  __all__.extend(["crop", "find_content_area"])
31
33
  except ImportError:
32
34
  pass