Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9cc1d18
Add Matplotlib colormap compatibility shim
C-Achard Jul 13, 2026
9518b9d
Allow custom warning stacklevel in deprecated
C-Achard Jul 13, 2026
bd7fdbb
Use shared get_colormap helper everywhere
C-Achard Jul 13, 2026
4bf4921
Merge branch 'main' into cy/init-mpl-version-change
C-Achard Jul 16, 2026
a5d5221
Merge remote-tracking branch 'upstream/main' into cy/init-mpl-version…
deruyter92 Aug 9, 2026
d5945cc
Fix get_colormap_names() legacy fallback to call _legacy_get_colormap…
deruyter92 Aug 9, 2026
d77d4f8
update version for "deprecated since .." warning
deruyter92 Aug 9, 2026
5e739e2
Add name override to deprecated() for legacy shim warnings
deruyter92 Aug 9, 2026
e6cfac6
Rename matplotlib_future_mode.py → matplotlib_compat.py
deruyter92 Aug 9, 2026
d9288fc
add tests for matplotlib compat layer
deruyter92 Aug 9, 2026
350e209
Import matplotlib.cm explicitly in the compat layer
C-Achard Sep 4, 2026
54ac2ac
Prevent duplicate legacy colormap registration
C-Achard Sep 4, 2026
b1c97fb
Route all plt.get_cmap calls throught the compat layer
C-Achard Sep 4, 2026
c1cf140
Use compat helper to clear matplotlib artists
C-Achard Sep 4, 2026
0e4a691
Patch matplotlib's Axes logger by name
C-Achard Sep 4, 2026
d3472c3
Test the name and stacklevel options of @deprecated
C-Achard Sep 4, 2026
0be8837
Expand matplotlib compat test coverage
C-Achard Sep 4, 2026
fe9d488
Remove unused compat helpers
C-Achard Sep 4, 2026
304bcc6
Remove unecessary tests
C-Achard Sep 4, 2026
aa007f6
Rename matplotlib compat module to utils
C-Achard Sep 4, 2026
0584d53
Replace matplotlib_compat colormap helpers
C-Achard Sep 4, 2026
1c15225
Simplify Matplotlib axes logger helper
C-Achard Sep 4, 2026
8a0ea71
Fix Matplotlib artist iteration
C-Achard Sep 4, 2026
426943f
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 8, 2026
31380eb
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 8, 2026
de29151
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 9, 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
10 changes: 8 additions & 2 deletions deeplabcut/core/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def deprecated(
replacement: str | None = None,
since: str | None = None,
removed_in: str | None = None,
stacklevel: int = 2,
name: str | None = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Mark a function as deprecated.

Expand All @@ -93,12 +95,16 @@ def deprecated(
``"deeplabcut.utils.auxfun_videos.list_videos_in_folder"``.
since: Version in which the function was deprecated.
removed_in: Version in which the function will be removed.
stacklevel: Stack level for the warning. Default is 2, which points to the
caller of the deprecated function. Increase this if you wrap calls in
an extra legacy wrapper function that you don't want to be blamed for the deprecation.
name: Name to report in the warning message. Defaults to ``__qualname__``.
"""

def decorator(fn: Callable[P, R]) -> Callable[P, R]:
info = DeprecationInfo(
kind="callable",
target=fn.__qualname__,
target=name or fn.__qualname__,
replacement=replacement,
since=since,
removed_in=removed_in,
Expand All @@ -107,7 +113,7 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]:

@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
warnings.warn(message, DLCDeprecationWarning, stacklevel=2)
warnings.warn(message, DLCDeprecationWarning, stacklevel=stacklevel)
return fn(*args, **kwargs)

wrapper.__doc__ = f"Deprecated. {message}\n\n" + (fn.__doc__ or "")
Expand Down
2 changes: 1 addition & 1 deletion deeplabcut/core/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def _filename(map_name) -> str:
if paf_all_in_one:
inds = [elem[0] for elem in edge_list]
n_inds = len(inds)
cmap = plt.cm.get_cmap(paf_colormap, n_inds)
cmap = plt.get_cmap(paf_colormap, n_inds)
colors = cmap(range(n_inds))
fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors)
fig3.savefig(output_folder / _filename("paf"))
Expand Down
4 changes: 2 additions & 2 deletions deeplabcut/gui/tracklet_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def reconnect(self):
class TrackletVisualizer:
def __init__(self, manager, videoname, trail_len=50):
self.manager = manager
self.cmap = plt.cm.get_cmap(manager.cfg["colormap"], len(set(manager.tracklet2id)))
self.cmap = plt.get_cmap(manager.cfg["colormap"], len(set(manager.tracklet2id)))
self.videoname = videoname
self.video = VideoReader(videoname)
self.nframes = len(self.video)
Expand Down Expand Up @@ -744,7 +744,7 @@ def on_click(self, event):
self.clean_collections()

def clean_collections(self):
for coll in self.ax2.collections + self.ax3.collections + self.ax_slider.collections:
for coll in [*self.ax2.collections, *self.ax3.collections, *self.ax_slider.collections]:
coll.remove()

def display_points(self, val):
Expand Down
2 changes: 1 addition & 1 deletion deeplabcut/modelzoo/generalized_data_converter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def customized_colormap(config_path):

import matplotlib.pyplot as plt

cmap = plt.cm.get_cmap("rainbow", n_bodyparts)
cmap = plt.get_cmap("rainbow", n_bodyparts)

colors = [cmap(i) for i in range(n_bodyparts)]

Expand Down
4 changes: 2 additions & 2 deletions deeplabcut/pose_estimation_3d/camera_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes._axes import _log as matplotlib_axes_logger

from deeplabcut.utils import auxiliaryfunctions, auxiliaryfunctions_3d
from deeplabcut.utils.matplotlib_utils import silence_axes_logger

matplotlib_axes_logger.setLevel("ERROR")
silence_axes_logger()


def calibrate_cameras(
Expand Down
8 changes: 4 additions & 4 deletions deeplabcut/pose_estimation_3d/plotting3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import pandas as pd
from matplotlib import gridspec
from matplotlib.animation import FFMpegWriter
from matplotlib.axes._axes import _log as matplotlib_axes_logger
from matplotlib.collections import LineCollection
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from tqdm import tqdm
Expand All @@ -27,8 +26,9 @@
make_labeled_video,
)
from deeplabcut.utils.auxfun_videos import VideoReader
from deeplabcut.utils.matplotlib_utils import silence_axes_logger

matplotlib_axes_logger.setLevel("ERROR")
silence_axes_logger()


def set_up_grid(figsize, xlim, ylim, zlim, view):
Expand Down Expand Up @@ -235,11 +235,11 @@ def create_labeled_video_3d(
ind_links = tuple(zip(*links, strict=False))

if color_by == "bodypart":
color = plt.cm.get_cmap(cmap, len(bodyparts2plot))
color = plt.get_cmap(cmap, len(bodyparts2plot))
colors_ = color(range(len(bodyparts2plot)))
colors = np.tile(colors_, (num_animals, 1))
elif color_by == "individual":
color = plt.cm.get_cmap(cmap, num_animals)
color = plt.get_cmap(cmap, num_animals)
colors_ = color(range(num_animals))
colors = np.repeat(colors_, len(bodyparts2plot), axis=0)

Expand Down
2 changes: 1 addition & 1 deletion deeplabcut/pose_estimation_pytorch/apis/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def create_labeled_images(

color_by_individual = mode == "individual"
if isinstance(cmap, str):
cmap = plt.cm.get_cmap(cmap)
cmap = plt.get_cmap(cmap)

for image_path, image_predictions in predictions.items():
# Load frame
Expand Down
2 changes: 1 addition & 1 deletion deeplabcut/pose_estimation_tensorflow/visualizemaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def extract_save_all_maps(
else:
inds = [elem[0] for elem in list_of_inds]
n_inds = len(inds)
cmap = plt.cm.get_cmap(cfg["colormap"], n_inds)
cmap = plt.get_cmap(cfg["colormap"], n_inds)
colors = cmap(range(n_inds))
fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors)
temp = dest_path.format(
Expand Down
27 changes: 27 additions & 0 deletions deeplabcut/utils/matplotlib_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# DeepLabCut Toolbox (deeplabcut.org)
# © A. & M.W. Mathis Labs
# https://github.com/DeepLabCut/DeepLabCut
#
# Please see AUTHORS for contributors.
# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS
#
# Licensed under GNU Lesser General Public License v3.0
#
from __future__ import annotations

import logging

AXES_LOGGER_NAME = "matplotlib.axes._axes"


def silence_axes_logger(level: int | str = "ERROR") -> None:
"""Raise the log level of Matplotlib's ``Axes`` logger.

Suppresses the chatty per-artist messages Matplotlib emits while drawing,
most notably the invalid-color warnings raised during 3D plotting.

Args:
level: Any level accepted by ``logging.Logger.setLevel``.
"""
logging.getLogger(AXES_LOGGER_NAME).setLevel(level)
4 changes: 2 additions & 2 deletions deeplabcut/utils/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_cmap(n: int, name: str = "hsv") -> Colormap:
A function that maps each index in 0, 1, ..., n-1 to a distinct
RGB color; the keyword argument name must be a standard mpl colormap name.
"""
return plt.cm.get_cmap(name, n)
return plt.get_cmap(name, n)


def make_labeled_image(
Expand Down Expand Up @@ -284,7 +284,7 @@ def create_minimal_figure(dpi=100):


def erase_artists(ax):
for artist in ax.lines + ax.collections + ax.artists + ax.patches + ax.images:
for artist in [*ax.lines, *ax.collections, *ax.artists, *ax.patches, *ax.images]:
artist.remove()
ax.figure.canvas.draw_idle()

Expand Down
47 changes: 47 additions & 0 deletions tests/utils/test_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#
# Licensed under GNU Lesser General Public License v3.0
#
import inspect
import warnings

import pytest
Expand Down Expand Up @@ -97,6 +98,52 @@ def old_fn():
assert info.removed_in == Version("4.0")


def test_deprecated_name_overrides_reported_target():
"""``name`` reports the public API, not the private shim implementing it."""

@deprecated(name="public_api")
def _private_shim():
pass

with pytest.warns(DLCDeprecationWarning, match=r"^public_api") as record:
_private_shim()

assert "_private_shim" not in str(record[0].message)
assert _private_shim.__deprecated_info__.target == "public_api"


def test_deprecated_default_stacklevel_blames_immediate_caller():
@deprecated()
def legacy_shim():
pass

def public_wrapper():
legacy_shim()

with pytest.warns(DLCDeprecationWarning) as record:
public_wrapper()

# stacklevel=2 stops inside public_wrapper, on the line calling the shim.
assert record[0].lineno == public_wrapper.__code__.co_firstlineno + 1


def test_deprecated_stacklevel_can_blame_the_callers_caller():
"""``stacklevel=3`` skips a wrapper that should not be blamed for the warning."""

@deprecated(stacklevel=3)
def legacy_shim():
pass

def public_wrapper():
legacy_shim()

with pytest.warns(DLCDeprecationWarning) as record:
call_line = inspect.currentframe().f_lineno + 1
public_wrapper()

assert record[0].lineno == call_line


def test_deprecated_invalid_since_raises():
with pytest.raises(ValueError, match="Invalid version"):

Expand Down
49 changes: 49 additions & 0 deletions tests/utils/test_matplotlib_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#
# DeepLabCut Toolbox (deeplabcut.org)
# © A. & M.W. Mathis Labs
# https://github.com/DeepLabCut/DeepLabCut
#
# Please see AUTHORS for contributors.
# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS
#
# Licensed under GNU Lesser General Public License v3.0
#
"""Tests for ``deeplabcut.utils.matplotlib_utils``."""

from __future__ import annotations

import logging

import pytest

from deeplabcut.utils import matplotlib_utils as mu


@pytest.fixture
def axes_logger():
logger = logging.getLogger(mu.AXES_LOGGER_NAME)
original = logger.level
yield logger
logger.setLevel(original)


def test_axes_logger_name_matches_matplotlibs_own_logger():
"""``AXES_LOGGER_NAME`` must resolve to the logger Matplotlib actually uses.

``silence_axes_logger`` reaches that logger by name instead of importing
``_log``, so a module rename upstream would silently stop suppressing
messages rather than raising. This is the assertion that turns such a
rename back into a loud failure, and the one place where importing the
private module is the point.
"""
from matplotlib.axes._axes import _log

assert logging.getLogger(mu.AXES_LOGGER_NAME) is _log


def test_silence_axes_logger_sets_the_level(axes_logger):
axes_logger.setLevel(logging.DEBUG)

mu.silence_axes_logger()

assert axes_logger.level == logging.ERROR
Loading