figrecipe 0.6.0__py3-none-any.whl → 0.7.4__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 (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -9,421 +9,17 @@ Parses *args/**kwargs from docstrings and expands them to actual parameters.
9
9
  """
10
10
 
11
11
  import inspect
12
- import re
13
12
  from typing import Any, Dict, List, Optional
14
13
 
15
14
  import matplotlib.pyplot as plt
16
15
 
16
+ from ._kwargs import get_kwargs_mapping
17
+ from ._parsing import extract_args_from_docstring, parse_parameter_types
18
+
17
19
  # Cache for signatures
18
20
  _SIGNATURE_CACHE: Dict[str, Dict[str, Any]] = {}
19
21
 
20
22
 
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
23
  def _get_type_str(annotation) -> Optional[str]:
428
24
  """Convert annotation to string."""
429
25
  if annotation is inspect.Parameter.empty:
@@ -448,9 +44,6 @@ def _serialize_default(default) -> Any:
448
44
  return repr(default)
449
45
 
450
46
 
451
- # -----------------------------------------------------------------------------
452
- # Main API
453
- # -----------------------------------------------------------------------------
454
47
  def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any]:
455
48
  """Get signature for a matplotlib Axes method with deep inspection.
456
49
 
@@ -485,7 +78,7 @@ def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any
485
78
  return {"args": [], "kwargs": {}}
486
79
 
487
80
  # Parse parameter types from docstring
488
- param_types = _parse_parameter_types(method.__doc__)
81
+ param_types = parse_parameter_types(method.__doc__)
489
82
 
490
83
  args = []
491
84
  kwargs = {}
@@ -506,35 +99,42 @@ def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any
506
99
  if not typehint:
507
100
  typehint = param_types.get(name.lower())
508
101
 
509
- if param.default is inspect.Parameter.empty:
510
- # Positional argument
102
+ # Handle POSITIONAL_OR_KEYWORD and POSITIONAL_ONLY parameters
103
+ if param.kind in (
104
+ inspect.Parameter.POSITIONAL_ONLY,
105
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
106
+ ):
511
107
  args.append(
512
108
  {
513
109
  "name": name,
514
110
  "type": typehint,
515
111
  }
516
112
  )
517
- else:
518
- # Keyword argument with default
113
+ if param.default is not inspect.Parameter.empty:
114
+ kwargs[name] = {
115
+ "type": typehint,
116
+ "default": _serialize_default(param.default),
117
+ }
118
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
519
119
  kwargs[name] = {
520
120
  "type": typehint,
521
- "default": _serialize_default(param.default),
121
+ "default": _serialize_default(param.default)
122
+ if param.default is not inspect.Parameter.empty
123
+ else None,
522
124
  }
523
125
 
524
126
  # Expand *args from docstring
525
127
  if has_var_positional:
526
- docstring_args = _extract_args_from_docstring(method.__doc__, method_name)
128
+ docstring_args = extract_args_from_docstring(method.__doc__, method_name)
527
129
  if docstring_args:
528
- # Insert flattened args at the beginning
529
130
  for i, arg in enumerate(docstring_args):
530
131
  args.insert(i, arg)
531
132
  else:
532
- # No docstring info, keep generic *args
533
133
  args.insert(0, {"name": "*args", "type": "*args"})
534
134
 
535
135
  # Expand **kwargs based on function type
536
136
  if has_var_keyword and expand_kwargs:
537
- kwargs_mapping = _get_kwargs_mapping()
137
+ kwargs_mapping = get_kwargs_mapping()
538
138
  if method_name in kwargs_mapping:
539
139
  expanded_kwargs = kwargs_mapping[method_name]
540
140
  existing_names = {p["name"] for p in args} | set(kwargs.keys())
@@ -598,7 +198,6 @@ def validate_kwargs(
598
198
  sig = get_signature(method_name)
599
199
  known_kwargs = set(sig.get("kwargs", {}).keys()) - {"**kwargs"}
600
200
 
601
- # If method accepts **kwargs and we haven't expanded it, all are valid
602
201
  if "**kwargs" in sig.get("kwargs", {}):
603
202
  return {
604
203
  "valid": list(kwargs.keys()),
@@ -618,7 +217,7 @@ def validate_kwargs(
618
217
  return {
619
218
  "valid": valid,
620
219
  "unknown": unknown,
621
- "missing": [], # Not checking required kwargs for now
220
+ "missing": [],
622
221
  }
623
222
 
624
223
 
@@ -638,7 +237,6 @@ def list_plotting_methods() -> List[str]:
638
237
  fig, ax = plt.subplots()
639
238
  plt.close(fig)
640
239
 
641
- # Use _params.PLOTTING_METHODS as single source of truth
642
240
  return sorted([m for m in PLOTTING_METHODS if hasattr(ax, m)])
643
241
 
644
242
 
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Docstring parsing utilities for matplotlib signature extraction."""
4
+
5
+ import re
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def parse_parameter_types(docstring: Optional[str]) -> Dict[str, str]:
10
+ """Extract parameter types from NumPy-style docstring Parameters section."""
11
+ if not docstring:
12
+ return {}
13
+
14
+ types = {}
15
+
16
+ # Find Parameters section
17
+ params_match = re.search(
18
+ r"Parameters\s*[-]+\s*(.*?)(?:\n\s*Returns|\n\s*See Also|\n\s*Notes|\n\s*Examples|\n\s*Other Parameters|\Z)",
19
+ docstring,
20
+ re.DOTALL,
21
+ )
22
+ if not params_match:
23
+ return {}
24
+
25
+ params_text = params_match.group(1)
26
+
27
+ # Parse lines like "x, y : array-like or float" or "fmt : str, optional"
28
+ for match in re.finditer(
29
+ r"^(\w+(?:\s*,\s*\w+)*)\s*:\s*(.+?)(?=\n\s*\n|\n\w+\s*:|\Z)",
30
+ params_text,
31
+ re.MULTILINE | re.DOTALL,
32
+ ):
33
+ names_str = match.group(1)
34
+ type_str = match.group(2).split("\n")[0].strip()
35
+
36
+ # Clean up type string
37
+ type_str = re.sub(r",?\s*optional\s*$", "", type_str).strip()
38
+ type_str = re.sub(
39
+ r",?\s*default[^,]*$", "", type_str, flags=re.IGNORECASE
40
+ ).strip()
41
+
42
+ # Handle multiple names like "x, y"
43
+ for name in re.split(r"\s*,\s*", names_str):
44
+ name = name.strip()
45
+ if name:
46
+ types[name.lower()] = type_str
47
+
48
+ return types
49
+
50
+
51
+ def parse_args_pattern(
52
+ args_str: str, param_types: Dict[str, str]
53
+ ) -> List[Dict[str, Any]]:
54
+ """Parse args pattern like '[x], y, [fmt]' into list of arg dicts."""
55
+ if not args_str:
56
+ return []
57
+
58
+ args = []
59
+ parts = re.split(r",\s*", args_str)
60
+
61
+ for part in parts:
62
+ part = part.strip()
63
+ if not part or part == "/":
64
+ continue
65
+
66
+ optional = part.startswith("[") and part.endswith("]")
67
+ if optional:
68
+ name = part[1:-1].strip()
69
+ else:
70
+ name = part.strip("[]").strip()
71
+
72
+ if name and name not in ("...", "*"):
73
+ type_str = param_types.get(name.lower())
74
+ args.append(
75
+ {
76
+ "name": name,
77
+ "type": type_str,
78
+ "optional": optional,
79
+ }
80
+ )
81
+
82
+ return args
83
+
84
+
85
+ # Manual *args patterns for functions without parseable call signatures
86
+ MANUAL_ARGS_PATTERNS = {
87
+ "fill": "[x], y, [color]",
88
+ "stackplot": "x, *ys",
89
+ "legend": "[handles], [labels]",
90
+ "stem": "[locs], heads",
91
+ "tricontour": "[triangulation], x, y, z, [levels]",
92
+ "tricontourf": "[triangulation], x, y, z, [levels]",
93
+ "triplot": "[triangulation], x, y, [triangles]",
94
+ "loglog": "[x], y, [fmt]",
95
+ "semilogx": "[x], y, [fmt]",
96
+ "semilogy": "[x], y, [fmt]",
97
+ "barbs": "[X], [Y], U, V, [C]",
98
+ "quiver": "[X], [Y], U, V, [C]",
99
+ "pcolor": "[X], [Y], C",
100
+ "pcolormesh": "[X], [Y], C",
101
+ "pcolorfast": "[X], [Y], C",
102
+ "acorr": "x",
103
+ "xcorr": "x, y",
104
+ "plot": "[x], y, [fmt]",
105
+ }
106
+
107
+
108
+ def extract_args_from_docstring(
109
+ docstring: Optional[str], func_name: str = ""
110
+ ) -> List[Dict[str, Any]]:
111
+ """Extract *args as flattened list from docstring call signature."""
112
+ if not docstring:
113
+ return []
114
+
115
+ # First, parse parameter types
116
+ param_types = parse_parameter_types(docstring)
117
+
118
+ # Check for manual pattern first
119
+ if func_name in MANUAL_ARGS_PATTERNS:
120
+ return parse_args_pattern(MANUAL_ARGS_PATTERNS[func_name], param_types)
121
+
122
+ # Look for "Call signature:" patterns
123
+ patterns = [
124
+ r"Call signatures?::\s*\n\s*(.*?)(?:\n\n|\n[A-Z])",
125
+ r"^\s*(\w+\([^)]+\))\s*$",
126
+ ]
127
+
128
+ for pattern in patterns:
129
+ match = re.search(pattern, docstring, re.MULTILINE | re.DOTALL)
130
+ if match:
131
+ sig_text = match.group(1).strip()
132
+ first_line = sig_text.split("\n")[0].strip()
133
+ inner_match = re.search(r"\(([^*]+?)(?:,\s*\*|,\s*data|\))", first_line)
134
+ if inner_match:
135
+ args_str = inner_match.group(1).strip().rstrip(",")
136
+ return parse_args_pattern(args_str, param_types)
137
+ return []
138
+
139
+
140
+ __all__ = [
141
+ "parse_parameter_types",
142
+ "parse_args_pattern",
143
+ "extract_args_from_docstring",
144
+ "MANUAL_ARGS_PATTERNS",
145
+ ]
146
+
147
+ # EOF