Skip to content

Commit 519cd4a

Browse files
committed
Tweak color parsing and tests
1 parent 0d14033 commit 519cd4a

File tree

2 files changed

+50
-80
lines changed

2 files changed

+50
-80
lines changed

pgfutils.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ class ColorError(ValueError):
194194
pass
195195

196196

197-
def parse_color(spec: Literal["none", "transparent"] | str | float | tuple[float]):
197+
def parse_color(spec: Literal["none", "transparent"] | str | float | tuple[float, ...]):
198198
"""Parse a color specification to a Matplotlib color.
199199
200200
Recognised color formats are:
@@ -219,45 +219,49 @@ def parse_color(spec: Literal["none", "transparent"] | str | float | tuple[float
219219
The value could not be interpreted as a color.
220220
221221
"""
222+
if isinstance(spec, list):
223+
spec = tuple(spec)
224+
222225
# Transparent.
223226
if spec in {"none", "transparent", ""}:
224227
return "none"
225228

226229
# Single floating point number: grayscale.
227230
try:
228231
gray = float(spec)
229-
except ValueError:
232+
except (TypeError, ValueError):
230233
pass
231234
else:
232235
if not (0 <= gray <= 1):
233-
raise ColorError("greyscale floats must be in [0, 1].")
236+
raise ColorError("greyscale floats must be in [0, 1]")
234237

235238
# For historical reasons Matlotlib requires this to be a string.
236-
return spec
239+
return str(gray)
237240

238241
# Nth color in the cycle (i.e., C1, C2 etc), or a named color. Unfortunately,
239242
# this returns True for grayscale values outside [0, 1] so we have to do our own
240243
# check above.
241244
if matplotlib.colors.is_color_like(spec):
242245
return spec
243246

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.")
247+
# Any other string we accept is valid Python syntax, so parse it.
248+
if isinstance(spec, str):
249+
try:
250+
spec = ast.literal_eval(spec)
251+
except (SyntaxError, TypeError, ValueError):
252+
raise ColorError(f"could not interpret '{spec}' as a color.")
249253

250254
# Needs to be a list or tuple of channel values.
251-
if not isinstance(parsed, (list, tuple)):
255+
if not isinstance(spec, (list, tuple)):
252256
raise ColorError(f"could not interpret '{spec}' as a color.")
253257

254258
# Filter out Booleans which Matplotlib would treat as 0 or 1.
255-
if any(isinstance(entry, bool) for entry in parsed):
259+
if any(isinstance(entry, bool) for entry in spec):
256260
raise ColorError(f"could not interpret '{spec}' as a color.")
257261

258262
# And get Matplotlib to convert to a color.
259263
try:
260-
return matplotlib.colors.to_rgba(parsed)
264+
return matplotlib.colors.to_rgba(spec)
261265
except ValueError as e:
262266
raise ColorError(str(e))
263267

tests/test_colors.py

Lines changed: 34 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,24 @@
22
# SPDX-License-Identifier: BSD-3-Clause
33

44
from matplotlib.colors import get_named_colors_mapping
5+
import numpy as np
56
import pytest
67

7-
from pgfutils import ColorError, _config, _config_reset
8+
from pgfutils import ColorError, parse_color
89

910

10-
class TestColorClass:
11+
class TestColor:
1112
def test_greyscale(self):
1213
"""Grayscale fraction parsing..."""
13-
_config_reset()
14-
1514
# Test a range of valid floats.
1615
# N.B., Matplotlib uses strings for greyscale.
17-
for f in (0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0):
18-
_config.read_kwargs(figure_background=f)
19-
assert _config["pgfutils"].getcolor("figure_background") == str(f)
16+
for f in (0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0):
17+
assert parse_color(f) == str(f)
18+
assert parse_color(str(f)) == str(f)
2019

2120
# The same things as strings.
2221
for s in (
23-
"0",
22+
"0.0",
2423
"0.1",
2524
"0.2",
2625
"0.3",
@@ -32,56 +31,39 @@ def test_greyscale(self):
3231
"0.9",
3332
"1.0",
3433
):
35-
_config.read_kwargs(figure_background=s)
36-
assert _config["pgfutils"].getcolor("figure_background") == s
34+
assert parse_color(s) == s
3735

3836
# Check numbers outside the valid range.
3937
for f in (1.01, -1):
40-
with pytest.raises(ColorError):
41-
_config.read_kwargs(axes_background=f)
42-
_config["pgfutils"].getcolor("axes_background")
38+
with pytest.raises(ColorError, match=r"must be in \[0, 1\]"):
39+
parse_color(f)
4340

4441
def test_named(self):
4542
"""Named color parsing..."""
46-
_config_reset()
47-
4843
# Try all known Matplotlib colors.
4944
for color in get_named_colors_mapping().keys():
50-
_config.read_kwargs(axes_background=color)
51-
assert _config["pgfutils"].getcolor("axes_background") == color
45+
assert parse_color(color) == color
5246

5347
# And check it rejects non-existent named colors.
54-
with pytest.raises(ColorError):
55-
_config.read_kwargs(axes_background="nonexistentuglycolor")
56-
_config["pgfutils"].getcolor("axes_background")
48+
with pytest.raises(ColorError, match="could not interpret .+ as a color"):
49+
parse_color("some_ugly_color")
5750

