Skip to content
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
4 changes: 4 additions & 0 deletions doc/api/next_api_changes/deprecations/31023-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``cbook.normalize_kwargs`` only supports passing artists and artist classes as second argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for directly passing an alias mapping or None as second argument to
`.cbook.normalize_kwargs` has been deprecated.
16 changes: 10 additions & 6 deletions lib/matplotlib/_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,9 @@ class so far, an alias named ``get_alias`` will be defined; the same will
be done for setters. If neither the getter nor the setter exists, an
exception will be raised.

The alias map is stored as the ``_alias_map`` attribute on the class and
can be used by `.normalize_kwargs`.
The alias map is stored as the ``_alias_to_prop`` attribute under the format
``{"alias": "property", ...}` on the class, and can be used by
`.normalize_kwargs`.
"""
if cls is None: # Return the actual class decorator.
return functools.partial(define_aliases, alias_d)
Expand All @@ -295,17 +296,20 @@ def method(self, *args, **kwargs):
raise ValueError(
f"Neither getter nor setter exists for {prop!r}")

alias_to_prop = {
alias: prop for prop, aliases in alias_d.items() for alias in aliases}

def get_aliased_and_aliases(d):
return {*d, *(alias for aliases in d.values() for alias in aliases)}
return {*d.keys(), *d.values()}

preexisting_aliases = getattr(cls, "_alias_map", {})
preexisting_aliases = getattr(cls, "_alias_to_prop", {})
conflicting = (get_aliased_and_aliases(preexisting_aliases)
& get_aliased_and_aliases(alias_d))
& get_aliased_and_aliases(alias_to_prop))
if conflicting:
# Need to decide on conflict resolution policy.
raise NotImplementedError(
f"Parent class already defines conflicting aliases: {conflicting}")
cls._alias_map = {**preexisting_aliases, **alias_d}
cls._alias_to_prop = {**preexisting_aliases, **alias_to_prop}
return cls


Expand Down
38 changes: 21 additions & 17 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ def normalize_kwargs(kw, alias_mapping=None):
as an empty dict, to support functions with an optional parameter of
the form ``props=None``.

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

Expand All @@ -1849,31 +1849,35 @@ def normalize_kwargs(kw, alias_mapping=None):
"""
from matplotlib.artist import Artist

# deal with default value of alias_mapping
if (isinstance(alias_mapping, type) and issubclass(alias_mapping, Artist)
or isinstance(alias_mapping, Artist)):
alias_to_prop = getattr(alias_mapping, "_alias_to_prop", {})
else:
if alias_mapping is None:
alias_mapping = {}
_api.warn_deprecated("3.11", message=(
"Passing a dict or None as alias_mapping to normalize_kwargs is "
"deprecated since %(since)s and support will be removed "
"%(removal)s; pass an Artist instance or type instead."))
# Convert old format to new format.
alias_to_prop = {alias: prop for prop, aliases in alias_mapping.items()
for alias in aliases}

if kw is None:
return {}

# deal with default value of alias_mapping
if alias_mapping is None:
alias_mapping = {}
elif (isinstance(alias_mapping, type) and issubclass(alias_mapping, Artist)
or isinstance(alias_mapping, Artist)):
alias_mapping = getattr(alias_mapping, "_alias_map", {})
canonicalized = {alias_to_prop.get(k, k): v for k, v in kw.items()}
if len(canonicalized) == len(kw):
return canonicalized

to_canonical = {alias: canonical
for canonical, alias_list in alias_mapping.items()
for alias in alias_list}
canonical_to_seen = {}
ret = {} # output dictionary

for k, v in kw.items():
canonical = to_canonical.get(k, k)
for k in kw:
canonical = alias_to_prop.get(k, k)
if canonical in canonical_to_seen:
raise TypeError(f"Got both {canonical_to_seen[canonical]!r} and "
f"{k!r}, which are aliases of one another")
canonical_to_seen[canonical] = k
ret[canonical] = v

return ret


@contextlib.contextmanager
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/cbook.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def sanitize_sequence(data): ...
def _resize_sequence(seq: Sequence, N: int) -> Sequence: ...
def normalize_kwargs(
kw: dict[str, Any],
alias_mapping: dict[str, list[str]] | type[Artist] | Artist | None = ...,
alias_mapping: type[Artist] | Artist | None = ...,
) -> dict[str, Any]: ...
def _lock_path(path: str | os.PathLike) -> contextlib.AbstractContextManager[None]: ...
def _str_equal(obj: Any, s: str) -> bool: ...
Expand Down
49 changes: 31 additions & 18 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
assert_array_almost_equal)
import pytest

import matplotlib as mpl
from matplotlib import _api, cbook
import matplotlib.colors as mcolors
from matplotlib.cbook import delete_masked_points, strip_math
Expand Down Expand Up @@ -480,30 +481,42 @@ def test_resize_sequence():
assert_array_equal(cbook._resize_sequence(arr, 5), [1, 2, 3, 1, 2])


fail_mapping: tuple[tuple[dict, dict], ...] = (
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}}),
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
)
fail_mapping: list[tuple[dict, dict]] = [
({"a": 1, "b": 2}, {"a": ["b"]}),
({"a": 1, "b": 2}, {"a": ["a", "b"]}),
]

pass_mapping: tuple[tuple[Any, dict, dict], ...] = (
pass_mapping: list[tuple[Any, dict, dict]] = [
(None, {}, {}),
({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
)
({"a": 1, "b": 2}, {"a": 1, "b": 2}, {}),
({"b": 2}, {"a": 2}, {"a": ["a", "b"]}),
]


@pytest.mark.parametrize('inp, kwargs_to_norm', fail_mapping)
def test_normalize_kwargs_fail(inp, kwargs_to_norm):
with pytest.raises(TypeError), _api.suppress_matplotlib_deprecation_warning():
cbook.normalize_kwargs(inp, **kwargs_to_norm)
@pytest.mark.parametrize('inp, alias_def', fail_mapping)
def test_normalize_kwargs_fail(inp, alias_def):

@_api.define_aliases(alias_def)
class Type(mpl.artist.Artist):
def get_a(self): return None

@pytest.mark.parametrize('inp, expected, kwargs_to_norm',
pass_mapping)
def test_normalize_kwargs_pass(inp, expected, kwargs_to_norm):
with _api.suppress_matplotlib_deprecation_warning():
# No other warning should be emitted.
assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
with pytest.raises(TypeError):
cbook.normalize_kwargs(inp, Type)


@pytest.mark.parametrize('inp, expected, alias_def', pass_mapping)
def test_normalize_kwargs_pass(inp, expected, alias_def):

@_api.define_aliases(alias_def)
class Type(mpl.artist.Artist):
def get_a(self): return None

assert expected == cbook.normalize_kwargs(inp, Type)
old_alias_map = {}
for alias, prop in Type._alias_to_prop.items():
old_alias_map.setdefault(prop, []).append(alias)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert expected == cbook.normalize_kwargs(inp, old_alias_map)


def test_warn_external(recwarn):
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3090,7 +3090,7 @@ def __init__(self, ax, x, y, *, marker='o', marker_props=None, useblit=True):
props = {'marker': marker, 'markersize': 7, 'markerfacecolor': 'w',
'linestyle': 'none', 'alpha': 0.5, 'visible': False,
'label': '_nolegend_',
**cbook.normalize_kwargs(marker_props, Line2D._alias_map)}
**cbook.normalize_kwargs(marker_props, Line2D)}
self._markers = Line2D(x, y, animated=useblit, **props)
self.ax.add_line(self._markers)

Expand Down
Loading