From 8175daaa4fbd8b318a770691137a0ecf73dc1439 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 5 Oct 2020 11:59:23 -0700 Subject: [PATCH 1/3] GSOD: LineStyle class --- examples/lines_bars_and_markers/linestyles.py | 92 ++---- lib/matplotlib/_enums.py | 265 +++++++++++++++++- lib/matplotlib/cbook/__init__.py | 6 - lib/matplotlib/lines.py | 159 ++--------- lib/matplotlib/patches.py | 36 +-- lib/matplotlib/rcsetup.py | 31 +- 6 files changed, 340 insertions(+), 249 deletions(-) diff --git a/examples/lines_bars_and_markers/linestyles.py b/examples/lines_bars_and_markers/linestyles.py index 35920617c90c..d71488bcfa87 100644 --- a/examples/lines_bars_and_markers/linestyles.py +++ b/examples/lines_bars_and_markers/linestyles.py @@ -3,74 +3,28 @@ Linestyles ========== -Simple linestyles can be defined using the strings "solid", "dotted", "dashed" -or "dashdot". More refined control can be achieved by providing a dash tuple -``(offset, (on_off_seq))``. For example, ``(0, (3, 10, 1, 15))`` means -(3pt line, 10pt space, 1pt line, 15pt space) with no offset. See also -`.Line2D.set_linestyle`. - -*Note*: The dash style can also be configured via `.Line2D.set_dashes` -as shown in :doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` -and passing a list of dash sequences using the keyword *dashes* to the -cycler in :doc:`property_cycle `. +The Matplotlib `~mpl._enums.LineStyle` specifies the dash pattern used to draw +a given line. The simplest line styles can be accessed by name using the +strings "solid", "dotted", "dashed" and "dashdot" (or their short names, "-", +":", "--", and "-.", respectively). + +The exact spacing of the dashes used can be controlled using the +'lines.*_pattern' family of rc parameters. For example, +:rc:`lines.dashdot_pattern` controls the exact spacing of dashed used whenever +the '-.' `~mpl._enums.LineStyle` is specified. + +For more information about how to create custom `~mpl._enums.LineStyle` +specifications, see `the LineStyle docs `. + +*Note*: For historical reasons, one can also specify the dash pattern for a +particular line using `.Line2D.set_dashes` as shown in +:doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` (or by passing a +list of dash sequences using the keyword *dashes* to the cycler in +:doc:`property_cycle `). This interface is +strictly less expressive, and we recommend using LineStyle (or the keyword +*linestyle* to the :doc:`property cycler +`). """ -import numpy as np -import matplotlib.pyplot as plt - -linestyle_str = [ - ('solid', 'solid'), # Same as (0, ()) or '-' - ('dotted', 'dotted'), # Same as (0, (1, 1)) or '.' - ('dashed', 'dashed'), # Same as '--' - ('dashdot', 'dashdot')] # Same as '-.' - -linestyle_tuple = [ - ('loosely dotted', (0, (1, 10))), - ('dotted', (0, (1, 1))), - ('densely dotted', (0, (1, 1))), - - ('loosely dashed', (0, (5, 10))), - ('dashed', (0, (5, 5))), - ('densely dashed', (0, (5, 1))), - - ('loosely dashdotted', (0, (3, 10, 1, 10))), - ('dashdotted', (0, (3, 5, 1, 5))), - ('densely dashdotted', (0, (3, 1, 1, 1))), - - ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), - ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), - ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))] - - -def plot_linestyles(ax, linestyles, title): - X, Y = np.linspace(0, 100, 10), np.zeros(10) - yticklabels = [] - - for i, (name, linestyle) in enumerate(linestyles): - ax.plot(X, Y+i, linestyle=linestyle, linewidth=1.5, color='black') - yticklabels.append(name) - - ax.set_title(title) - ax.set(ylim=(-0.5, len(linestyles)-0.5), - yticks=np.arange(len(linestyles)), - yticklabels=yticklabels) - ax.tick_params(left=False, bottom=False, labelbottom=False) - ax.spines[:].set_visible(False) - - # For each line style, add a text annotation with a small offset from - # the reference point (0 in Axes coords, y tick value in Data coords). - for i, (name, linestyle) in enumerate(linestyles): - ax.annotate(repr(linestyle), - xy=(0.0, i), xycoords=ax.get_yaxis_transform(), - xytext=(-6, -12), textcoords='offset points', - color="blue", fontsize=8, ha="right", family="monospace") - - -ax0, ax1 = (plt.figure(figsize=(10, 8)) - .add_gridspec(2, 1, height_ratios=[1, 3]) - .subplots()) - -plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') -plot_linestyles(ax1, linestyle_tuple[::-1], title='Parametrized linestyles') +from matplotlib._enums import LineStyle -plt.tight_layout() -plt.show() +LineStyle.demo() diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index 35fe82482869..594b70f381e3 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -11,7 +11,9 @@ """ from enum import Enum, auto -from matplotlib import cbook, docstring +from numbers import Number + +from matplotlib import _api, docstring class _AutoStringNameEnum(Enum): @@ -28,12 +30,12 @@ def _deprecate_case_insensitive_join_cap(s): s_low = s.lower() if s != s_low: if s_low in ['miter', 'round', 'bevel']: - cbook.warn_deprecated( + _api.warn_deprecated( "3.3", message="Case-insensitive capstyles are deprecated " "since %(since)s and support for them will be removed " "%(removal)s; please pass them in lowercase.") elif s_low in ['butt', 'round', 'projecting']: - cbook.warn_deprecated( + _api.warn_deprecated( "3.3", message="Case-insensitive joinstyles are deprecated " "since %(since)s and support for them will be removed " "%(removal)s; please pass them in lowercase.") @@ -206,3 +208,260 @@ def demo(): docstring.interpd.update({'JoinStyle': JoinStyle.input_description, 'CapStyle': CapStyle.input_description}) + + +#: Maps short codes for line style to their full name used by backends. +_ls_mapper = {'': 'None', ' ': 'None', 'none': 'None', + '-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} +_deprecated_lineStyles = { + '-': '_draw_solid', + '--': '_draw_dashed', + '-.': '_draw_dash_dot', + ':': '_draw_dotted', + 'None': '_draw_nothing', + ' ': '_draw_nothing', + '': '_draw_nothing', +} + + +class NamedLineStyle(str, _AutoStringNameEnum): + """ + Describe if the line is solid or dashed, and the dash pattern, if any. + + All lines in Matplotlib are considered either solid or "dashed". Some + common dashing patterns are built-in, and are sufficient for a majority of + uses: + + =============================== ================= + Linestyle Description + =============================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'None'`` or ``' '`` or ``''`` draw nothing + =============================== ================= + + However, for more fine-grained control, one can directly specify the + dashing pattern by specifying:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple specifying the lengths of each + subsequent dash and space, and ``offset`` controls at which point in this + pattern the start of the line will begin (to allow you to e.g. prevent + corners from happening to land in between dashes). + + For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point + dashes separated by 2 point spaces. + + Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*. + + The default dashing patterns described in the table above are themselves + all described in this notation, and can therefore be customized by editing + the appropriate ``lines.*_pattern`` *rc* parameter, as described in + :doc:`/tutorials/introductory/customizing`. + + .. plot:: + :alt: Demo of possible LineStyle's. + + from matplotlib._types import LineStyle + LineStyle.demo() + + .. note:: + + In addition to directly taking a ``linestyle`` argument, + `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that + can be used to create a new *LineStyle* by providing just the + ``onoffseq``, but does not let you customize the offset. This method is + called when using the keyword *dashes* to the cycler , as shown in + :doc:`property_cycle `. + """ + solid = auto() + dashed = auto() + dotted = auto() + dashdot = auto() + none = auto() + custom = auto() + +class LineStyle(str): + + def __init__(self, ls, scale=1): + """ + Parameters + ---------- + ls : str or dash tuple + A description of the dashing pattern of the line. Allowed string + inputs are {'-', 'solid', '--', 'dashed', '-.', 'dashdot', ':', + 'dotted', '', ' ', 'None', 'none'}. Alternatively, the dash tuple + (``offset``, ``onoffseq``) can be specified directly in points. + scale : float + Uniformly scale the internal dash sequence length by a constant + factor. + """ + + self._linestyle_spec = ls + if isinstance(ls, str): + if ls in [' ', '', 'None']: + ls = 'none' + if ls in _ls_mapper: + ls = _ls_mapper[ls] + Enum.__init__(self) + offset, onoffseq = None, None + else: + try: + offset, onoffseq = ls + except ValueError: # not enough/too many values to unpack + raise ValueError('LineStyle should be a string or a 2-tuple, ' + 'instead received: ' + str(ls)) + if offset is None: + _api.warn_deprecated( + "3.3", message="Passing the dash offset as None is deprecated " + "since %(since)s and support for it will be removed " + "%(removal)s; pass it as zero instead.") + offset = 0 + + if onoffseq is not None: + # normalize offset to be positive and shorter than the dash cycle + dsum = sum(onoffseq) + if dsum: + offset %= dsum + if len(onoffseq) % 2 != 0: + raise ValueError('LineStyle onoffseq must be of even length.') + if not all(isinstance(elem, Number) for elem in onoffseq): + raise ValueError('LineStyle onoffseq must be list of floats.') + self._us_offset = offset + self._us_onoffseq = onoffseq + + def __hash__(self): + if self == LineStyle.custom: + return (self._us_offset, tuple(self._us_onoffseq)).__hash__() + return _AutoStringNameEnum.__hash__(self) + + + def get_dashes(self, lw=1): + """ + Get the (scaled) dash sequence for this `.LineStyle`. + """ + # defer lookup until draw time + if self._us_offset is None or self._us_onoffseq is None: + self._us_offset, self._us_onoffseq = \ + LineStyle._get_dash_pattern(self.name) + # normalize offset to be positive and shorter than the dash cycle + dsum = sum(self._us_onoffseq) + self._us_offset %= dsum + return self._scale_dashes(self._us_offset, self._us_onoffseq, lw) + + @staticmethod + def _scale_dashes(offset, dashes, lw): + from . import rcParams + if not rcParams['lines.scale_dashes']: + return offset, dashes + scaled_offset = offset * lw + scaled_dashes = ([x * lw if x is not None else None for x in dashes] + if dashes is not None else None) + return scaled_offset, scaled_dashes + + @staticmethod + def _get_dash_pattern(style): + """Convert linestyle string to explicit dash pattern.""" + # import must be local for validator code to live here + from . import rcParams + # un-dashed styles + if style in ['solid', 'None']: + offset = 0 + dashes = None + # dashed styles + elif style in ['dashed', 'dashdot', 'dotted']: + offset = 0 + dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + return offset, dashes + + @staticmethod + def from_dashes(seq): + """ + Create a `.LineStyle` from a dash sequence (i.e. the ``onoffseq``). + + The dash sequence is a sequence of floats of even length describing + the length of dashes and spaces in points. + + Parameters + ---------- + seq : sequence of floats (on/off ink in points) or (None, None) + If *seq* is empty or ``(None, None)``, the `.LineStyle` will be + solid. + """ + if seq == (None, None) or len(seq) == 0: + return LineStyle('-') + else: + return LineStyle((0, seq)) + + @staticmethod + def demo(): + import numpy as np + import matplotlib.pyplot as plt + + linestyle_str = [ + ('solid', 'solid'), # Same as (0, ()) or '-' + ('dotted', 'dotted'), # Same as (0, (1, 1)) or '.' + ('dashed', 'dashed'), # Same as '--' + ('dashdot', 'dashdot')] # Same as '-.' + + linestyle_tuple = [ + ('loosely dotted', (0, (1, 10))), + ('dotted', (0, (1, 1))), + ('densely dotted', (0, (1, 1))), + + ('loosely dashed', (0, (5, 10))), + ('dashed', (0, (5, 5))), + ('densely dashed', (0, (5, 1))), + + ('loosely dashdotted', (0, (3, 10, 1, 10))), + ('dashdotted', (0, (3, 5, 1, 5))), + ('densely dashdotted', (0, (3, 1, 1, 1))), + + ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), + ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), + ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))] + + def plot_linestyles(ax, linestyles, title): + X, Y = np.linspace(0, 100, 10), np.zeros(10) + yticklabels = [] + + for i, (name, linestyle) in enumerate(linestyles): + ax.plot(X, Y+i, linestyle=linestyle, linewidth=1.5, + color='black') + yticklabels.append(name) + + ax.set_title(title) + ax.set(ylim=(-0.5, len(linestyles)-0.5), + yticks=np.arange(len(linestyles)), + yticklabels=yticklabels) + ax.tick_params(left=False, bottom=False, labelbottom=False) + for spine in ax.spines.values(): + spine.set_visible(False) + + # For each line style, add a text annotation with a small offset + # from the reference point (0 in Axes coords, y tick value in Data + # coords). + for i, (name, linestyle) in enumerate(linestyles): + ax.annotate(repr(linestyle), + xy=(0.0, i), xycoords=ax.get_yaxis_transform(), + xytext=(-6, -12), textcoords='offset points', + color="blue", fontsize=8, ha="right", + family="monospace") + + ax0, ax1 = (plt.figure(figsize=(10, 8)) + .add_gridspec(2, 1, height_ratios=[1, 3]) + .subplots()) + + plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') + plot_linestyles(ax1, linestyle_tuple[::-1], + title='Parametrized linestyles') + + plt.tight_layout() + plt.show() + + +LineStyle._ls_mapper = _ls_mapper +LineStyle._deprecated_lineStyles = _deprecated_lineStyles diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 31055dcd7990..92e72b863ed3 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1266,12 +1266,6 @@ def _compute_conf_interval(data, med, iqr, bootstrap): return bxpstats -#: Maps short codes for line style to their full name used by backends. -ls_mapper = {'-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} -#: Maps full names for line styles used by backends to their short codes. -ls_mapper_r = {v: k for k, v in ls_mapper.items()} - - def contiguous_regions(mask): """ Return a list of (ind0, ind1) such that ``mask[ind0:ind1].all()`` is diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 41b2f98c314e..6008cafc8929 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -11,13 +11,12 @@ import matplotlib as mpl from . import _api, artist, cbook, colors as mcolors, docstring, rcParams from .artist import Artist, allow_rasterization -from .cbook import ( - _to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP) +from .cbook import _to_unmasked_float_array, STEP_LOOKUP_MAP from .colors import is_color_like, get_named_colors_mapping from .markers import MarkerStyle from .path import Path from .transforms import Bbox, BboxTransformTo, TransformedPath -from ._enums import JoinStyle, CapStyle +from ._enums import JoinStyle, CapStyle, LineStyle # Imported here for backward compatibility, even though they don't # really belong. @@ -30,49 +29,6 @@ _log = logging.getLogger(__name__) -def _get_dash_pattern(style): - """Convert linestyle to dash pattern.""" - # go from short hand -> full strings - if isinstance(style, str): - style = ls_mapper.get(style, style) - # un-dashed styles - if style in ['solid', 'None']: - offset = 0 - dashes = None - # dashed styles - elif style in ['dashed', 'dashdot', 'dotted']: - offset = 0 - dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) - # - elif isinstance(style, tuple): - offset, dashes = style - if offset is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - offset = 0 - else: - raise ValueError('Unrecognized linestyle: %s' % str(style)) - - # normalize offset to be positive and shorter than the dash cycle - if dashes is not None: - dsum = sum(dashes) - if dsum: - offset %= dsum - - return offset, dashes - - -def _scale_dashes(offset, dashes, lw): - if not rcParams['lines.scale_dashes']: - return offset, dashes - scaled_offset = offset * lw - scaled_dashes = ([x * lw if x is not None else None for x in dashes] - if dashes is not None else None) - return scaled_offset, scaled_dashes - - def segment_hits(cx, cy, x, y, radius): """ Return the indices of the segments in the polyline with coordinates (*cx*, @@ -217,15 +173,7 @@ class Line2D(Artist): can create "stepped" lines in various styles. """ - lineStyles = _lineStyles = { # hidden names deprecated - '-': '_draw_solid', - '--': '_draw_dashed', - '-.': '_draw_dash_dot', - ':': '_draw_dotted', - 'None': '_draw_nothing', - ' ': '_draw_nothing', - '': '_draw_nothing', - } + lineStyles = _lineStyles = LineStyle._deprecated_lineStyles _drawStyles_l = { 'default': '_draw_lines', @@ -303,7 +251,7 @@ def __init__(self, xdata, ydata, %(Line2D_kwdoc)s - See :meth:`set_linestyle` for a description of the line styles, + See `.LineStyle` for a description of the line styles, :meth:`set_marker` for a description of the markers, and :meth:`set_drawstyle` for a description of the draw styles. @@ -320,7 +268,7 @@ def __init__(self, xdata, ydata, linewidth = rcParams['lines.linewidth'] if linestyle is None: - linestyle = rcParams['lines.linestyle'] + linestyle = LineStyle(rcParams['lines.linestyle']) if marker is None: marker = rcParams['lines.marker'] if markerfacecolor is None: @@ -359,16 +307,8 @@ def __init__(self, xdata, ydata, self._drawstyle = None self._linewidth = linewidth - # scaled dash + offset - self._dashSeq = None - self._dashOffset = 0 - # unscaled dash + offset - # this is needed scaling the dash pattern by linewidth - self._us_dashSeq = None - self._us_dashOffset = 0 - - self.set_linewidth(linewidth) self.set_linestyle(linestyle) + self.set_linewidth(linewidth) self.set_drawstyle(drawstyle) self._color = None @@ -792,7 +732,7 @@ def draw(self, renderer): if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) - gc.set_dashes(self._dashOffset, self._dashSeq) + gc.set_dashes(*self._linestyle.get_dashes(self._linewidth)) renderer.draw_path(gc, tpath, affine.frozen()) gc.restore() @@ -902,7 +842,7 @@ def get_linestyle(self): See also `~.Line2D.set_linestyle`. """ - return self._linestyle + return self._linestyle._linestyle_spec def get_linewidth(self): """ @@ -1105,57 +1045,32 @@ def set_linewidth(self, w): if self._linewidth != w: self.stale = True self._linewidth = w - # rescale the dashes + offset - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) + if rcParams['lines.scale_dashes']: + self._linestyle.scale = self._linewidth def set_linestyle(self, ls): """ - Set the linestyle of the line. + Set the `.LineStyle` of the line. Parameters ---------- - ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} - Possible values: - - - A string: + ls : {'-', '--', ':', '-.', ...} or other `.LineStyle`-like + Set the dashing pattern for the line. Typical values are - =============================== ================= - Linestyle Description - =============================== ================= - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``'None'`` or ``' '`` or ``''`` draw nothing - =============================== ================= + =============================== ================= + Linestyle Description + =============================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'None'`` or ``' '`` or ``''`` draw nothing + =============================== ================= - - Alternatively a dash tuple of the following form can be - provided:: - - (offset, onoffseq) - - where ``onoffseq`` is an even length tuple of on and off ink - in points. See also :meth:`set_dashes`. - - For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. + For a full description of possible inputs and examples, see the + `.LineStyle` docs. """ - if isinstance(ls, str): - if ls in [' ', '', 'none']: - ls = 'None' - - _api.check_in_list([*self._lineStyles, *ls_mapper_r], ls=ls) - if ls not in self._lineStyles: - ls = ls_mapper_r[ls] - self._linestyle = ls - else: - self._linestyle = '--' - - # get the unscaled dashes - self._us_dashOffset, self._us_dashSeq = _get_dash_pattern(ls) - # compute the linewidth scaled dashes - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) + self._linestyle = LineStyle(ls) @docstring.interpd def set_marker(self, marker): @@ -1268,25 +1183,9 @@ def set_ydata(self, y): self.stale = True def set_dashes(self, seq): - """ - Set the dash sequence. + self.set_linestyle(LineStyle.from_dashes(seq)) - The dash sequence is a sequence of floats of even length describing - the length of dashes and spaces in points. - - For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point - dashes separated by 2 point spaces. - - Parameters - ---------- - seq : sequence of floats (on/off ink in points) or (None, None) - If *seq* is empty or ``(None, None)``, the linestyle will be set - to solid. - """ - if seq == (None, None) or len(seq) == 0: - self.set_linestyle('-') - else: - self.set_linestyle((0, seq)) + set_dashes.__doc__ = LineStyle.from_dashes.__doc__ def update_from(self, other): """Copy properties from *other* to self.""" @@ -1299,10 +1198,6 @@ def update_from(self, other): self._markerfacecoloralt = other._markerfacecoloralt self._markeredgecolor = other._markeredgecolor self._markeredgewidth = other._markeredgewidth - self._dashSeq = other._dashSeq - self._us_dashSeq = other._us_dashSeq - self._dashOffset = other._dashOffset - self._us_dashOffset = other._us_dashOffset self._dashcapstyle = other._dashcapstyle self._dashjoinstyle = other._dashjoinstyle self._solidcapstyle = other._solidcapstyle diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 81df8568f243..60fcacf3a6be 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -16,7 +16,7 @@ get_parallels, inside_circle, make_wedged_bezier2, split_bezier_intersecting_with_closedpath, split_path_inout) from .path import Path -from ._enums import JoinStyle, CapStyle +from ._enums import JoinStyle, CapStyle, LineStyle @cbook._define_aliases({ @@ -92,8 +92,6 @@ def __init__(self, else: self.set_edgecolor(edgecolor) self.set_facecolor(facecolor) - # unscaled dashes. Needed to scale dash patterns by lw - self._us_dashes = None self._linewidth = 0 self.set_fill(fill) @@ -254,9 +252,8 @@ def update_from(self, other): self._fill = other._fill self._hatch = other._hatch self._hatch_color = other._hatch_color - # copy the unscaled dash pattern - self._us_dashes = other._us_dashes - self.set_linewidth(other._linewidth) # also sets dash properties + self.set_linestyle(other._linestyle) + self.set_linewidth(other._linewidth) self.set_transform(other.get_data_transform()) # If the transform of other needs further initialization, then it will # be the case for this artist too. @@ -308,7 +305,7 @@ def get_linewidth(self): def get_linestyle(self): """Return the linestyle.""" - return self._linestyle + return self._linestyle._linestyle_spec def set_antialiased(self, aa): """ @@ -404,10 +401,6 @@ def set_linewidth(self, w): w = mpl.rcParams['axes.linewidth'] self._linewidth = float(w) - # scale the dash pattern by the linewidth - offset, ls = self._us_dashes - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) self.stale = True def set_linestyle(self, ls): @@ -438,16 +431,7 @@ def set_linestyle(self, ls): ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} The line style. """ - if ls is None: - ls = "solid" - if ls in [' ', '', 'none']: - ls = 'None' - self._linestyle = ls - # get the unscaled dash pattern - offset, ls = self._us_dashes = mlines._get_dash_pattern(ls) - # scale the dash pattern by the linewidth - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) + self._linestyle = LineStyle(ls) self.stale = True def set_fill(self, b): @@ -560,10 +544,12 @@ def _bind_draw_path_function(self, renderer): gc.set_foreground(self._edgecolor, isRGBA=True) lw = self._linewidth - if self._edgecolor[3] == 0 or self._linestyle == 'None': + if self._edgecolor[3] == 0 or self.get_linestyle() == 'None': lw = 0 gc.set_linewidth(lw) - gc.set_dashes(self._dashoffset, self._dashes) + dash_offset, onoffseq = self._linestyle.get_dashes(lw) + # Patch has traditionally ignored the dashoffset. + gc.set_dashes(0, onoffseq) gc.set_capstyle(self._capstyle) gc.set_joinstyle(self._joinstyle) @@ -600,9 +586,7 @@ def draw(self, renderer): # docstring inherited if not self.get_visible(): return - # Patch has traditionally ignored the dashoffset. - with cbook._setattr_cm(self, _dashoffset=0), \ - self._bind_draw_path_function(renderer) as draw_path: + with self._bind_draw_path_function(renderer) as draw_path: path = self.get_path() transform = self.get_transform() tpath = transform.transform_path_non_affine(path) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 535649b03f9f..3b10e6218a54 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -3,7 +3,7 @@ Matplotlib's rc settings. Each rc setting is assigned a function used to validate any attempted changes -to that setting. The validation functions are defined in the rcsetup module, +to that setting. The validation functions are defined in the rcsetup module, and are used to construct the rcParams global object which stores the settings and is referenced throughout Matplotlib. @@ -23,10 +23,9 @@ import numpy as np from matplotlib import _api, animation, cbook -from matplotlib.cbook import ls_mapper from matplotlib.colors import Colormap, is_color_like from matplotlib.fontconfig_pattern import parse_fontconfig_pattern -from matplotlib._enums import JoinStyle, CapStyle +from matplotlib._enums import JoinStyle, CapStyle, LineStyle # Don't let the original cycler collide with our validating cycler from cycler import Cycler, cycler as ccycler @@ -530,27 +529,18 @@ def validate_ps_distiller(s): # A validator dedicated to the named line styles, based on the items in # ls_mapper, and a list of possible strings read from Line2D.set_linestyle -_validate_named_linestyle = ValidateInStrings( - 'linestyle', - [*ls_mapper.keys(), *ls_mapper.values(), 'None', 'none', ' ', ''], - ignorecase=True) - - def _validate_linestyle(ls): """ A validator for all possible line styles, the named ones *and* the on-off ink sequences. """ if isinstance(ls, str): - try: # Look first for a valid named line style, like '--' or 'solid'. - return _validate_named_linestyle(ls) - except ValueError: - pass try: ls = ast.literal_eval(ls) # Parsing matplotlibrc. except (SyntaxError, ValueError): pass # Will error with the ValueError at the end. +<<<<<<< HEAD def _is_iterable_not_string_like(x): # Explicitly exclude bytes/bytearrays so that they are not # nonsensically interpreted as sequences of numbers (codepoints). @@ -576,6 +566,21 @@ def _is_iterable_not_string_like(x): and all(isinstance(elem, Number) for elem in ls)): return (0, ls) raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") +======= + try: + LineStyle(ls) + except ValueError as e: + # For backcompat, only in rc, we allow the user to pash only the + # onoffseq, and set the offset implicitly to 0 + if (np.iterable(ls) and not isinstance(ls, (str, bytes, bytearray)) + and len(ls) % 2 == 0 + and all(isinstance(elem, Number) for elem in ls)): + try: + LineStyle((0, ls)) + except ValueError: + raise e + return ls +>>>>>>> b2d2793cc... GSOD: LineStyle class validate_fillstyle = ValidateInStrings( From 451ca5a77dc5b9c44cdafbb2512ae493481d4f82 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 4 Jan 2021 12:03:33 -0800 Subject: [PATCH 2/3] META: aliasable _enums and some LineStyle bugfixes --- lib/matplotlib/_api/__init__.py | 4 + lib/matplotlib/_enums.py | 225 ++++++++++++++++++++++---------- lib/matplotlib/rcsetup.py | 45 +------ 3 files changed, 164 insertions(+), 110 deletions(-) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index f251e07bab59..cbf6800913f6 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -211,3 +211,7 @@ def warn_external(message, category=None): break frame = frame.f_back warnings.warn(message, category, stacklevel) + + +def is_string_like(x): + return isinstance(x, (str, bytes, bytearray)) diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index 594b70f381e3..c690cebb12a6 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -10,17 +10,81 @@ they define. """ -from enum import Enum, auto -from numbers import Number +from enum import _EnumDict, EnumMeta, Enum, auto + +import numpy as np from matplotlib import _api, docstring -class _AutoStringNameEnum(Enum): - """Automate the ``name = 'name'`` part of making a (str, Enum).""" +class _AliasableStrEnumDict(_EnumDict): + """Helper for `_AliasableEnumMeta`.""" + def __init__(self): + super().__init__() + self._aliases = {} + # adopt the Python 3.10 convention of "auto()" simply using the name of + # the attribute: https://bugs.python.org/issue42385 + # this can be removed once we no longer support Python 3.9 + self._generate_next_value \ + = lambda name, start, count, last_values: name + + def __setitem__(self, key, value): + # if a class attribute with this name has already been created, + # register this as an "alias" + if key in self: + self._aliases[value] = self[key] + else: + super().__setitem__(key, value) - def _generate_next_value_(name, start, count, last_values): - return name + +class _AliasableEnumMeta(EnumMeta): + """ + Allow Enums to have multiple "values" which are equivalent. + + For a discussion of several approaches to "value aliasing", see + https://stackoverflow.com/questions/24105268/is-it-possible-to-override-new-in-an-enum-to-parse-strings-to-an-instance + """ + @classmethod + def __prepare__(metacls, cls, bases): + # a custom dict (_EnumDict) is used when handing the __prepared__ + # class's namespace to EnumMeta.__new__. This way, when non-dunder, + # non-descriptor class-level variables are added to the class namespace + # during class-body execution, their values can be replaced with the + # singletons that will later be returned by Enum.__call__. + + # We over-ride this dict to prevent _EnumDict's internal checks from + # throwing an error whenever preventing the same name is inserted + # twice. Instead, we add that name to a _aliases dict that can be + # used to look up the correct singleton later. + return _AliasableStrEnumDict() + + def __new__(metacls, cls, bases, classdict): + # add our _aliases dict to the newly created class, so that it + # can be used by __call__. + enum_class = super().__new__(metacls, cls, bases, classdict) + enum_class._aliases_ = classdict._aliases + return enum_class + + def __call__(cls, value, *args, **kw): + # convert the value to the "default" if it is an alias, and then simply + # forward to Enum + if value not in cls. _value2member_map_ and value in cls._aliases_: + value = cls._aliases_[value] + return super().__call__(value, *args, **kw) + + +class _AliasableStringNameEnum(Enum, metaclass=_AliasableEnumMeta): + """ + Convenience mix-in for easier construction of string enums. + + Automates the ``name = 'name'`` part of making a (str, Enum), using the + semantics that have now been adopted as part of Python 3.10: + (bugs.python.org/issue42385). + + In addition, allow multiple strings to be synonyms for the same underlying + Enum value. This allows us to easily have things like ``LineStyle('--') == + LineStyle('dashed')`` work as expected. + """ def __hash__(self): return str(self).__hash__() @@ -43,7 +107,7 @@ def _deprecate_case_insensitive_join_cap(s): return s_low -class JoinStyle(str, _AutoStringNameEnum): +class JoinStyle(str, _AliasableStringNameEnum): """ Define how the connection between two line segments is drawn. @@ -139,7 +203,7 @@ def plot_angle(ax, x, y, angle, style): + "}" -class CapStyle(str, _AutoStringNameEnum): +class CapStyle(str, _AliasableStringNameEnum): r""" Define how the two endpoints (caps) of an unclosed line are drawn. @@ -211,7 +275,7 @@ def demo(): #: Maps short codes for line style to their full name used by backends. -_ls_mapper = {'': 'None', ' ': 'None', 'none': 'None', +_ls_mapper = {'': 'none', ' ': 'none', 'none': 'none', '-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} _deprecated_lineStyles = { '-': '_draw_solid', @@ -224,7 +288,37 @@ def demo(): } -class NamedLineStyle(str, _AutoStringNameEnum): +def _validate_onoffseq(x): + """Raise a helpful error message for malformed onoffseq.""" + err = 'In a custom LineStyle (offset, onoffseq), the onoffseq must ' + if _api.is_string_like(x): + raise ValueError(err + 'not be a string.') + if not np.iterable(x): + raise ValueError(err + 'be iterable.') + if not len(x) % 2 == 0: + raise ValueError(err + 'be of even length.') + if not np.all(x > 0): + raise ValueError(err + 'have strictly positive, numerical elements.') + + +class _NamedLineStyle(_AliasableStringNameEnum): + """A standardized way to refer to each named LineStyle internally.""" + solid = auto() + solid = '-' + dashed = auto() + dashed = '--' + dotted = auto() + dotted = ':' + dashdot = auto() + dashdot = '-.' + none = auto() + none = 'None' + none = ' ' + none = '' + custom = auto() + + +class LineStyle: """ Describe if the line is solid or dashed, and the dash pattern, if any. @@ -239,7 +333,7 @@ class NamedLineStyle(str, _AutoStringNameEnum): ``'--'`` or ``'dashed'`` dashed line ``'-.'`` or ``'dashdot'`` dash-dotted line ``':'`` or ``'dotted'`` dotted line - ``'None'`` or ``' '`` or ``''`` draw nothing + ``'none'`` or ``' '`` or ``''`` draw nothing =============================== ================= However, for more fine-grained control, one can directly specify the @@ -249,18 +343,17 @@ class NamedLineStyle(str, _AutoStringNameEnum): where ``onoffseq`` is an even length tuple specifying the lengths of each subsequent dash and space, and ``offset`` controls at which point in this - pattern the start of the line will begin (to allow you to e.g. prevent - corners from happening to land in between dashes). - - For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point - dashes separated by 2 point spaces. + pattern the start of the line will begin (allowing you to, for example, + prevent a sharp corner landing in between dashes and therefore not being + drawn). - Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*. + For example, the ``onoffseq`` (5, 2, 1, 2) describes a sequence of 5 point + and 1 point dashes separated by 2 point spaces. The default dashing patterns described in the table above are themselves - all described in this notation, and can therefore be customized by editing - the appropriate ``lines.*_pattern`` *rc* parameter, as described in - :doc:`/tutorials/introductory/customizing`. + defined under the hood using an offset and an onoffseq, and can therefore + be customized by editing the appropriate ``lines.*_pattern`` *rc* + parameter, as described in :doc:`/tutorials/introductory/customizing`. .. plot:: :alt: Demo of possible LineStyle's. @@ -271,22 +364,15 @@ class NamedLineStyle(str, _AutoStringNameEnum): .. note:: In addition to directly taking a ``linestyle`` argument, - `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that - can be used to create a new *LineStyle* by providing just the - ``onoffseq``, but does not let you customize the offset. This method is - called when using the keyword *dashes* to the cycler , as shown in - :doc:`property_cycle `. + `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method (and + the :doc:`property_cycle ` has a + *dashes* keyword) that can be used to create a new *LineStyle* by + providing just the ``onoffseq``, but does not let you customize the + offset. This method simply sets the underlying linestyle, and is only + kept for backwards compatibility. """ - solid = auto() - dashed = auto() - dotted = auto() - dashdot = auto() - none = auto() - custom = auto() -class LineStyle(str): - - def __init__(self, ls, scale=1): + def __init__(self, ls): """ Parameters ---------- @@ -301,56 +387,58 @@ def __init__(self, ls, scale=1): """ self._linestyle_spec = ls - if isinstance(ls, str): - if ls in [' ', '', 'None']: - ls = 'none' - if ls in _ls_mapper: - ls = _ls_mapper[ls] - Enum.__init__(self) - offset, onoffseq = None, None + if _api.is_string_like(ls): + self._name = _NamedLineStyle(ls) + self._offset, self._onoffseq = None, None else: + self._name = _NamedLineStyle('custom') try: - offset, onoffseq = ls + self._offset, self._onoffseq = ls except ValueError: # not enough/too many values to unpack - raise ValueError('LineStyle should be a string or a 2-tuple, ' - 'instead received: ' + str(ls)) - if offset is None: + raise ValueError('Custom LineStyle must be a 2-tuple (offset, ' + 'onoffseq), instead received: ' + str(ls)) + _validate_onoffseq(self._onoffseq) + if self._offset is None: _api.warn_deprecated( "3.3", message="Passing the dash offset as None is deprecated " "since %(since)s and support for it will be removed " "%(removal)s; pass it as zero instead.") - offset = 0 + self._offset = 0 - if onoffseq is not None: - # normalize offset to be positive and shorter than the dash cycle - dsum = sum(onoffseq) - if dsum: - offset %= dsum - if len(onoffseq) % 2 != 0: - raise ValueError('LineStyle onoffseq must be of even length.') - if not all(isinstance(elem, Number) for elem in onoffseq): - raise ValueError('LineStyle onoffseq must be list of floats.') - self._us_offset = offset - self._us_onoffseq = onoffseq + def __eq__(self, other): + if not isinstance(other, LineStyle): + other = LineStyle(other) + return self.get_dashes() == other.get_dashes() def __hash__(self): - if self == LineStyle.custom: - return (self._us_offset, tuple(self._us_onoffseq)).__hash__() - return _AutoStringNameEnum.__hash__(self) + if self._name == LineStyle.custom: + return (self._offset, tuple(self._onoffseq)).__hash__() + return _AliasableStringNameEnum.__hash__(self._name) + @staticmethod + def _normalize_offset(offset, onoffseq): + """Normalize offset to be positive and shorter than the dash cycle.""" + dsum = sum(onoffseq) + if dsum: + offset %= dsum + return offset + + def is_dashed(self): + offset, onoffseq = self.get_dashes() + return np.isclose(np.sum(onoffseq), 0) def get_dashes(self, lw=1): """ Get the (scaled) dash sequence for this `.LineStyle`. """ - # defer lookup until draw time - if self._us_offset is None or self._us_onoffseq is None: - self._us_offset, self._us_onoffseq = \ - LineStyle._get_dash_pattern(self.name) - # normalize offset to be positive and shorter than the dash cycle - dsum = sum(self._us_onoffseq) - self._us_offset %= dsum - return self._scale_dashes(self._us_offset, self._us_onoffseq, lw) + # named linestyle lookup happens at draw time (here) + if self._onoffseq is None: + offset, onoffseq = LineStyle._get_dash_pattern(self._name) + else: + offset, onoff_seq = self._offset, self._onoffseq + # force 0 <= offset < dash cycle length + offset = LineStyle._normalize_offset(offset, onoffseq) + return self._scale_dashes(offset, onoffseq, lw) @staticmethod def _scale_dashes(offset, dashes, lw): @@ -462,6 +550,5 @@ def plot_linestyles(ax, linestyles, title): plt.tight_layout() plt.show() - LineStyle._ls_mapper = _ls_mapper LineStyle._deprecated_lineStyles = _deprecated_lineStyles diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 3b10e6218a54..e601e279fde4 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -16,7 +16,6 @@ import ast from functools import lru_cache, reduce import logging -from numbers import Number import operator import re @@ -527,8 +526,6 @@ def validate_ps_distiller(s): return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s) -# A validator dedicated to the named line styles, based on the items in -# ls_mapper, and a list of possible strings read from Line2D.set_linestyle def _validate_linestyle(ls): """ A validator for all possible line styles, the named ones *and* @@ -539,48 +536,14 @@ def _validate_linestyle(ls): ls = ast.literal_eval(ls) # Parsing matplotlibrc. except (SyntaxError, ValueError): pass # Will error with the ValueError at the end. - -<<<<<<< HEAD - def _is_iterable_not_string_like(x): - # Explicitly exclude bytes/bytearrays so that they are not - # nonsensically interpreted as sequences of numbers (codepoints). - return np.iterable(x) and not isinstance(x, (str, bytes, bytearray)) - - # (offset, (on, off, on, off, ...)) - if (_is_iterable_not_string_like(ls) - and len(ls) == 2 - and isinstance(ls[0], (type(None), Number)) - and _is_iterable_not_string_like(ls[1]) - and len(ls[1]) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls[1])): - if ls[0] is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - ls = (0, ls[1]) - return ls - # For backcompat: (on, off, on, off, ...); the offset is implicitly None. - if (_is_iterable_not_string_like(ls) - and len(ls) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls)): - return (0, ls) - raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") -======= try: LineStyle(ls) except ValueError as e: - # For backcompat, only in rc, we allow the user to pash only the - # onoffseq, and set the offset implicitly to 0 - if (np.iterable(ls) and not isinstance(ls, (str, bytes, bytearray)) - and len(ls) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls)): - try: - LineStyle((0, ls)) - except ValueError: - raise e + try: + LineStyle((0, ls)) + except ValueError: + raise e return ls ->>>>>>> b2d2793cc... GSOD: LineStyle class validate_fillstyle = ValidateInStrings( From 3b8a30feab307e08ad1d9e8d3a5a0a9408100700 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 8 Feb 2021 12:20:15 -0800 Subject: [PATCH 3/3] BF: LineStyle eq and hash fixes --- lib/matplotlib/_enums.py | 41 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index c690cebb12a6..c4bd5cd098fd 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -301,7 +301,7 @@ def _validate_onoffseq(x): raise ValueError(err + 'have strictly positive, numerical elements.') -class _NamedLineStyle(_AliasableStringNameEnum): +class _NamedLineStyle(str, _AliasableStringNameEnum): """A standardized way to refer to each named LineStyle internally.""" solid = auto() solid = '-' @@ -388,10 +388,10 @@ def __init__(self, ls): self._linestyle_spec = ls if _api.is_string_like(ls): - self._name = _NamedLineStyle(ls) - self._offset, self._onoffseq = None, None + self._named = _NamedLineStyle(ls) + self._offset, self._onoffseq = 0, None else: - self._name = _NamedLineStyle('custom') + self._named = _NamedLineStyle('custom') try: self._offset, self._onoffseq = ls except ValueError: # not enough/too many values to unpack @@ -400,9 +400,9 @@ def __init__(self, ls): _validate_onoffseq(self._onoffseq) if self._offset is None: _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") + "3.3", message="Passing the dash offset as None is " + "deprecated since %(since)s and support for it will be " + "removed %(removal)s; pass it as zero instead.") self._offset = 0 def __eq__(self, other): @@ -411,13 +411,19 @@ def __eq__(self, other): return self.get_dashes() == other.get_dashes() def __hash__(self): - if self._name == LineStyle.custom: + if self._named == 'custom': return (self._offset, tuple(self._onoffseq)).__hash__() - return _AliasableStringNameEnum.__hash__(self._name) + return self._named.__hash__() + + def __repr__(self): + return self._named.__repr__() + ' with (offset, onoffseq) = ' \ + + str(self.get_dashes()) @staticmethod def _normalize_offset(offset, onoffseq): """Normalize offset to be positive and shorter than the dash cycle.""" + if onoffseq is None: + return 0 dsum = sum(onoffseq) if dsum: offset %= dsum @@ -425,17 +431,18 @@ def _normalize_offset(offset, onoffseq): def is_dashed(self): offset, onoffseq = self.get_dashes() - return np.isclose(np.sum(onoffseq), 0) + total_dash_length = np.sum(onoffseq) + return total_dash_length is None or np.isclose(total_dash_length, 0) def get_dashes(self, lw=1): """ Get the (scaled) dash sequence for this `.LineStyle`. """ - # named linestyle lookup happens at draw time (here) - if self._onoffseq is None: - offset, onoffseq = LineStyle._get_dash_pattern(self._name) + # named linestyle lookup happens each time dashes are requested + if self._named != 'custom': + offset, onoffseq = LineStyle._get_named_pattern(self._named) else: - offset, onoff_seq = self._offset, self._onoffseq + offset, onoffseq = self._offset, self._onoffseq # force 0 <= offset < dash cycle length offset = LineStyle._normalize_offset(offset, onoffseq) return self._scale_dashes(offset, onoffseq, lw) @@ -451,7 +458,7 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes @staticmethod - def _get_dash_pattern(style): + def _get_named_pattern(style): """Convert linestyle string to explicit dash pattern.""" # import must be local for validator code to live here from . import rcParams @@ -463,6 +470,10 @@ def _get_dash_pattern(style): elif style in ['dashed', 'dashdot', 'dotted']: offset = 0 dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + else: + raise ValueError("Attempted to get dash pattern from RC for " + "unknown dash name. Allowed values are 'dashed', " + "'dashdot', and 'dotted'.") return offset, dashes @staticmethod pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy