diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index b79d3cc62338..9e18f2615f57 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -15,8 +15,15 @@ from .colors import BoundaryNorm from .cm import ScalarMappable from .path import Path -from .transforms import (BboxBase, Bbox, IdentityTransform, Transform, TransformedBbox, - TransformedPatchPath, TransformedPath) +from .transforms import ( + BboxBase, + Bbox, + IdentityTransform, + Transform, + TransformedBbox, + TransformedPatchPath, + TransformedPath, +) _log = logging.getLogger(__name__) @@ -75,8 +82,11 @@ def draw_wrapper(artist, renderer): renderer.stop_filter(artist.get_agg_filter()) if artist.get_rasterized(): renderer._raster_depth -= 1 - if (renderer._rasterizing and artist.figure and - artist.figure.suppressComposite): + if ( + renderer._rasterizing + and artist.figure + and artist.figure.suppressComposite + ): # restart rasterizing to prevent merging renderer.stop_rasterizing() renderer.start_rasterizing() @@ -90,6 +100,7 @@ def _finalize_rasterization(draw): Decorator for Artist.draw method. Needed on the outermost artist, i.e. Figure, to finish up if the render is still in rasterized mode. """ + @wraps(draw) def draw_wrapper(artist, renderer, *args, **kwargs): result = draw(artist, renderer, *args, **kwargs) @@ -97,6 +108,7 @@ def draw_wrapper(artist, renderer, *args, **kwargs): renderer.stop_rasterizing() renderer._rasterizing = False return result + return draw_wrapper @@ -111,6 +123,8 @@ def _stale_axes_callback(self, val): class _Unset: def __repr__(self): return "" + + _UNSET = _Unset() @@ -124,7 +138,6 @@ class Artist: zorder = 0 def __init_subclass__(cls): - # Decorate draw() method so that all artists are able to stop # rastrization when necessary. If the artist's draw method is already # decorated (has a `_supports_rasterization` attribute), it won't be @@ -136,7 +149,7 @@ def __init_subclass__(cls): # Inject custom set() methods into the subclass with signature and # docstring based on the subclasses' properties. - if not hasattr(cls.set, '_autogenerated_signature'): + if not hasattr(cls.set, "_autogenerated_signature"): # Don't overwrite cls.set if the subclass or one of its parents # has defined a set method set itself. # If there was no explicit definition, cls.set is inherited from @@ -150,10 +163,10 @@ def __init_subclass__(cls): cls._update_set_signature_and_docstring() _PROPERTIES_EXCLUDED_FROM_SET = [ - 'navigate_mode', # not a user-facing function - 'figure', # changing the figure is such a profound operation - # that we don't want this in set() - '3d_properties', # cannot be used as a keyword due to leading digit + "navigate_mode", # not a user-facing function + "figure", # changing the figure is such a profound operation + # that we don't want this in set() + "3d_properties", # cannot be used as a keyword due to leading digit ] @classmethod @@ -166,16 +179,21 @@ def _update_set_signature_and_docstring(cls): are still accepted as keyword arguments. """ cls.set.__signature__ = Signature( - [Parameter("self", Parameter.POSITIONAL_OR_KEYWORD), - *[Parameter(prop, Parameter.KEYWORD_ONLY, default=_UNSET) - for prop in ArtistInspector(cls).get_setters() - if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET]]) + [ + Parameter("self", Parameter.POSITIONAL_OR_KEYWORD), + *[ + Parameter(prop, Parameter.KEYWORD_ONLY, default=_UNSET) + for prop in ArtistInspector(cls).get_setters() + if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET + ], + ] + ) cls.set._autogenerated_signature = True cls.set.__doc__ = ( "Set multiple properties at once.\n\n" - "Supported properties are\n\n" - + kwdoc(cls)) + "Supported properties are\n\n" + kwdoc(cls) + ) def __init__(self): self._stale = True @@ -191,7 +209,7 @@ def __init__(self): self.clipbox = None self._clippath = None self._clipon = True - self._label = '' + self._label = "" self._picker = None self._rasterized = False self._agg_filter = None @@ -208,14 +226,14 @@ def __init__(self): self._url = None self._gid = None self._snap = None - self._sketch = mpl.rcParams['path.sketch'] - self._path_effects = mpl.rcParams['path.effects'] + self._sketch = mpl.rcParams["path.sketch"] + self._path_effects = mpl.rcParams["path.effects"] self._sticky_edges = _XYPair([], []) self._in_layout = True def __getstate__(self): d = self.__dict__.copy() - d['stale_callback'] = None + d["stale_callback"] = None return d def remove(self): @@ -241,7 +259,7 @@ def remove(self): # clear stale callback self.stale_callback = None _ax_flag = False - if hasattr(self, 'axes') and self.axes: + if hasattr(self, "axes") and self.axes: # remove from the mouse hit list self.axes._mouseover_set.discard(self) self.axes.stale = True @@ -254,7 +272,7 @@ def remove(self): self.figure = None else: - raise NotImplementedError('cannot remove artist') + raise NotImplementedError("cannot remove artist") # TODO: the fix for the collections relim problem is to move the # limits calculation into the artist itself, including the property of # whether or not the artist should affect the limits. Then there will @@ -273,7 +291,7 @@ def convert_xunits(self, x): If the artist is not contained in an Axes or if the xaxis does not have units, *x* itself is returned. """ - ax = getattr(self, 'axes', None) + ax = getattr(self, "axes", None) if ax is None or ax.xaxis is None: return x return ax.xaxis.convert_units(x) @@ -285,7 +303,7 @@ def convert_yunits(self, y): If the artist is not contained in an Axes or if the yaxis does not have units, *y* itself is returned. """ - ax = getattr(self, 'axes', None) + ax = getattr(self, "axes", None) if ax is None or ax.yaxis is None: return y return ax.yaxis.convert_units(y) @@ -297,10 +315,11 @@ def axes(self): @axes.setter def axes(self, new_axes): - if (new_axes is not None and self._axes is not None - and new_axes != self._axes): - raise ValueError("Can not reset the Axes. You are probably trying to reuse " - "an artist in more than one Axes which is not supported") + if new_axes is not None and self._axes is not None and new_axes != self._axes: + raise ValueError( + "Can not reset the Axes. You are probably trying to reuse " + "an artist in more than one Axes which is not supported" + ) self._axes = new_axes if new_axes is not None and new_axes is not self: self.stale_callback = _stale_axes_callback @@ -450,8 +469,9 @@ def get_transform(self): """Return the `.Transform` instance used by this artist.""" if self._transform is None: self._transform = IdentityTransform() - elif (not isinstance(self._transform, Transform) - and hasattr(self._transform, '_as_mpl_transform')): + elif not isinstance(self._transform, Transform) and hasattr( + self._transform, "_as_mpl_transform" + ): self._transform = self._transform._as_mpl_transform(self.axes) return self._transform @@ -473,8 +493,11 @@ def _different_canvas(self, event): return False, {} # subclass-specific implementation follows """ - return (getattr(event, "canvas", None) is not None and self.figure is not None - and event.canvas is not self.figure.canvas) + return ( + getattr(event, "canvas", None) is not None + and self.figure is not None + and event.canvas is not self.figure.canvas + ) def contains(self, mouseevent): """ @@ -521,6 +544,7 @@ def pick(self, mouseevent): .Artist.set_picker, .Artist.get_picker, .Artist.pickable """ from .backend_bases import PickEvent # Circular import. + # Pick self if self.pickable(): picker = self.get_picker() @@ -529,16 +553,20 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - PickEvent("pick_event", self.figure.canvas, - mouseevent, self, **prop)._process() + PickEvent( + "pick_event", self.figure.canvas, mouseevent, self, **prop + )._process() # Pick children for a in self.get_children(): # make sure the event happened in the same Axes - ax = getattr(a, 'axes', None) - if (isinstance(a, mpl.figure.SubFigure) - or mouseevent.inaxes is None or ax is None - or mouseevent.inaxes == ax): + ax = getattr(a, "axes", None) + if ( + isinstance(a, mpl.figure.SubFigure) + or mouseevent.inaxes is None + or ax is None + or mouseevent.inaxes == ax + ): # we need to check if mouseevent.inaxes is None # because some objects associated with an Axes (e.g., a # tick label) can be outside the bounding box of the @@ -628,7 +656,7 @@ def get_snap(self): See `.set_snap` for details. """ - if mpl.rcParams['path.snap']: + if mpl.rcParams["path.snap"]: return self._snap else: return False @@ -744,8 +772,7 @@ def set_figure(self, fig): # is not allowed for the same reason as adding the same instance # to more than one Axes if self.figure is not None: - raise RuntimeError("Can not put single artist in " - "more than one figure") + raise RuntimeError("Can not put single artist in more than one figure") self.figure = fig if self.figure and self.figure is not self: self.pchanged() @@ -799,8 +826,7 @@ def set_clip_path(self, path, transform=None): success = False if transform is None: if isinstance(path, Rectangle): - self.clipbox = TransformedBbox(Bbox.unit(), - path.get_transform()) + self.clipbox = TransformedBbox(Bbox.unit(), path.get_transform()) self._clippath = None success = True elif isinstance(path, Patch): @@ -825,7 +851,8 @@ def set_clip_path(self, path, transform=None): if not success: raise TypeError( "Invalid arguments to set_clip_path, of type " - f"{type(path).__name__} and {type(transform).__name__}") + f"{type(path).__name__} and {type(transform).__name__}" + ) # This may result in the callbacks being hit twice, but guarantees they # will be hit at least once. self.pchanged() @@ -872,14 +899,17 @@ def _fully_clipped_to_axes(self): # to determine whether ``axes.get_tightbbox()`` may bypass drawing clip_box = self.get_clip_box() clip_path = self.get_clip_path() - return (self.axes is not None - and self.get_clip_on() - and (clip_box is not None or clip_path is not None) - and (clip_box is None - or np.all(clip_box.extents == self.axes.bbox.extents)) - and (clip_path is None - or isinstance(clip_path, TransformedPatchPath) - and clip_path._patch is self.axes.patch)) + return ( + self.axes is not None + and self.get_clip_on() + and (clip_box is not None or clip_path is not None) + and (clip_box is None or np.all(clip_box.extents == self.axes.bbox.extents)) + and ( + clip_path is None + or isinstance(clip_path, TransformedPatchPath) + and clip_path._patch is self.axes.patch + ) + ) def get_clip_on(self): """Return whether the artist uses clipping.""" @@ -950,8 +980,7 @@ def set_rasterized(self, rasterized): ---------- rasterized : bool """ - supports_rasterization = getattr(self.draw, - "_supports_rasterization", False) + supports_rasterization = getattr(self.draw, "_supports_rasterization", False) if rasterized and not supports_rasterization: _api.warn_external(f"Rasterization of '{self}' will be ignored") @@ -1008,10 +1037,9 @@ def set_alpha(self, alpha): *alpha* must be within the 0-1 range, inclusive. """ if alpha is not None and not isinstance(alpha, Real): - raise TypeError( - f'alpha must be numeric or None, not {type(alpha)}') + raise TypeError(f"alpha must be numeric or None, not {type(alpha)}") if alpha is not None and not (0 <= alpha <= 1): - raise ValueError(f'alpha ({alpha}) is outside 0-1 range') + raise ValueError(f"alpha ({alpha}) is outside 0-1 range") if alpha != self._alpha: self._alpha = alpha self.pchanged() @@ -1034,8 +1062,10 @@ def _set_alpha_for_array(self, alpha): return alpha = np.asarray(alpha) if not (0 <= alpha.min() and alpha.max() <= 1): - raise ValueError('alpha must be between 0 and 1, inclusive, ' - f'but min is {alpha.min()}, max is {alpha.max()}') + raise ValueError( + "alpha must be between 0 and 1, inclusive, " + f"but min is {alpha.min()}, max is {alpha.max()}" + ) self._alpha = alpha self.pchanged() self.stale = True @@ -1190,8 +1220,7 @@ def _update_props(self, props, errfmt): else: func = getattr(self, f"set_{k}", None) if not callable(func): - raise AttributeError( - errfmt.format(cls=type(self), prop_name=k)) + raise AttributeError(errfmt.format(cls=type(self), prop_name=k)) ret.append(func(v)) if ret: self.pchanged() @@ -1207,7 +1236,8 @@ def update(self, props): props : dict """ return self._update_props( - props, "{cls.__name__!r} object has no property {prop_name!r}") + props, "{cls.__name__!r} object has no property {prop_name!r}" + ) def _internal_update(self, kwargs): """ @@ -1217,8 +1247,9 @@ def _internal_update(self, kwargs): The lack of prenormalization is to maintain backcompatibility. """ return self._update_props( - kwargs, "{cls.__name__}.set() got an unexpected keyword argument " - "{prop_name!r}") + kwargs, + "{cls.__name__}.set() got an unexpected keyword argument {prop_name!r}", + ) def set(self, **kwargs): # docstring and signature are auto-generated via @@ -1265,16 +1296,19 @@ def findobj(self, match=None, include_self=True): """ if match is None: # always return True + def matchfunc(x): return True elif isinstance(match, type) and issubclass(match, Artist): + def matchfunc(x): return isinstance(x, match) elif callable(match): matchfunc = match else: - raise ValueError('match must be None, a matplotlib.artist.Artist ' - 'subclass, or a callable') + raise ValueError( + "match must be None, a matplotlib.artist.Artist subclass, or a callable" + ) artists = sum([c.findobj(matchfunc) for c in self.get_children()], []) if include_self and matchfunc(self): @@ -1346,14 +1380,13 @@ def format_cursor_data(self, data): cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) neigh_idx = max(0, cur_idx - 1) # use max diff to prevent delta == 0 - delta = np.diff( - self.norm.boundaries[neigh_idx:cur_idx + 2] - ).max() + delta = np.diff(self.norm.boundaries[neigh_idx : cur_idx + 2]).max() else: # Midpoints of neighboring color intervals. neighbors = self.norm.inverse( - (int(normed * n) + np.array([0, 1])) / n) + (int(normed * n) + np.array([0, 1])) / n + ) delta = abs(neighbors - data).max() g_sig_digits = cbook._g_sig_digits(data, delta) else: @@ -1364,8 +1397,9 @@ def format_cursor_data(self, data): data[0] except (TypeError, IndexError): data = [data] - data_str = ', '.join(f'{item:0.3g}' for item in data - if isinstance(item, Number)) + data_str = ", ".join( + f"{item:0.3g}" for item in data if isinstance(item, Number) + ) return "[" + data_str + "]" def get_mouseover(self): @@ -1450,16 +1484,20 @@ def get_aliases(self): 'linewidth' : {'lw'}, } """ - names = [name for name in dir(self.o) - if name.startswith(('set_', 'get_')) - and callable(getattr(self.o, name))] + names = [ + name + for name in dir(self.o) + if name.startswith(("set_", "get_")) and callable(getattr(self.o, name)) + ] aliases = {} for name in names: func = getattr(self.o, name) if not self.is_alias(func): continue - propname = re.search(f"`({name[:4]}.*)`", # get_.*/set_.* - inspect.getdoc(func)).group(1) + propname = re.search( + f"`({name[:4]}.*)`", # get_.*/set_.* + inspect.getdoc(func), + ).group(1) aliases.setdefault(propname[4:], set()).add(name[4:]) return aliases @@ -1476,19 +1514,19 @@ def get_valid_values(self, attr): numpydoc-style documentation for the setter's first argument. """ - name = 'set_%s' % attr + name = "set_%s" % attr if not hasattr(self.o, name): - raise AttributeError(f'{self.o} has no function {name}') + raise AttributeError(f"{self.o} has no function {name}") func = getattr(self.o, name) - if hasattr(func, '_kwarg_doc'): + if hasattr(func, "_kwarg_doc"): return func._kwarg_doc docstring = inspect.getdoc(func) if docstring is None: - return 'unknown' + return "unknown" - if docstring.startswith('Alias for '): + if docstring.startswith("Alias for "): return None match = self._get_valid_values_regex.search(docstring) @@ -1500,19 +1538,18 @@ def get_valid_values(self, attr): param_name = func.__code__.co_varnames[1] # We could set the presence * based on whether the parameter is a # varargs (it can't be a varkwargs) but it's not really worth it. - match = re.search(fr"(?m)^ *\*?{param_name} : (.+)", docstring) + match = re.search(rf"(?m)^ *\*?{param_name} : (.+)", docstring) if match: return match.group(1) - return 'unknown' + return "unknown" def _replace_path(self, source_class): """ Changes the full path to the public API path that is used in sphinx. This is needed for links to work. """ - replace_dict = {'_base._AxesBase': 'Axes', - '_axes.Axes': 'Axes'} + replace_dict = {"_base._AxesBase": "Axes", "_axes.Axes": "Axes"} for key, value in replace_dict.items(): source_class = source_class.replace(key, value) return source_class @@ -1526,12 +1563,14 @@ def get_setters(self): """ setters = [] for name in dir(self.o): - if not name.startswith('set_'): + if not name.startswith("set_"): continue func = getattr(self.o, name) - if (not callable(func) - or self.number_of_parameters(func) < 2 - or self.is_alias(func)): + if ( + not callable(func) + or self.number_of_parameters(func) < 2 + or self.is_alias(func) + ): continue setters.append(name[4:]) return setters @@ -1553,7 +1592,7 @@ def is_alias(method): if ds is None: return False - return ds.startswith('Alias for ') + return ds.startswith("Alias for ") def aliased_name(self, s): """ @@ -1563,7 +1602,7 @@ def aliased_name(self, s): alias, return 'markerfacecolor or mfc' and for the transform property, which does not, return 'transform'. """ - aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) + aliases = "".join(" or %s" % x for x in sorted(self.aliasd.get(s, []))) return s + aliases _NOT_LINKABLE = { @@ -1571,15 +1610,15 @@ def aliased_name(self, s): # current docs. This is a workaround used to prevent trying to link # these setters which would lead to "target reference not found" # warnings during doc build. - 'matplotlib.image._ImageBase.set_alpha', - 'matplotlib.image._ImageBase.set_array', - 'matplotlib.image._ImageBase.set_data', - 'matplotlib.image._ImageBase.set_filternorm', - 'matplotlib.image._ImageBase.set_filterrad', - 'matplotlib.image._ImageBase.set_interpolation', - 'matplotlib.image._ImageBase.set_interpolation_stage', - 'matplotlib.image._ImageBase.set_resample', - 'matplotlib.text._AnnotationBase.set_annotation_clip', + "matplotlib.image._ImageBase.set_alpha", + "matplotlib.image._ImageBase.set_array", + "matplotlib.image._ImageBase.set_data", + "matplotlib.image._ImageBase.set_filternorm", + "matplotlib.image._ImageBase.set_filterrad", + "matplotlib.image._ImageBase.set_interpolation", + "matplotlib.image._ImageBase.set_interpolation_stage", + "matplotlib.image._ImageBase.set_resample", + "matplotlib.text._AnnotationBase.set_annotation_clip", } def aliased_name_rest(self, s, target): @@ -1593,10 +1632,10 @@ def aliased_name_rest(self, s, target): """ # workaround to prevent "reference target not found" if target in self._NOT_LINKABLE: - return f'``{s}``' + return f"``{s}``" - aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) - return f':meth:`{s} <{target}>`{aliases}' + aliases = "".join(" or %s" % x for x in sorted(self.aliasd.get(s, []))) + return f":meth:`{s} <{target}>`{aliases}" def pprint_setters(self, prop=None, leadingspace=2): """ @@ -1608,18 +1647,18 @@ def pprint_setters(self, prop=None, leadingspace=2): values. """ if leadingspace: - pad = ' ' * leadingspace + pad = " " * leadingspace else: - pad = '' + pad = "" if prop is not None: accepts = self.get_valid_values(prop) - return f'{pad}{prop}: {accepts}' + return f"{pad}{prop}: {accepts}" lines = [] for prop in sorted(self.get_setters()): accepts = self.get_valid_values(prop) name = self.aliased_name(prop) - lines.append(f'{pad}{name}: {accepts}') + lines.append(f"{pad}{name}: {accepts}") return lines def pprint_setters_rest(self, prop=None, leadingspace=4): @@ -1632,12 +1671,12 @@ def pprint_setters_rest(self, prop=None, leadingspace=4): values. """ if leadingspace: - pad = ' ' * leadingspace + pad = " " * leadingspace else: - pad = '' + pad = "" if prop is not None: accepts = self.get_valid_values(prop) - return f'{pad}{prop}: {accepts}' + return f"{pad}{prop}: {accepts}" prop_and_qualnames = [] for prop in sorted(self.get_setters()): @@ -1649,39 +1688,49 @@ def pprint_setters_rest(self, prop=None, leadingspace=4): else: # No docstring available. method = getattr(self.o, f"set_{prop}") prop_and_qualnames.append( - (prop, f"{method.__module__}.{method.__qualname__}")) - - names = [self.aliased_name_rest(prop, target) - .replace('_base._AxesBase', 'Axes') - .replace('_axes.Axes', 'Axes') - for prop, target in prop_and_qualnames] - accepts = [self.get_valid_values(prop) - for prop, _ in prop_and_qualnames] + (prop, f"{method.__module__}.{method.__qualname__}") + ) + + names = [ + self.aliased_name_rest(prop, target) + .replace("_base._AxesBase", "Axes") + .replace("_axes.Axes", "Axes") + for prop, target in prop_and_qualnames + ] + accepts = [self.get_valid_values(prop) for prop, _ in prop_and_qualnames] col0_len = max(len(n) for n in names) col1_len = max(len(a) for a in accepts) - table_formatstr = pad + ' ' + '=' * col0_len + ' ' + '=' * col1_len + table_formatstr = pad + " " + "=" * col0_len + " " + "=" * col1_len return [ - '', - pad + '.. table::', - pad + ' :class: property-table', - '', + "", + pad + ".. table::", + pad + " :class: property-table", + "", table_formatstr, - pad + ' ' + 'Property'.ljust(col0_len) - + ' ' + 'Description'.ljust(col1_len), + pad + + " " + + "Property".ljust(col0_len) + + " " + + "Description".ljust(col1_len), table_formatstr, - *[pad + ' ' + n.ljust(col0_len) + ' ' + a.ljust(col1_len) - for n, a in zip(names, accepts)], + *[ + pad + " " + n.ljust(col0_len) + " " + a.ljust(col1_len) + for n, a in zip(names, accepts) + ], table_formatstr, - '', + "", ] def properties(self): """Return a dictionary mapping property name -> value.""" o = self.oorig - getters = [name for name in dir(o) - if name.startswith('get_') and callable(getattr(o, name))] + getters = [ + name + for name in dir(o) + if name.startswith("get_") and callable(getattr(o, name)) + ] getters.sort() d = {} for name in getters: @@ -1690,7 +1739,7 @@ def properties(self): continue try: with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") val = func() except Exception: continue @@ -1702,15 +1751,15 @@ def pprint_getters(self): """Return the getters and actual values as list of strings.""" lines = [] for name, val in sorted(self.properties().items()): - if getattr(val, 'shape', ()) != () and len(val) > 6: - s = str(val[:6]) + '...' + if getattr(val, "shape", ()) != () and len(val) > 6: + s = str(val[:6]) + "..." else: s = str(val) - s = s.replace('\n', ' ') + s = s.replace("\n", " ") if len(s) > 50: - s = s[:50] + '...' + s = s[:50] + "..." name = self.aliased_name(name) - lines.append(f' {name} = {s}') + lines.append(f" {name} = {s}") return lines @@ -1745,9 +1794,10 @@ def getp(obj, property=None): if property is None: insp = ArtistInspector(obj) ret = insp.pprint_getters() - print('\n'.join(ret)) + print("\n".join(ret)) return - return getattr(obj, 'get_' + property)() + return getattr(obj, "get_" + property)() + # alias get = getp @@ -1815,7 +1865,7 @@ def setp(obj, *args, file=None, **kwargs): if isinstance(obj, Artist): objs = [obj] else: - objs = list(cbook.flatten(obj)) + objs = list(_flatten_fast(obj)) if not objs: return @@ -1826,15 +1876,15 @@ def setp(obj, *args, file=None, **kwargs): if args: print(insp.pprint_setters(prop=args[0]), file=file) else: - print('\n'.join(insp.pprint_setters()), file=file) + print("\n".join(insp.pprint_setters()), file=file) return if len(args) % 2: - raise ValueError('The set args must be string, value pairs') + raise ValueError("The set args must be string, value pairs") funcvals = dict(zip(args[::2], args[1::2])) ret = [o.update(funcvals) for o in objs] + [o.set(**kwargs) for o in objs] - return list(cbook.flatten(ret)) + return list(_flatten_fast(ret)) def kwdoc(artist): @@ -1854,9 +1904,31 @@ def kwdoc(artist): use in Sphinx) if it is True. """ ai = ArtistInspector(artist) - return ('\n'.join(ai.pprint_setters_rest(leadingspace=4)) - if mpl.rcParams['docstring.hardcopy'] else - 'Properties:\n' + '\n'.join(ai.pprint_setters(leadingspace=4))) + return ( + "\n".join(ai.pprint_setters_rest(leadingspace=4)) + if mpl.rcParams["docstring.hardcopy"] + else "Properties:\n" + "\n".join(ai.pprint_setters(leadingspace=4)) + ) + + +def _flatten_fast(seq): + """Fast iterative flatten avoiding recursion overhead.""" + stack = [iter(seq)] + while stack: + try: + item = next(stack[-1]) + except StopIteration: + stack.pop() + continue + if ( + hasattr(item, "__iter__") + and not isinstance(item, (str, bytes)) + and item is not None + ): + stack.append(iter(item)) + else: + yield item + # We defer this to the end of them module, because it needs ArtistInspector # to be defined.