Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b725bd
Bump the actions group across 1 directory with 2 updates
dependabot[bot] Jan 19, 2026
b9a5ae5
Remove outdated notion of property alias priority from docs.
anntzer Jan 20, 2026
5953dd1
Fix grammar: 'it would better' -> 'it would be better' in comment
bodhisilberling Jan 22, 2026
ec04898
reorg docs to remove misc section, consolidate and cleanup file struc…
story645 Jan 21, 2026
ed0f0f6
widgets.pyi: Add missing RadioButtons.index_selected type hint
doronbehar Dec 1, 2025
e42b312
CheckButtons: rename _checks to _buttons
doronbehar Jan 19, 2026
37aa7f8
*Buttons: use a shared _Buttons class
doronbehar Jan 19, 2026
cdc231f
*Buttons: use a semi-public self._buttons_ys variable
doronbehar Jan 19, 2026
81b2196
*Buttons: share more __init__ functionality
doronbehar Jan 19, 2026
9aae0a8
Fix formatting: add space after # in TODO comment
bodhisilberling Jan 23, 2026
0be344d
move doc build options into tables and tabs
story645 Jan 23, 2026
fce26f5
Remove mutable defaults from tool manager functions
QuLogic Jan 23, 2026
e721843
DOC: Use the old class-based autodoc with Sphinx 9
QuLogic Jan 23, 2026
71b6bf2
DOC: Fix cross-references to new _Buttons class
QuLogic Jan 24, 2026
deec641
FIX: Handle AxesWidget cleanup after failed init (#30935)
larrybradley Jan 24, 2026
f17ca37
TST: Fix warnings from Pillow for unavailable features
QuLogic Jan 22, 2026
9387f36
Speedup normalize_kwargs by storing aliases in a more practical format.
anntzer Jan 23, 2026
6361f6f
Improving error message for width and position type mismatch in viol…
hasanrashid Jan 25, 2026
6bea6f6
DOC: Make grammatical corrections [ci doc]
star1327p Jan 25, 2026
442b53a
DOCS: Fix typo in time array step size comment
vujadinovic Jan 26, 2026
4460ed3
DOC: bring the credits page a little more up-to-date [ci doc]
rcomer Jan 24, 2026
90dd742
Add type hint for fig_kw in subplots (#31033)
sanrishi Jan 27, 2026
6b499a4
mathtext: add support for unicode mathematics fonts
llohse Jan 29, 2026
3d98533
bundle STIX Two Math in tests
llohse Jan 29, 2026
d8bf6d2
inline _is_digit, etc.
llohse Jan 29, 2026
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
Prev Previous commit
Next Next commit
Speedup normalize_kwargs by storing aliases in a more practical format.
Directly store property alias information in `{"alias": "propety", ...}`
format, which greatly speeds up normalize_kwargs.

In order to not worry about two different dict formats (the new one, and
the old one, which was `{"property": ["alias", ...], ...}`) in
normalize_kwargs, deprecate support for directly passing an alias map --
callers should always pass an artist (or artist type) directly, which
hides the detail of the format of the alias map.  Also deprecate passing
`alias_mapping=None`, which doesn't really seem needed (unlike
`kw=None`, which is regularly used).

Whether the format of aliases passed to `_api.define_aliases` should
also be changed to match the new format is a separate question that
doesn't have to be addressed here (it can be changed later, being fully
private anyways).
  • Loading branch information
anntzer authored and llohse committed Feb 3, 2026
commit 9387f36f14573827250a05e6864c1b1e204d5b7e
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 @@ -3089,7 +3089,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