diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index d0cd93552c56..f10171c035d3 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -14,13 +14,17 @@ :file:`matplotlibrc.template` in matplotlib's root source directory. """ +import ast from collections.abc import Iterable, Mapping from functools import partial, reduce import logging +from numbers import Number import operator import os import re +import numpy as np + import matplotlib as mpl from matplotlib import animation, cbook from matplotlib.cbook import ls_mapper @@ -532,6 +536,50 @@ def validate_ps_distiller(s): 'ghostscript or xpdf') +# 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. + + 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])): + 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 (None, ls) + raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") + + validate_joinstyle = ValidateInStrings('joinstyle', ['miter', 'round', 'bevel'], ignorecase=True) @@ -748,7 +796,7 @@ def validate_hatch(s): 'color': _listify_validator(validate_color_for_prop_cycle, allow_stringlist=True), 'linewidth': validate_floatlist, - 'linestyle': validate_stringlist, + 'linestyle': _listify_validator(_validate_linestyle), 'facecolor': validate_colorlist, 'edgecolor': validate_colorlist, 'joinstyle': validate_joinstylelist, @@ -970,43 +1018,6 @@ def validate_webagg_address(s): raise ValueError("'webagg.address' is not a valid IP address") -# 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. - """ - # Look first for a valid named line style, like '--' or 'solid' Also - # includes bytes(-arrays) here (they all fail _validate_named_linestyle); - # otherwise, if *ls* is of even-length, it will be passed to the instance - # of validate_nseq_float, which will return an absurd on-off ink - # sequence... - if isinstance(ls, (str, bytes, bytearray)): - return _validate_named_linestyle(ls) - - # Look for an on-off ink sequence (in points) *of even length*. - # Offset is set to None. - try: - if len(ls) % 2 != 0: - raise ValueError("the linestyle sequence {!r} is not of even " - "length.".format(ls)) - - return (None, validate_nseq_float()(ls)) - - except (ValueError, TypeError): - # TypeError can be raised inside the instance of validate_nseq_float, - # by wrong types passed to float(), like NoneType. - raise ValueError("linestyle {!r} is not a valid on-off ink " - "sequence.".format(ls)) - - validate_axes_titlelocation = ValidateInStrings('axes.titlelocation', ['left', 'center', 'right']) # a map from key -> value, converter diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 87dfbeceba8c..7a61170dadda 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -370,18 +370,21 @@ def generate_validator_testcases(valid): ('', ''), (' ', ' '), ('None', 'none'), ('none', 'none'), ('DoTtEd', 'dotted'), # case-insensitive - (['1.23', '4.56'], (None, [1.23, 4.56])), + ('1, 3', (None, (1, 3))), ([1.23, 456], (None, [1.23, 456.0])), ([1, 2, 3, 4], (None, [1.0, 2.0, 3.0, 4.0])), + ((None, [1, 2]), (None, [1, 2])), + ((0, [1, 2]), (0, [1, 2])), + ((-1, [1, 2]), (-1, [1, 2])), ), 'fail': (('aardvark', ValueError), # not a valid string (b'dotted', ValueError), ('dotted'.encode('utf-16'), ValueError), - ((None, [1, 2]), ValueError), # (offset, dashes) != OK - ((0, [1, 2]), ValueError), # idem - ((-1, [1, 2]), ValueError), # idem ([1, 2, 3], ValueError), # sequence with odd length (1.23, ValueError), # not a sequence + (("a", [1, 2]), ValueError), # wrong explicit offset + ((1, [1, 2, 3]), ValueError), # odd length sequence + (([1, 2], 1), ValueError), # inverted offset/onoff ) }, )