Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 451ca5a

Browse filesBrowse files
committed
META: aliasable _enums and some LineStyle bugfixes
1 parent 8175daa commit 451ca5a
Copy full SHA for 451ca5a

File tree

3 files changed

+164
-110
lines changed
Filter options

3 files changed

+164
-110
lines changed

‎lib/matplotlib/_api/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/_api/__init__.py
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,7 @@ def warn_external(message, category=None):
211211
break
212212
frame = frame.f_back
213213
warnings.warn(message, category, stacklevel)
214+
215+
216+
def is_string_like(x):
217+
return isinstance(x, (str, bytes, bytearray))

‎lib/matplotlib/_enums.py

Copy file name to clipboardExpand all lines: lib/matplotlib/_enums.py
+156-69Lines changed: 156 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,81 @@
1010
they define.
1111
"""
1212

13-
from enum import Enum, auto
14-
from numbers import Number
13+
from enum import _EnumDict, EnumMeta, Enum, auto
14+
15+
import numpy as np
1516

1617
from matplotlib import _api, docstring
1718

1819

19-
class _AutoStringNameEnum(Enum):
20-
"""Automate the ``name = 'name'`` part of making a (str, Enum)."""
20+
class _AliasableStrEnumDict(_EnumDict):
21+
"""Helper for `_AliasableEnumMeta`."""
22+
def __init__(self):
23+
super().__init__()
24+
self._aliases = {}
25+
# adopt the Python 3.10 convention of "auto()" simply using the name of
26+
# the attribute: https://bugs.python.org/issue42385
27+
# this can be removed once we no longer support Python 3.9
28+
self._generate_next_value \
29+
= lambda name, start, count, last_values: name
30+
31+
def __setitem__(self, key, value):
32+
# if a class attribute with this name has already been created,
33+
# register this as an "alias"
34+
if key in self:
35+
self._aliases[value] = self[key]
36+
else:
37+
super().__setitem__(key, value)
2138

22-
def _generate_next_value_(name, start, count, last_values):
23-
return name
39+
40+
class _AliasableEnumMeta(EnumMeta):
41+
"""
42+
Allow Enums to have multiple "values" which are equivalent.
43+
44+
For a discussion of several approaches to "value aliasing", see
45+
https://stackoverflow.com/questions/24105268/is-it-possible-to-override-new-in-an-enum-to-parse-strings-to-an-instance
46+
"""
47+
@classmethod
48+
def __prepare__(metacls, cls, bases):
49+
# a custom dict (_EnumDict) is used when handing the __prepared__
50+
# class's namespace to EnumMeta.__new__. This way, when non-dunder,
51+
# non-descriptor class-level variables are added to the class namespace
52+
# during class-body execution, their values can be replaced with the
53+
# singletons that will later be returned by Enum.__call__.
54+
55+
# We over-ride this dict to prevent _EnumDict's internal checks from
56+
# throwing an error whenever preventing the same name is inserted
57+
# twice. Instead, we add that name to a _aliases dict that can be
58+
# used to look up the correct singleton later.
59+
return _AliasableStrEnumDict()
60+
61+
def __new__(metacls, cls, bases, classdict):
62+
# add our _aliases dict to the newly created class, so that it
63+
# can be used by __call__.
64+
enum_class = super().__new__(metacls, cls, bases, classdict)
65+
enum_class._aliases_ = classdict._aliases
66+
return enum_class
67+
68+
def __call__(cls, value, *args, **kw):
69+
# convert the value to the "default" if it is an alias, and then simply
70+
# forward to Enum
71+
if value not in cls. _value2member_map_ and value in cls._aliases_:
72+
value = cls._aliases_[value]
73+
return super().__call__(value, *args, **kw)
74+
75+
76+
class _AliasableStringNameEnum(Enum, metaclass=_AliasableEnumMeta):
77+
"""
78+
Convenience mix-in for easier construction of string enums.
79+
80+
Automates the ``name = 'name'`` part of making a (str, Enum), using the
81+
semantics that have now been adopted as part of Python 3.10:
82+
(bugs.python.org/issue42385).
83+
84+
In addition, allow multiple strings to be synonyms for the same underlying
85+
Enum value. This allows us to easily have things like ``LineStyle('--') ==
86+
LineStyle('dashed')`` work as expected.
87+
"""
2488

2589
def __hash__(self):
2690
return str(self).__hash__()
@@ -43,7 +107,7 @@ def _deprecate_case_insensitive_join_cap(s):
43107
return s_low
44108

45109

46-
class JoinStyle(str, _AutoStringNameEnum):
110+
class JoinStyle(str, _AliasableStringNameEnum):
47111
"""
48112
Define how the connection between two line segments is drawn.
49113
@@ -139,7 +203,7 @@ def plot_angle(ax, x, y, angle, style):
139203
+ "}"
140204