5851
def test_cycle(self):
5952
"""Color cycle parsing..."""
60-
_config_reset()
61-
6253
for i in range(0, 10):
6354
cycle = "C{0:d}".format(i)
64-
_config.read_kwargs(axes_background=cycle)
65-
assert _config["pgfutils"].getcolor("axes_background") == cycle
55+
assert parse_color(cycle) == cycle
6656

6757
def test_transparent(self):
6858
"""Color parsing supports transparency..."""
69-
_config_reset()
70-
_config.read_kwargs(axes_background="none")
71-
assert _config["pgfutils"].getcolor("axes_background") == "none"
72-
_config.read_kwargs(axes_background="transparent")
73-
assert _config["pgfutils"].getcolor("axes_background") == "none"
74-
_config.read_kwargs(axes_background="")
75-
assert _config["pgfutils"].getcolor("axes_background") == "none"
59+
assert parse_color("none") == "none"
60+
assert parse_color("transparent") == "none"
61+
assert parse_color("") == "none"
7662

7763
def test_rgb(self):
7864
"""RGB list/tuple color parsing..."""
79-
_config_reset()
80-
8165
# Generate a set of valid colors. We have to include an alpha channel
8266
# here as it is always returned from the parser with alpha=1.
83-
import numpy as np
84-
8567
c = np.linspace(0, 1, 5)
8668
a = (c * 0) + 1.0
8769
colors = np.stack(np.meshgrid(c, c, c, a), -1).reshape(-1, 4)
@@ -90,71 +72,55 @@ def test_rgb(self):
9072
for color in colors:
9173
list_c = color.tolist()
9274
tuple_c = tuple(list_c)
93-
_config.read_kwargs(figure_background=str(list_c[:-1]))
94-
assert _config["pgfutils"].getcolor("figure_background") == tuple_c
95-
_config.read_kwargs(axes_background=str(tuple_c[:-1]))
96-
assert _config["pgfutils"].getcolor("axes_background") == tuple_c
75+
assert parse_color(list_c) == tuple_c
76+
assert parse_color(tuple_c) == tuple_c
9777

9878
# Check it fails on channels with invalid values.
9979
color = [0, 0, 0]
10080
for channel in range(3):
101-
for value in (-0.1, 1.2, "a", True, False, None):
81+
for value in (-0.1, 1.2, "a", None):
82+
color[channel] = value
10283
with pytest.raises(ColorError):
103-
color[channel] = value
104-
_config.read_kwargs(axes_background=color)
105-
_config["pgfutils"].getcolor("axes_background")
84+
parse_color(color)
10685
color[channel] = 0
10786

10887
# And some invalid formats too.
10988
for value in ("fail", "yes", "no"):
110-
with pytest.raises(ColorError):
111-
_config.read_kwargs(axes_background=value)
112-
_config["pgfutils"].getcolor("axes_background")
89+
with pytest.raises(ColorError, match="could not interpret.+as a color"):
90+
parse_color(value)
11391

11492
def test_rgba(self):
11593
"""RGBA list/tuple color parsing..."""
116-
_config_reset()
117-
11894
# Generate a set of valid colors.
119-
import numpy as np
120-
12195
c = np.linspace(0, 1, 5)
12296
colors = np.stack(np.meshgrid(c, c, c, c), -1).reshape(-1, 4)
12397

12498
# Check they are accepted. The parser always returns colors as tuples.
12599
for color in colors:
126100
list_c = color.tolist()
127101
tuple_c = tuple(list_c)
128-
_config.read_kwargs(figure_background=str(list_c))
129-
assert _config["pgfutils"].getcolor("figure_background") == tuple_c
130-
_config.read_kwargs(axes_background=str(tuple_c))
131-
assert _config["pgfutils"].getcolor("axes_background") == tuple_c
102+
assert parse_color(list_c) == tuple_c
103+
assert parse_color(tuple_c) == tuple_c
132104

133105
# Check it fails on channels with invalid values.
134106
color = [0, 0, 0, 0]
135107
for channel in range(4):
136-
for value in (-0.1, 1.2, "a", True, False, None):
108+
for value in (-0.1, 1.2, "a", None):
109+
color[channel] = value
137110
with pytest.raises(ColorError):
138-
color[channel] = value
139-
_config.read_kwargs(axes_background=color)
140-
_config["pgfutils"].getcolor("axes_background")
111+
parse_color(color)
141112
color[channel] = 0
142113

143114
# And some invalid formats too.
144115
for value in ("fail", "yes", "no"):
145-
with pytest.raises(ColorError):
146-
_config.read_kwargs(axes_background=value)
147-
_config["pgfutils"].getcolor("axes_background")
116+
with pytest.raises(ColorError, match="could not interpret.+as a color"):
117+
parse_color(value)
148118

149119
def test_invalid_tuples(self):
150120
"""Check RGB/RGBA parsing rejects tuples of invalid length..."""
151-
_config_reset()
152121
with pytest.raises(ColorError):
153-
_config.read_kwargs(axes_background="(1,)")
154-
_config["pgfutils"].getcolor("axes_background")
122+
parse_color((1,))
155123
with pytest.raises(ColorError):
156-
_config.read_kwargs(axes_background="(1,1)")
157-
_config["pgfutils"].getcolor("axes_background")
124+
parse_color((1, 1))
158125
with pytest.raises(ColorError):
159-
_config.read_kwargs(axes_background="(1,1,1,1,1)")
160-
_config["pgfutils"].getcolor("axes_background")
126+
parse_color((1, 1, 1, 1, 1))

0 commit comments

Comments
 (0)