From dcb9b5d8a0bf9abd5a785ab17caf7302d762169d Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 30 Dec 2023 22:38:37 +0100 Subject: [PATCH] Cleanup compat code for obsolete versions of dependencies --- seaborn/_base.py | 6 +--- seaborn/_compat.py | 16 ---------- seaborn/_core/plot.py | 10 ++----- seaborn/_core/properties.py | 9 ++---- seaborn/_statistics.py | 6 ++-- seaborn/categorical.py | 20 ++++++------- seaborn/distributions.py | 6 ++-- seaborn/relational.py | 6 ++-- seaborn/utils.py | 55 +++------------------------------- tests/_core/test_plot.py | 2 +- tests/_core/test_properties.py | 14 ++++++++- tests/test_matrix.py | 9 +----- 12 files changed, 44 insertions(+), 115 deletions(-) diff --git a/seaborn/_base.py b/seaborn/_base.py index 9aa83d408e..e96954c07b 100644 --- a/seaborn/_base.py +++ b/seaborn/_base.py @@ -1160,11 +1160,7 @@ def _attach( # For categorical y, we want the "first" level to be at the top of the axis if self.var_types.get("y", None) == "categorical": for ax in ax_list: - try: - ax.yaxis.set_inverted(True) - except AttributeError: # mpl < 3.1 - if not ax.yaxis_inverted(): - ax.invert_yaxis() + ax.yaxis.set_inverted(True) # TODO -- Add axes labels diff --git a/seaborn/_compat.py b/seaborn/_compat.py index 190ec6b62b..5427b7c843 100644 --- a/seaborn/_compat.py +++ b/seaborn/_compat.py @@ -7,22 +7,6 @@ from seaborn.utils import _version_predates -def MarkerStyle(marker=None, fillstyle=None): - """ - Allow MarkerStyle to accept a MarkerStyle object as parameter. - - Supports matplotlib < 3.3.0 - https://github.com/matplotlib/matplotlib/pull/16692 - - """ - if isinstance(marker, mpl.markers.MarkerStyle): - if fillstyle is None: - return marker - else: - marker = marker.get_marker() - return mpl.markers.MarkerStyle(marker, fillstyle) - - def norm_from_scale(scale, norm): """Produce a Normalize object given a Scale and min/max domain limits.""" # This is an internal maplotlib function that simplifies things to access diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py index a563b6516e..c432056baf 100644 --- a/seaborn/_core/plot.py +++ b/seaborn/_core/plot.py @@ -858,7 +858,7 @@ def layout( # TODO def legend (ugh) - def theme(self, *args: dict[str, Any]) -> Plot: + def theme(self, config: dict[str, Any], /) -> Plot: """ Control the appearance of elements in the plot. @@ -880,13 +880,7 @@ def theme(self, *args: dict[str, Any]) -> Plot: """ new = self._clone() - # We can skip this whole block on Python 3.8+ with positional-only syntax - nargs = len(args) - if nargs != 1: - err = f"theme() takes 1 positional argument, but {nargs} were given" - raise TypeError(err) - - rc = mpl.RcParams(args[0]) + rc = mpl.RcParams(config) new._theme.update(rc) return new diff --git a/seaborn/_core/properties.py b/seaborn/_core/properties.py index 8658fd22c0..4e2df91b49 100644 --- a/seaborn/_core/properties.py +++ b/seaborn/_core/properties.py @@ -3,25 +3,20 @@ import warnings import numpy as np +from numpy.typing import ArrayLike from pandas import Series import matplotlib as mpl from matplotlib.colors import to_rgb, to_rgba, to_rgba_array +from matplotlib.markers import MarkerStyle from matplotlib.path import Path from seaborn._core.scales import Scale, Boolean, Continuous, Nominal, Temporal from seaborn._core.rules import categorical_order, variable_type -from seaborn._compat import MarkerStyle from seaborn.palettes import QUAL_PALETTES, color_palette, blend_palette from seaborn.utils import get_color_cycle from typing import Any, Callable, Tuple, List, Union, Optional -try: - from numpy.typing import ArrayLike -except ImportError: - # numpy<1.20.0 (Jan 2021) - ArrayLike = Any - RGBTuple = Tuple[float, float, float] RGBATuple = Tuple[float, float, float, float] ColorSpec = Union[RGBTuple, RGBATuple, str] diff --git a/seaborn/_statistics.py b/seaborn/_statistics.py index 01211c9533..40346b0269 100644 --- a/seaborn/_statistics.py +++ b/seaborn/_statistics.py @@ -25,6 +25,7 @@ class instantiation. """ from numbers import Number +from statistics import NormalDist import numpy as np import pandas as pd try: @@ -35,7 +36,7 @@ class instantiation. _no_scipy = True from .algorithms import bootstrap -from .utils import _check_argument, _normal_quantile_func +from .utils import _check_argument class KDE: @@ -627,7 +628,8 @@ def _compute_k(self, n): elif self.k_depth == "proportion": k = int(np.log2(n)) - int(np.log2(n * self.outlier_prop)) + 1 elif self.k_depth == "trustworthy": - point_conf = 2 * _normal_quantile_func(1 - self.trust_alpha / 2) ** 2 + normal_quantile_func = np.vectorize(NormalDist().inv_cdf) + point_conf = 2 * normal_quantile_func(1 - self.trust_alpha / 2) ** 2 k = int(np.log2(n / point_conf)) + 1 else: # Allow having k directly specified as input diff --git a/seaborn/categorical.py b/seaborn/categorical.py index ab9f9680c7..99a813b2a7 100644 --- a/seaborn/categorical.py +++ b/seaborn/categorical.py @@ -8,7 +8,9 @@ import pandas as pd import matplotlib as mpl +from matplotlib.cbook import normalize_kwargs from matplotlib.collections import PatchCollection +from matplotlib.markers import MarkerStyle from matplotlib.patches import Rectangle import matplotlib.pyplot as plt @@ -23,11 +25,9 @@ _default_color, _get_patch_legend_artist, _get_transform_functions, - _normalize_kwargs, _scatter_legend_artist, _version_predates, ) -from seaborn._compat import MarkerStyle from seaborn._statistics import ( EstimateAggregator, LetterValues, @@ -605,7 +605,7 @@ def plot_boxes( value_var = {"x": "y", "y": "x"}[self.orient] def get_props(element, artist=mpl.lines.Line2D): - return _normalize_kwargs(plot_kws.pop(f"{element}props", {}), artist) + return normalize_kwargs(plot_kws.pop(f"{element}props", {}), artist) if not fill and linewidth is None: linewidth = mpl.rcParams["lines.linewidth"] @@ -1175,7 +1175,7 @@ def plot_points( agg_var = {"x": "y", "y": "x"}[self.orient] iter_vars = ["hue"] - plot_kws = _normalize_kwargs(plot_kws, mpl.lines.Line2D) + plot_kws = normalize_kwargs(plot_kws, mpl.lines.Line2D) plot_kws.setdefault("linewidth", mpl.rcParams["lines.linewidth"] * 1.8) plot_kws.setdefault("markeredgewidth", plot_kws["linewidth"] * 0.75) plot_kws.setdefault("markersize", plot_kws["linewidth"] * np.sqrt(2 * np.pi)) @@ -2371,7 +2371,7 @@ def barplot( agg_cls = WeightedAggregator if "weight" in p.plot_data else EstimateAggregator aggregator = agg_cls(estimator, errorbar, n_boot=n_boot, seed=seed) - err_kws = {} if err_kws is None else _normalize_kwargs(err_kws, mpl.lines.Line2D) + err_kws = {} if err_kws is None else normalize_kwargs(err_kws, mpl.lines.Line2D) # Deprecations to remove in v0.15.0. err_kws, capsize = p._err_kws_backcompat(err_kws, errcolor, errwidth, capsize) @@ -2506,7 +2506,7 @@ def pointplot( agg_cls = WeightedAggregator if "weight" in p.plot_data else EstimateAggregator aggregator = agg_cls(estimator, errorbar, n_boot=n_boot, seed=seed) - err_kws = {} if err_kws is None else _normalize_kwargs(err_kws, mpl.lines.Line2D) + err_kws = {} if err_kws is None else normalize_kwargs(err_kws, mpl.lines.Line2D) # Deprecations to remove in v0.15.0. p._point_kwargs_backcompat(scale, join, kwargs) @@ -2848,7 +2848,7 @@ def catplot( color = desaturate(color, saturation) if kind in ["strip", "swarm"]: - kwargs = _normalize_kwargs(kwargs, mpl.collections.PathCollection) + kwargs = normalize_kwargs(kwargs, mpl.collections.PathCollection) kwargs["edgecolor"] = p._complement_color( kwargs.pop("edgecolor", default), color, p._hue_map ) @@ -3023,14 +3023,14 @@ def catplot( # Deprecations to remove in v0.15.0. # TODO Uncomment when removing deprecation backcompat # capsize = kwargs.pop("capsize", 0) - # err_kws = _normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D) + # err_kws = normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D) p._point_kwargs_backcompat( kwargs.pop("scale", deprecated), kwargs.pop("join", deprecated), kwargs ) err_kws, capsize = p._err_kws_backcompat( - _normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D), + normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D), None, errwidth=kwargs.pop("errwidth", deprecated), capsize=kwargs.pop("capsize", 0), @@ -3052,7 +3052,7 @@ def catplot( aggregator = agg_cls(estimator, errorbar, n_boot=n_boot, seed=seed) err_kws, capsize = p._err_kws_backcompat( - _normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D), + normalize_kwargs(kwargs.pop("err_kws", {}), mpl.lines.Line2D), errcolor=kwargs.pop("errcolor", deprecated), errwidth=kwargs.pop("errwidth", deprecated), capsize=kwargs.pop("capsize", 0), diff --git a/seaborn/distributions.py b/seaborn/distributions.py index e3d57c9a42..f8ec166cf4 100644 --- a/seaborn/distributions.py +++ b/seaborn/distributions.py @@ -10,6 +10,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.transforms as tx +from matplotlib.cbook import normalize_kwargs from matplotlib.colors import to_rgba from matplotlib.collections import LineCollection @@ -28,7 +29,6 @@ remove_na, _get_transform_functions, _kde_support, - _normalize_kwargs, _check_argument, _assign_default_kwargs, _default_color, @@ -171,7 +171,7 @@ def _artist_kws(self, kws, fill, element, multiple, color, alpha): """Handle differences between artists in filled/unfilled plots.""" kws = kws.copy() if fill: - kws = _normalize_kwargs(kws, mpl.collections.PolyCollection) + kws = normalize_kwargs(kws, mpl.collections.PolyCollection) kws.setdefault("facecolor", to_rgba(color, alpha)) if element == "bars": @@ -916,7 +916,7 @@ def plot_univariate_density( artist = mpl.collections.PolyCollection else: artist = mpl.lines.Line2D - plot_kws = _normalize_kwargs(plot_kws, artist) + plot_kws = normalize_kwargs(plot_kws, artist) # Input checking _check_argument("multiple", ["layer", "stack", "fill"], multiple) diff --git a/seaborn/relational.py b/seaborn/relational.py index 8d0d856021..bd5ecfdfd1 100644 --- a/seaborn/relational.py +++ b/seaborn/relational.py @@ -5,6 +5,7 @@ import pandas as pd import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.cbook import normalize_kwargs from ._base import ( VectorPlotter, @@ -14,7 +15,6 @@ _default_color, _deprecate_ci, _get_transform_functions, - _normalize_kwargs, _scatter_legend_artist, ) from ._statistics import EstimateAggregator, WeightedAggregator @@ -237,7 +237,7 @@ def plot(self, ax, kws): # gotten from the corresponding matplotlib function, and calling the # function will advance the axes property cycle. - kws = _normalize_kwargs(kws, mpl.lines.Line2D) + kws = normalize_kwargs(kws, mpl.lines.Line2D) kws.setdefault("markeredgewidth", 0.75) kws.setdefault("markeredgecolor", "w") @@ -400,7 +400,7 @@ def plot(self, ax, kws): if data.empty: return - kws = _normalize_kwargs(kws, mpl.collections.PathCollection) + kws = normalize_kwargs(kws, mpl.collections.PathCollection) # Define the vectors of x and y positions empty = np.full(len(data), np.nan) diff --git a/seaborn/utils.py b/seaborn/utils.py index 83527ba445..98720ba36d 100644 --- a/seaborn/utils.py +++ b/seaborn/utils.py @@ -55,29 +55,6 @@ def ci_to_errsize(cis, heights): return errsize -def _normal_quantile_func(q): - """ - Compute the quantile function of the standard normal distribution. - - This wrapper exists because we are dropping scipy as a mandatory dependency - but statistics.NormalDist was added to the standard library in 3.8. - - """ - try: - from statistics import NormalDist - qf = np.vectorize(NormalDist().inv_cdf) - except ImportError: - try: - from scipy.stats import norm - qf = norm.ppf - except ImportError: - msg = ( - "Standard normal quantile functions require either Python>=3.8 or scipy" - ) - raise RuntimeError(msg) - return qf(q) - - def _draw_figure(fig): """Force draw of a matplotlib figure, accounting for back-compat.""" # See https://github.com/matplotlib/matplotlib/issues/19197 for context @@ -110,7 +87,7 @@ def _default_color(method, hue, color, kws, saturation=1): elif method.__name__ == "plot": - color = _normalize_kwargs(kws, mpl.lines.Line2D).get("color") + color = normalize_kwargs(kws, mpl.lines.Line2D).get("color") scout, = method([], [], scalex=False, scaley=False, color=color) color = scout.get_color() scout.remove() @@ -155,7 +132,7 @@ def _default_color(method, hue, color, kws, saturation=1): elif method.__name__ == "fill_between": - kws = _normalize_kwargs(kws, mpl.collections.PolyCollection) + kws = normalize_kwargs(kws, mpl.collections.PolyCollection) scout = method([], [], **kws) facecolor = scout.get_facecolor() color = to_rgb(facecolor[0]) @@ -714,11 +691,7 @@ def get_view_interval(self): formatter.set_scientific(False) formatter.axis = dummy_axis() - # TODO: The following two lines should be replaced - # once pinned matplotlib>=3.1.0 with: - # formatted_levels = formatter.format_ticks(raw_levels) - formatter.set_locs(raw_levels) - formatted_levels = [formatter(x) for x in raw_levels] + formatted_levels = formatter.format_ticks(raw_levels) return raw_levels, formatted_levels @@ -774,26 +747,6 @@ def to_utf8(obj): return str(obj) -def _normalize_kwargs(kws, artist): - """Wrapper for mpl.cbook.normalize_kwargs that supports <= 3.2.1.""" - _alias_map = { - 'color': ['c'], - 'linewidth': ['lw'], - 'linestyle': ['ls'], - 'facecolor': ['fc'], - 'edgecolor': ['ec'], - 'markerfacecolor': ['mfc'], - 'markeredgecolor': ['mec'], - 'markeredgewidth': ['mew'], - 'markersize': ['ms'] - } - try: - kws = normalize_kwargs(kws, artist) - except AttributeError: - kws = normalize_kwargs(kws, _alias_map) - return kws - - def _check_argument(param, options, value, prefix=False): """Raise if value for param is not in options.""" if prefix and value is not None: @@ -905,7 +858,7 @@ def _version_predates(lib: ModuleType, version: str) -> bool: def _scatter_legend_artist(**kws): - kws = _normalize_kwargs(kws, mpl.collections.PathCollection) + kws = normalize_kwargs(kws, mpl.collections.PathCollection) edgecolor = kws.pop("edgecolor", None) rc = mpl.rcParams diff --git a/tests/_core/test_plot.py b/tests/_core/test_plot.py index cdf3e52cb5..5554ea650f 100644 --- a/tests/_core/test_plot.py +++ b/tests/_core/test_plot.py @@ -934,7 +934,7 @@ def test_theme_params(self): def test_theme_error(self): p = Plot() - with pytest.raises(TypeError, match=r"theme\(\) takes 1 positional"): + with pytest.raises(TypeError, match=r"theme\(\) takes 2 positional"): p.theme("arg1", "arg2") def test_theme_validation(self): diff --git a/tests/_core/test_properties.py b/tests/_core/test_properties.py index 93567482c3..c87dd918d0 100644 --- a/tests/_core/test_properties.py +++ b/tests/_core/test_properties.py @@ -3,6 +3,7 @@ import pandas as pd import matplotlib as mpl from matplotlib.colors import same_color, to_rgb, to_rgba +from matplotlib.markers import MarkerStyle import pytest from numpy.testing import assert_array_equal @@ -20,7 +21,7 @@ Marker, PointSize, ) -from seaborn._compat import MarkerStyle, get_colormap +from seaborn._compat import get_colormap from seaborn.palettes import color_palette @@ -358,6 +359,17 @@ class TestMarker(ObjectPropertyBase): values = ["o", (5, 2, 0), MarkerStyle("^")] standardized_values = [MarkerStyle(x) for x in values] + def assert_equal(self, a, b): + a_path, b_path = a.get_path(), b.get_path() + assert_array_equal(a_path.vertices, b_path.vertices) + assert_array_equal(a_path.codes, b_path.codes) + assert a_path.simplify_threshold == b_path.simplify_threshold + assert a_path.should_simplify == b_path.should_simplify + + assert a.get_joinstyle() == b.get_joinstyle() + assert a.get_transform().to_values() == b.get_transform().to_values() + assert a.get_fillstyle() == b.get_fillstyle() + def unpack(self, x): return ( x.get_path(), diff --git a/tests/test_matrix.py b/tests/test_matrix.py index 110f4f5c79..889e5da461 100644 --- a/tests/test_matrix.py +++ b/tests/test_matrix.py @@ -387,8 +387,6 @@ def test_heatmap_cbar(self): assert len(f.axes) == 2 plt.close(f) - @pytest.mark.xfail(mpl.__version__ == "3.1.1", - reason="matplotlib 3.1.1 bug") def test_heatmap_axes(self): ax = mat.heatmap(self.df_norm) @@ -443,10 +441,7 @@ def test_heatmap_inner_lines(self): def test_square_aspect(self): ax = mat.heatmap(self.df_norm, square=True) - obs_aspect = ax.get_aspect() - # mpl>3.3 returns 1 for setting "equal" aspect - # so test for the two possible equal outcomes - assert obs_aspect == "equal" or obs_aspect == 1 + npt.assert_equal(ax.get_aspect(), 1) def test_mask_validation(self): @@ -679,8 +674,6 @@ def test_dendrogram_plot(self): assert len(ax.collections[0].get_paths()) == len(d.dependent_coord) - @pytest.mark.xfail(mpl.__version__ == "3.1.1", - reason="matplotlib 3.1.1 bug") def test_dendrogram_rotate(self): kws = self.default_kws.copy() kws['rotate'] = True