141205

142-
class CapStyle(str, _AutoStringNameEnum):
206+
class CapStyle(str, _AliasableStringNameEnum):
143207
r"""
144208
Define how the two endpoints (caps) of an unclosed line are drawn.
145209
@@ -211,7 +275,7 @@ def demo():
211275

212276

213277
#: Maps short codes for line style to their full name used by backends.
214-
_ls_mapper = {'': 'None', ' ': 'None', 'none': 'None',
278+
_ls_mapper = {'': 'none', ' ': 'none', 'none': 'none',
215279
'-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'}
216280
_deprecated_lineStyles = {
217281
'-': '_draw_solid',
@@ -224,7 +288,37 @@ def demo():
224288
}
225289

226290

227-
class NamedLineStyle(str, _AutoStringNameEnum):
291+
def _validate_onoffseq(x):
292+
"""Raise a helpful error message for malformed onoffseq."""
293+
err = 'In a custom LineStyle (offset, onoffseq), the onoffseq must '
294+
if _api.is_string_like(x):
295+
raise ValueError(err + 'not be a string.')
296+
if not np.iterable(x):
297+
raise ValueError(err + 'be iterable.')
298+
if not len(x) % 2 == 0:
299+
raise ValueError(err + 'be of even length.')
300+
if not np.all(x > 0):
301+
raise ValueError(err + 'have strictly positive, numerical elements.')
302+
303+
304+
class _NamedLineStyle(_AliasableStringNameEnum):
305+
"""A standardized way to refer to each named LineStyle internally."""
306+
solid = auto()
307+
solid = '-'
308+
dashed = auto()
309+
dashed = '--'
310+
dotted = auto()
311+
dotted = ':'
312+
dashdot = auto()
313+
dashdot = '-.'
314+
none = auto()
315+
none = 'None'
316+
none = ' '
317+
none = ''
318+
custom = auto()
319+
320+
321+
class LineStyle:
228322
"""
229323
Describe if the line is solid or dashed, and the dash pattern, if any.
230324
@@ -239,7 +333,7 @@ class NamedLineStyle(str, _AutoStringNameEnum):
239333
``'--'`` or ``'dashed'`` dashed line
240334
``'-.'`` or ``'dashdot'`` dash-dotted line
241335
``':'`` or ``'dotted'`` dotted line
242-
``'None'`` or ``' '`` or ``''`` draw nothing
336+
``'none'`` or ``' '`` or ``''`` draw nothing
243337
=============================== =================
244338
245339
However, for more fine-grained control, one can directly specify the
@@ -249,18 +343,17 @@ class NamedLineStyle(str, _AutoStringNameEnum):
249343
250344
where ``onoffseq`` is an even length tuple specifying the lengths of each
251345
subsequent dash and space, and ``offset`` controls at which point in this
252-
pattern the start of the line will begin (to allow you to e.g. prevent
253-
corners from happening to land in between dashes).
254-
255-
For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point
256-
dashes separated by 2 point spaces.
346+
pattern the start of the line will begin (allowing you to, for example,
347+
prevent a sharp corner landing in between dashes and therefore not being
348+
drawn).
257349
258-
Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*.
350+
For example, the ``onoffseq`` (5, 2, 1, 2) describes a sequence of 5 point
351+
and 1 point dashes separated by 2 point spaces.
259352
260353
The default dashing patterns described in the table above are themselves
261-
all described in this notation, and can therefore be customized by editing
262-
the appropriate ``lines.*_pattern`` *rc* parameter, as described in
263-
:doc:`/tutorials/introductory/customizing`.
354+
defined under the hood using an offset and an onoffseq, and can therefore
355+
be customized by editing the appropriate ``lines.*_pattern`` *rc*
356+
parameter, as described in :doc:`/tutorials/introductory/customizing`.
264357
265358
.. plot::
266359
:alt: Demo of possible LineStyle's.
@@ -271,22 +364,15 @@ class NamedLineStyle(str, _AutoStringNameEnum):
271364
.. note::
272365
273366
In addition to directly taking a ``linestyle`` argument,
274-
`~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that
275-
can be used to create a new *LineStyle* by providing just the
276-
``onoffseq``, but does not let you customize the offset. This method is
277-
called when using the keyword *dashes* to the cycler , as shown in
278-
:doc:`property_cycle </tutorials/intermediate/color_cycle>`.
367+
`~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method (and
368+
the :doc:`property_cycle </tutorials/intermediate/color_cycle>` has a
369+
*dashes* keyword) that can be used to create a new *LineStyle* by
370+
providing just the ``onoffseq``, but does not let you customize the
371+
offset. This method simply sets the underlying linestyle, and is only
372+
kept for backwards compatibility.
279373
"""
280-
solid = auto()
281-
dashed = auto()
282-
dotted = auto()
283-
dashdot = auto()
284-
none = auto()
285-
custom = auto()
286374

287-
class LineStyle(str):
288-
289-
def __init__(self, ls, scale=1):
375+
def __init__(self, ls):
290376
"""
291377
Parameters
292378
----------
@@ -301,56 +387,58 @@ def __init__(self, ls, scale=1):
301387
"""
302388

303389
self._linestyle_spec = ls
304-
if isinstance(ls, str):
305-
if ls in [' ', '', 'None']:
306-
ls = 'none'
307-
if ls in _ls_mapper:
308-
ls = _ls_mapper[ls]
309-
Enum.__init__(self)
310-
offset, onoffseq = None, None
390+
if _api.is_string_like(ls):
391+
self._name = _NamedLineStyle(ls)
392+
self._offset, self._onoffseq = None, None
311393
else:
394+
self._name = _NamedLineStyle('custom')
312395
try:
313-
offset, onoffseq = ls
396+
self._offset, self._onoffseq = ls
314397
except ValueError: # not enough/too many values to unpack
315-
raise ValueError('LineStyle should be a string or a 2-tuple, '
316-
'instead received: ' + str(ls))
317-
if offset is None:
398+
raise ValueError('Custom LineStyle must be a 2-tuple (offset, '
399+
'onoffseq), instead received: ' + str(ls))
400+
_validate_onoffseq(self._onoffseq)
401+
if self._offset is None:
318402
_api.warn_deprecated(
319403
"3.3", message="Passing the dash offset as None is deprecated "
320404
"since %(since)s and support for it will be removed "
321405
"%(removal)s; pass it as zero instead.")
322-
offset = 0
406+
self._offset = 0
323407

324-
if onoffseq is not None:
325-
# normalize offset to be positive and shorter than the dash cycle
326-
dsum = sum(onoffseq)
327-
if dsum:
328-
offset %= dsum
329-
if len(onoffseq) % 2 != 0:
330-
raise ValueError('LineStyle onoffseq must be of even length.')
331-
if not all(isinstance(elem, Number) for elem in onoffseq):
332-
raise ValueError('LineStyle onoffseq must be list of floats.')
333-
self._us_offset = offset
334-
self._us_onoffseq = onoffseq
408+
def __eq__(self, other):
409+
if not isinstance(other, LineStyle):
410+
other = LineStyle(other)
411+
return self.get_dashes() == other.get_dashes()
335412

336413
def __hash__(self):
337-
if self == LineStyle.custom:
338-
return (self._us_offset, tuple(self._us_onoffseq)).__hash__()
339-
return _AutoStringNameEnum.__hash__(self)
414+
if self._name == LineStyle.custom:
415+
return (self._offset, tuple(self._onoffseq)).__hash__()
416+
return _AliasableStringNameEnum.__hash__(self._name)
340417

418+
@staticmethod
419+
def _normalize_offset(offset, onoffseq):
420+
"""Normalize offset to be positive and shorter than the dash cycle."""
421+
dsum = sum(onoffseq)
422+
if dsum:
423+
offset %= dsum
424+
return offset
425+
426+
def is_dashed(self):
427+
offset, onoffseq = self.get_dashes()
428+
return np.isclose(np.sum(onoffseq), 0)
341429

342430
def get_dashes(self, lw=1):
343431
"""
344432
Get the (scaled) dash sequence for this `.LineStyle`.
345433
"""
346-
# defer lookup until draw time
347-
if self._us_offset is None or self._us_onoffseq is None:
348-
self._us_offset, self._us_onoffseq = \
349-
LineStyle._get_dash_pattern(self.name)
350-
# normalize offset to be positive and shorter than the dash cycle
351-
dsum = sum(self._us_onoffseq)
352-
self._us_offset %= dsum
353-
return self._scale_dashes(self._us_offset, self._us_onoffseq, lw)
434+
# named linestyle lookup happens at draw time (here)
435+
if self._onoffseq is None:
436+
offset, onoffseq = LineStyle._get_dash_pattern(self._name)
437+
else:
438+
offset, onoff_seq = self._offset, self._onoffseq
439+
# force 0 <= offset < dash cycle length
440+
offset = LineStyle._normalize_offset(offset, onoffseq)
441+
return self._scale_dashes(offset, onoffseq, lw)
354442

355443
@staticmethod
356444
def _scale_dashes(offset, dashes, lw):
@@ -462,6 +550,5 @@ def plot_linestyles(ax, linestyles, title):
462550
plt.tight_layout()
463551
plt.show()
464552

465-
466553
LineStyle._ls_mapper = _ls_mapper
467554
LineStyle._deprecated_lineStyles = _deprecated_lineStyles

‎lib/matplotlib/rcsetup.py

Copy file name to clipboardExpand all lines: lib/matplotlib/rcsetup.py
+4-41Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import ast
1717
from functools import lru_cache, reduce
1818
import logging
19-
from numbers import Number
2019
import operator
2120
import re
2221

@@ -527,8 +526,6 @@ def validate_ps_distiller(s):
527526
return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s)
528527

529528

530-
# A validator dedicated to the named line styles, based on the items in
531-
# ls_mapper, and a list of possible strings read from Line2D.set_linestyle
532529
def _validate_linestyle(ls):
533530
"""
534531
A validator for all possible line styles, the named ones *and*
@@ -539,48 +536,14 @@ def _validate_linestyle(ls):
539536
ls = ast.literal_eval(ls) # Parsing matplotlibrc.
540537
except (SyntaxError, ValueError):
541538
pass # Will error with the ValueError at the end.
542-
543-
<<<<<<< HEAD
544-
def _is_iterable_not_string_like(x):
545-
# Explicitly exclude bytes/bytearrays so that they are not
546-
# nonsensically interpreted as sequences of numbers (codepoints).
547-
return np.iterable(x) and not isinstance(x, (str, bytes, bytearray))
548-
549-
# (offset, (on, off, on, off, ...))
550-
if (_is_iterable_not_string_like(ls)
551-
and len(ls) == 2
552-
and isinstance(ls[0], (type(None), Number))
553-
and _is_iterable_not_string_like(ls[1])
554-
and len(ls[1]) % 2 == 0
555-
and all(isinstance(elem, Number) for elem in ls[1])):
556-
if ls[0] is None:
557-
_api.warn_deprecated(
558-
"3.3", message="Passing the dash offset as None is deprecated "
559-
"since %(since)s and support for it will be removed "
560-
"%(removal)s; pass it as zero instead.")
561-
ls = (0, ls[1])
562-
return ls
563-
# For backcompat: (on, off, on, off, ...); the offset is implicitly None.
564-
if (_is_iterable_not_string_like(ls)
565-
and len(ls) % 2 == 0
566-
and all(isinstance(elem, Number) for elem in ls)):
567-
return (0, ls)
568-
raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.")
569-
=======
570539
try:
571540
LineStyle(ls)
572541
except ValueError as e:
573-
# For backcompat, only in rc, we allow the user to pash only the
574-
# onoffseq, and set the offset implicitly to 0
575-
if (np.iterable(ls) and not isinstance(ls, (str, bytes, bytearray))
576-
and len(ls) % 2 == 0
577-
and all(isinstance(elem, Number) for elem in ls)):
578-
try:
579-
LineStyle((0, ls))
580-
except ValueError:
581-
raise e
542+
try:
543+
LineStyle((0, ls))
544+
except ValueError:
545+
raise e
582546
return ls
583-
>>>>>>> b2d2793cc... GSOD: LineStyle class
584547

585548

586549
validate_fillstyle = ValidateInStrings(

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.