Skip to content

ENH: add kwarg normalization function to cbook #5975

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 1 commit into from
Feb 15, 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
108 changes: 108 additions & 0 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,114 @@ def safe_first_element(obj):
return next(iter(obj))


def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(),
allowed=None):
"""Helper function to normalize kwarg inputs

The order they are resolved are:

1. aliasing
2. required
3. forbidden
4. allowed

This order means that only the canonical names need appear in
`allowed`, `forbidden`, `required`

Parameters
----------

alias_mapping, dict, optional
A mapping between a canonical name to a list of
aliases, in order of precedence from lowest to highest.

If the canonical value is not in the list it is assumed to have
the highest priority.

required : iterable, optional
A tuple of fields that must be in kwargs.

forbidden : iterable, optional
A list of keys which may not be in kwargs

allowed : tuple, optional
A tuple of allowed fields. If this not None, then raise if
`kw` contains any keys not in the union of `required`
and `allowed`. To allow only the required fields pass in
``()`` for `allowed`

Raises
------
TypeError
To match what python raises if invalid args/kwargs are passed to
a callable.

"""
# deal with default value of alias_mapping
if alias_mapping is None:
alias_mapping = dict()

# make a local so we can pop
kw = dict(kw)
# output dictionary
ret = dict()

# hit all alias mappings
for canonical, alias_list in six.iteritems(alias_mapping):

# the alias lists are ordered from lowest to highest priority
# so we know to use the last value in this list
tmp = []
seen = []
for a in alias_list:
try:
tmp.append(kw.pop(a))
seen.append(a)
except KeyError:
pass
# if canonical is not in the alias_list assume highest priority
if canonical not in alias_list:
try:
tmp.append(kw.pop(canonical))
seen.append(canonical)
except KeyError:
pass
# if we found anything in this set of aliases put it in the return
# dict
if tmp:
ret[canonical] = tmp[-1]
if len(tmp) > 1:
warnings.warn("Saw kwargs {seen!r} which are all aliases for "
"{canon!r}. Kept value from {used!r}".format(
seen=seen, canon=canonical, used=seen[-1]))

# at this point we know that all keys which are aliased are removed, update
# the return dictionary from the cleaned local copy of the input
ret.update(kw)

fail_keys = [k for k in required if k not in ret]
if fail_keys:
raise TypeError("The required keys {!r} "
"are not in kwargs".format(fail_keys))

fail_keys = [k for k in forbidden if k in ret]
if fail_keys:
raise TypeError("The forbidden keys {!r} "
"are in kwargs".format(fail_keys))

if allowed is not None:
allowed_set = set(required) | set(allowed)
fail_keys = [k for k in ret if k not in allowed_set]
if fail_keys:
raise TypeError("kwargs contains {keys!r} which are not in "
"the required {req!r} or "
"allowed {allow!r} keys".format(
keys=fail_keys, req=required,
allow=allowed))

return ret


def get_label(y, default_name):
try:
return y.name
Expand Down
62 changes: 62 additions & 0 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
unicode_literals)
import itertools
from weakref import ref
import warnings

from matplotlib.externals import six

Expand Down Expand Up @@ -309,6 +310,67 @@ def dummy(self):
pass


def _kwarg_norm_helper(inp, expected, kwargs_to_norm, warn_count=0):

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
assert len(w) == warn_count


def _kwarg_norm_fail_helper(inp, kwargs_to_norm):
assert_raises(TypeError, cbook.normalize_kwargs, inp, **kwargs_to_norm)


def test_normalize_kwargs():
fail_mapping = (
({'a': 1}, {'forbidden': ('a')}),
({'a': 1}, {'required': ('b')}),
({'a': 1, 'b': 2}, {'required': ('a'), 'allowed': ()})
)

for inp, kwargs in fail_mapping:
yield _kwarg_norm_fail_helper, inp, kwargs

warn_passing_mapping = (
({'a': 1, 'b': 2}, {'a': 1}, {'alias_mapping': {'a': ['b']}}, 1),
({'a': 1, 'b': 2}, {'a': 1}, {'alias_mapping': {'a': ['b']},
'allowed': ('a',)}, 1),
({'a': 1, 'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}, 1),

({'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'c': 3},
{'alias_mapping': {'a': ['b']}, 'required': ('a', )}, 1),

)

for inp, exp, kwargs, wc in warn_passing_mapping:
yield _kwarg_norm_helper, inp, exp, kwargs, wc

pass_mapping = (
({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['b']},
'forbidden': ('b', )}),

({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', ),
'allowed': ('c', )}),

({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
'allowed': ('c', )}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
'allowed': ('a', 'c')}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c'),
'allowed': ()}),

({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c')}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'allowed': ('a', 'c')}),

)

for inp, exp, kwargs in pass_mapping:
yield _kwarg_norm_helper, inp, exp, kwargs


def test_to_prestep():
x = np.arange(4)
y1 = np.arange(4)
Expand Down