From 4899580d96d3e0b4f5df14710b8a829f9751d5a8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 Dec 2019 00:09:58 +0100 Subject: [PATCH] Fix validation of linestyle in rcparams and cycler. Correcly use _validate_linestyle instead of validate_stringlist in the cycler linestyle validator. For compat, support both the (on, off, on, off, ...) and (offset, (on, off, on, off, ...)) forms. Don't support passing on, off, etc. as *individual* separate strings anymore as that's not a realistic use case (unlike passing the whole thing as a single string, which is supported for parsing the rc file). --- lib/matplotlib/rcsetup.py | 87 +++++++++++++++------------ lib/matplotlib/tests/test_rcparams.py | 11 ++-- 2 files changed, 56 insertions(+), 42 deletions(-) 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 ) }, )