Skip to content

Fix cycler validation #6275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,15 +726,7 @@ def cycler(*args, **kwargs):
if not isinstance(args[0], Cycler):
raise TypeError("If only one positional argument given, it must "
" be a Cycler instance.")

c = args[0]
unknowns = c.keys - (set(_prop_validators.keys()) |
set(_prop_aliases.keys()))
if unknowns:
# This is about as much validation I can do
raise TypeError("Unknown artist properties: %s" % unknowns)
else:
return Cycler(c)
return validate_cycler(args[0])
elif len(args) == 2:
pairs = [(args[0], args[1])]
elif len(args) > 2:
Expand Down Expand Up @@ -789,6 +781,33 @@ def validate_cycler(s):
else:
raise ValueError("object was not a string or Cycler instance: %s" % s)

unknowns = cycler_inst.keys - (set(_prop_validators) | set(_prop_aliases))
if unknowns:
raise ValueError("Unknown artist properties: %s" % unknowns)

# Not a full validation, but it'll at least normalize property names
# A fuller validation would require v0.10 of cycler.
checker = set()
for prop in cycler_inst.keys:
norm_prop = _prop_aliases.get(prop, prop)
if norm_prop != prop and norm_prop in cycler_inst.keys:
raise ValueError("Cannot specify both '{0}' and alias '{1}'"
" in the same prop_cycle".format(norm_prop, prop))
if norm_prop in checker:
raise ValueError("Another property was already aliased to '{0}'."
" Collision normalizing '{1}'.".format(norm_prop,
prop))
checker.update([norm_prop])

# This is just an extra-careful check, just in case there is some
# edge-case I haven't thought of.
assert len(checker) == len(cycler_inst.keys)

# Now, it should be safe to mutate this cycler
for prop in cycler_inst.keys:
norm_prop = _prop_aliases.get(prop, prop)
cycler_inst.change_key(prop, norm_prop)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to catch the ValueError that can come out of this and raise a better error?

This is a sneaky way to do double key validation 😜

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you suggest as a better error message? How about something along the lines of "cannot specify both '{}' and its alias '{}' in the same cycler"?

One concern I have is that this function can mutate a given cycler, and then raise an exception, which is not optimal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should do this in two steps? One explicit check of no aliased keys and then one normalization step we know will work?

I like that error message

Worried I am (re)painting the bikeshed with this review.


return cycler_inst


Expand Down
17 changes: 12 additions & 5 deletions lib/matplotlib/tests/test_cycles.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_colorcycle_basic():
def test_marker_cycle():
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) +
ax.set_prop_cycle(cycler('c', ['r', 'g', 'y']) +
cycler('marker', ['.', '*', 'x']))
xs = np.arange(10)
ys = 0.25 * xs + 2
Expand All @@ -48,7 +48,7 @@ def test_marker_cycle():
fig = plt.figure()
ax = fig.add_subplot(111)
# Test keyword arguments, numpy arrays, and generic iterators
ax.set_prop_cycle(color=np.array(['r', 'g', 'y']),
ax.set_prop_cycle(c=np.array(['r', 'g', 'y']),
marker=iter(['.', '*', 'x']))
xs = np.arange(10)
ys = 0.25 * xs + 2
Expand All @@ -67,7 +67,7 @@ def test_marker_cycle():
def test_linestylecycle_basic():
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_prop_cycle(cycler('linestyle', ['-', '--', ':']))
ax.set_prop_cycle(cycler('ls', ['-', '--', ':']))
xs = np.arange(10)
ys = 0.25 * xs + 2
ax.plot(xs, ys, label='solid', lw=4)
Expand All @@ -85,7 +85,7 @@ def test_linestylecycle_basic():
def test_fillcycle_basic():
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) +
ax.set_prop_cycle(cycler('c', ['r', 'g', 'y']) +
cycler('hatch', ['xx', 'O', '|-']) +
cycler('linestyle', ['-', '--', ':']))
xs = np.arange(10)
Expand Down Expand Up @@ -131,7 +131,7 @@ def test_valid_input_forms():
ax.set_prop_cycle(None)
ax.set_prop_cycle(cycler('linewidth', [1, 2]))
ax.set_prop_cycle('color', 'rgywkbcm')
ax.set_prop_cycle('linewidth', (1, 2))
ax.set_prop_cycle('lw', (1, 2))
ax.set_prop_cycle('linewidth', [1, 2])
ax.set_prop_cycle('linewidth', iter([1, 2]))
ax.set_prop_cycle('linewidth', np.array([1, 2]))
Expand Down Expand Up @@ -181,6 +181,13 @@ def test_invalid_input_forms():
'linewidth', {'1': 1, '2': 2})
assert_raises((TypeError, ValueError), ax.set_prop_cycle,
linewidth=1, color='r')
assert_raises((TypeError, ValueError), ax.set_prop_cycle, 'foobar', [1, 2])
assert_raises((TypeError, ValueError), ax.set_prop_cycle,
foobar=[1, 2])
assert_raises((TypeError, ValueError), ax.set_prop_cycle,
cycler(foobar=[1, 2]))
assert_raises(ValueError, ax.set_prop_cycle,
cycler(color='rgb', c='cmy'))


if __name__ == '__main__':
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/tests/test_rcparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ def test_validators():
("cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])",
(cycler('color', 'rgb') *
cycler('linestyle', ['-', '--']))),
(cycler('ls', ['-', '--']),
cycler('linestyle', ['-', '--'])),
(cycler(mew=[2, 5]),
cycler('markeredgewidth', [2, 5])),
),
# This is *so* incredibly important: validate_cycler() eval's
# an arbitrary string! I think I have it locked down enough,
Expand All @@ -343,6 +347,7 @@ def test_validators():
('cycler("waka", [1, 2, 3])', ValueError), # not a property
('cycler(c=[1, 2, 3])', ValueError), # invalid values
("cycler(lw=['a', 'b', 'c'])", ValueError), # invalid values
(cycler('waka', [1, 3, 5]), ValueError), # not a property
)
},
{'validator': validate_hatch,
Expand Down
2 changes: 1 addition & 1 deletion setupext.py
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,7 @@ def check(self):
return "using cycler version %s" % cycler.__version__

def get_install_requires(self):
return ['cycler']
return ['cycler>=0.9']


class Dateutil(SetupPackage):
Expand Down