8000 ENH: add kwarg normalization function to cbook · matplotlib/matplotlib@7131970 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7131970

Browse files
committed
ENH: add kwarg normalization function to cbook
This is the first step to centralizing the normalizing kwarg handling.
1 parent deea685 commit 7131970

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

lib/matplotlib/cbook.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,6 +2428,114 @@ def safe_first_element(obj):
24282428
return next(iter(obj))
24292429

24302430

2431+
def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(),
2432+
allowed=None):
2433+
"""Helper function to normalize kwarg inputs
2434+
2435+
The order they are resolved are:
2436+
2437+
1. aliasing
2438+
2. required
2439+
3. forbidden
2440+
4. allowed
2441+
2442+
This order means that only the canonical names need appear in
2443+
`allowed`, `forbidden`, `required`
2444+
2445+
Parameters
2446+
----------
2447+
2448+
alias_mapping, dict, optional
2449+
A mapping between a canonical name to a list of
2450+
aliases, in order of precedence from lowest to highest.
2451+
2452+
If the canonical value is not in the list it is assumed to have
2453+
the highest priority.
2454+
2455+
required : iterable, optional
2456+
A tuple of fields that must be in kwargs.
2457+
2458+
forbidden : iterable, optional
2459+
A list of keys which may not be in kwargs
2460+
2461+
allowed : tuple, optional
2462+
A tuple of allowed fields. If this not None, then raise if
2463+
`kw` contains any keys not in the union of `required`
2464+
and `allowed`. To allow only the required fields pass in
2465+
``()`` for `allowed`
2466+
2467+
Raises
2468+
------
2469+
TypeError
2470+
To match what python raises if invalid args/kwargs are passed to
2471+
a callable.
2472+
2473+
"""
2474+
# deal with default value of alias_mapping
2475+
if alias_mapping is None:
2476+
alias_mapping = dict()
2477+
2478+
# make a local so we can pop
2479+
kw = dict(kw)
2480+
# output dictionary
2481+
ret = dict()
2482+
2483+
# hit all alias mappings
2484+
for canonical, alias_list in six.iteritems(alias_mapping):
2485+
2486+
# the alias lists are ordered from lowest to highest priority
2487+
# so we know to use the last value in this list
2488+
tmp = []
2489+
seen = []
2490+
for a in alias_list:
2491+
try:
2492+
tmp.append(kw.pop(a))
2493+
seen.append(a)
2494+
except KeyError:
2495+
pass
2496+
# if canonical is not in the alias_list assume highest priority
2497+
if canonical not in alias_list:
2498+
try:
2499+
tmp.append(kw.pop(canonical))
2500+
seen.append(canonical)
2501+
except KeyError:
2502+
pass
2503+
# if we found anything in this set of aliases put it in the return
2504+
# dict
2505+
if tmp:
2506+
ret[canonical] = tmp[-1]
2507+
if len(tmp) > 1:
2508+
warnings.warn("Saw kwargs {seen!r} which are all aliases for "
2509+
"{canon!r}. Kept value from {used!r}".format(
2510+
seen=seen, canon=canonical, used=seen[-1]))
2511+
2512+
# at this point we know that all keys which are aliased are removed, update
2513+
# the return dictionary from the cleaned local copy of the input
2514+
ret.update(kw)
2515+
2516+
fail_keys = [k for k in required if k not in ret]
2517+
if fail_keys:
2518+
raise TypeError("The required keys {!r} "
2519+
"are not in kwargs".format(fail_keys))
2520+
2521+
fail_keys = [k for k in forbidden if k in ret]
2522+
if fail_keys:
2523+
raise TypeError("The forbidden keys {!r} "
2524+
"are in kwargs".format(fail_keys))
2525+
2526+
if allowed is not None:
2527+
allowed_set = set(required) | set(allowed)
2528+
fail_keys = [k for k in ret if k not in allowed_set]
2529+
if fail_keys:
2530+
raise TypeError("kwargs contains {keys!r} which are not in "
2531+
"the required {req!r} or "
2532+
"allowed {allow!r} keys".format(
2533+
keys=fail_keys, req=required,
2534+
allow=allowed))
2535+
2536+
return ret
2537+
2538+
24312539
def get_label(y, default_name):
24322540
try:
24332541
return y.name

lib/matplotlib/tests/test_cbook.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
unicode_literals)
33
import itertools
44
from weakref import ref
5+
import warnings
56

67
from matplotlib.externals import six
78

@@ -309,6 +310,67 @@ def dummy(self):
309310
pass
310311

311312

313+
def _kwarg_norm_helper(inp, expected, kwargs_to_norm, warn_count=0):
314+
315+
with warnings.catch_warnings(record=True) as w:
316+
warnings.simplefilter("always")
317+
assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
318+
assert len(w) == warn_count
319+
320+
321+
def _kwarg_norm_fail_helper(inp, kwargs_to_norm):
322+
assert_raises(TypeError, cbook.normalize_kwargs, inp, **kwargs_to_norm)
323+
324+
325+
def test_normalize_kwargs():
326+
fail_mapping = (
327+
({'a': 1}, {'forbidden': ('a')}),
328+
({'a': 1}, {'required': ('b')}),
329+
({'a': 1, 'b': 2}, {'required': ('a'), 'allowed': ()})
330+
)
331+
332+
for inp, kwargs in fail_mapping:
333+
yield _kwarg_norm_fail_helper, inp, kwargs
334+
335+
warn_passing_mapping = (
336+
({'a': 1, 'b': 2}, {'a': 1}, {'alias_mapping': {'a': ['b']}}, 1),
337+
({'a': 1, 'b': 2}, {'a': 1}, {'alias_mapping': {'a': ['b']},
338+
10000 'allowed': ('a',)}, 1),
339+
({'a': 1, 'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}, 1),
340+
341+
({'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'c': 3},
342+
{'alias_mapping': {'a': ['b']}, 'required': ('a', )}, 1),
343+
344+
)
345+
346+
for inp, exp, kwargs, wc in warn_passing_mapping:
347+
yield _kwarg_norm_helper, inp, exp, kwargs, wc
348+
349+
pass_mapping = (
350+
({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
351+
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
352+
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['b']},
353+
'forbidden': ('b', )}),
354+
355+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', ),
356+
'allowed': ('c', )}),
357+
358+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
359+
'allowed': ('c', )}),
360+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
361+
'allowed': ('a', 'c')}),
362+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
363+
'allowed': ()}),
364+
365+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c')}),
366+
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'allowed': ('a', 'c')}),
367+
368+
)
369+
370+
for inp, exp, kwargs in pass_mapping:
371+
yield _kwarg_norm_helper, inp, exp, kwargs
372+
373+
312374
def test_to_prestep():
313375
x = np.arange(4)
314376
y1 = np.arange(4)

0 commit comments

Comments
 (0)
0