Skip to content

Commit 0d14033

Browse files
committed
Split color parsing to standalone function
1 parent 9f45c0b commit 0d14033

File tree

1 file changed

+76
-51
lines changed

1 file changed

+76
-51
lines changed

pgfutils.py

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import string
2929
import sys
3030
import types
31-
from typing import Callable
31+
from typing import Callable, Literal
3232

3333

3434
class Tracker(importlib.abc.MetaPathFinder):
@@ -194,6 +194,74 @@ class ColorError(ValueError):
194194
pass
195195

196196

197+
def parse_color(spec: Literal["none", "transparent"] | str | float | tuple[float]):
198+
"""Parse a color specification to a Matplotlib color.
199+
200+
Recognised color formats are:
201+
* Named colors (red, yellow, etc.)
202+
* Cycle colors (C1, C2 etc.)
203+
* Tuples (r, g, b) or (r, g, b, a) with floating-point entries in [0, 1]
204+
* A floating-point value in [0, 1] for grayscale
205+
* 'none', 'transparent', or an empty value for transparent
206+
207+
Parameters
208+
----------
209+
spec
210+
The color specification to parse.
211+
212+
Returns
213+
-------
214+
matplotlib-compatible colour.
215+
216+
Raises
217+
------
218+
ColorError
219+
The value could not be interpreted as a color.
220+
221+
"""
222+
# Transparent.
223+
if spec in {"none", "transparent", ""}:
224+
return "none"
225+
226+
# Single floating point number: grayscale.
227+
try:
228+
gray = float(spec)
229+
except ValueError:
230+
pass
231+
else:
232+
if not (0 <= gray <= 1):
233+
raise ColorError("greyscale floats must be in [0, 1].")
234+
235+
# For historical reasons Matlotlib requires this to be a string.
236+
return spec
237+
238+
# Nth color in the cycle (i.e., C1, C2 etc), or a named color. Unfortunately,
239+
# this returns True for grayscale values outside [0, 1] so we have to do our own
240+
# check above.
241+
if matplotlib.colors.is_color_like(spec):
242+
return spec
243+
244+
# Anything else we accept is valid Python syntax, so parse it.
245+
try:
246+
parsed = ast.literal_eval(spec)
247+
except (SyntaxError, TypeError, ValueError):
248+
raise ColorError(f"could not interpret '{spec}' as a color.")
249+
250+
# Needs to be a list or tuple of channel values.
251+
if not isinstance(parsed, (list, tuple)):
252+
raise ColorError(f"could not interpret '{spec}' as a color.")
253+
254+
# Filter out Booleans which Matplotlib would treat as 0 or 1.
255+
if any(isinstance(entry, bool) for entry in parsed):
256+
raise ColorError(f"could not interpret '{spec}' as a color.")
257+
258+
# And get Matplotlib to convert to a color.
259+
try:
260+
return matplotlib.colors.to_rgba(parsed)
261+
except ValueError as e:
262+
raise ColorError(str(e))
263+
264+
197265
# Recognise pieces of a dimension string.
198266
dimension_pieces = re.compile(r"^\s*(?P<size>\d+(?:\.\d*)?)\s*(?P<unit>.+?)?\s*$")
199267

@@ -385,58 +453,15 @@ def getcolor(self, section, option, **kwargs):
385453
The value could not be interpreted as a color.
386454
387455
"""
388-
# Retrieve the string value. Empty values are interpreted as none.
389-
value = self.get(section, option, **kwargs).strip() or "none"
390-
391-
# Transparent.
392-
if value in {"none", "transparent"}:
393-
return "none"
394-
395-
# Single floating point number: grayscale.
396-
try:
397-
gray = float(value)
398-
except ValueError:
399-
pass
400-
else:
401-
if not (0 <= gray <= 1):
402-
raise ColorError(
403-
f"{section}.{option}: greyscale floats must be in [0, 1]."
404-
)
405-
406-
# For historical reasons Matlotlib requires this to be a string.
407-
return value
408-
409-
# Nth color in the cycle (i.e., C1, C2 etc), or a named color. Unfortunately,
410-
# this returns True for grayscale values outside [0, 1] so we have to do our own
411-
# check above.
412-
if matplotlib.colors.is_color_like(value):
413-
return value
414-
415-
# Anything else we accept is valid Python syntax, so parse it.
416-
try:
417-
parsed = ast.literal_eval(value)
418-
except (SyntaxError, TypeError, ValueError):
419-
raise ColorError(
420-
f"{section}.{option}: could not interpret '{value}' as a color."
421-
) from None
422-
423-
# Needs to be a list or tuple of channel values.
424-
if not isinstance(parsed, (list, tuple)):
425-
raise ColorError(
426-
f"{section}.{option}: could not interpret '{value}' as a color."
427-
)
428-
429-
# Filter out Booleans which Matplotlib would treat as 0 or 1.
430-
if any(isinstance(entry, bool) for entry in parsed):
431-
raise ColorError(
432-
f"{section}.{option}: could not interpret '{value}' as a color."
433-
)
456+
# Get the string version of the dimension.
457+
spec = self.get(section, option, **kwargs)
434458

435-
# And get Matplotlib to convert to a color.
459+
# And parse it; modify any parsing exception to include
460+
# the section and option we were parsing.
436461
try:
437-
return matplotlib.colors.to_rgba(parsed)
438-
except ValueError as e:
439-
raise ColorError(f"{section}.{option}: {e}.") from None
462+
return parse_color(spec)
463+
except ColorError as e:
464+
raise ColorError(f"{section}.{option}: {e}") from None
440465

441466
def in_tracking_dir(self, type, fn):
442467
"""Check if a file is in a tracking directory.

0 commit comments

Comments
 (0)