Skip to content

Commit 23a9dc1

Browse files
authored
deprecate some positional arguments (#54)
* deprecate some positional arguments * add CHANGELOG
1 parent e4ee966 commit 23a9dc1

File tree

8 files changed

+314
-5
lines changed

8 files changed

+314
-5
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ mplotutils now uses the MIT license instead of GPL-3.0 ([#51](https://github.com
99

1010
* Deprecated `mpu.infer_interval_breaks` as this is no longer necessary with matplotlib v3.2
1111
and cartopy v0.21 ([#32](https://github.com/mathause/mplotutils/pull/32)).
12+
* Deprecated a number of positional arguments, these are now keyword only, e.g. in
13+
`mpu.colorbar` ([#54](https://github.com/mathause/mplotutils/pull/54)).
1214

1315
### Enhancements
1416

licenses/SCIKIT_LEARN_LICENSE

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2007-2021 The scikit-learn developers.
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
* Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
* Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

mplotutils/_colorbar.py

+4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import matplotlib.pyplot as plt
44
import numpy as np
55

6+
from mplotutils._deprecate import _deprecate_positional_args
67

8+
9+
@_deprecate_positional_args("0.3")
710
def colorbar(
811
mappable,
912
ax1,
1013
ax2=None,
14+
*,
1115
orientation="vertical",
1216
aspect=None,
1317
size=None,

mplotutils/_deprecate.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Adapted from scikit-learn https://github.com/scikit-learn/scikit-learn/pull/13311
2+
# For reference, here is a copy of their copyright notice:
3+
4+
# BSD 3-Clause License
5+
6+
# Copyright (c) 2007-2021 The scikit-learn developers.
7+
# All rights reserved.
8+
9+
# Redistribution and use in source and binary forms, with or without
10+
# modification, are permitted provided that the following conditions are met:
11+
12+
# * Redistributions of source code must retain the above copyright notice, this
13+
# list of conditions and the following disclaimer.
14+
15+
# * Redistributions in binary form must reproduce the above copyright notice,
16+
# this list of conditions and the following disclaimer in the documentation
17+
# and/or other materials provided with the distribution.
18+
19+
# * Neither the name of the copyright holder nor the names of its
20+
# contributors may be used to endorse or promote products derived from
21+
# this software without specific prior written permission.
22+
23+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
34+
import inspect
35+
import warnings
36+
from functools import wraps
37+
38+
POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD
39+
KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY
40+
POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY
41+
EMPTY = inspect.Parameter.empty
42+
43+
44+
def _deprecate_positional_args(version):
45+
"""Decorator for methods that issues warnings for positional arguments
46+
Using the keyword-only argument syntax in pep 3102, arguments after the
47+
``*`` will issue a warning when passed as a positional argument.
48+
49+
Parameters
50+
----------
51+
version : str
52+
version of the library when the positional arguments were deprecated
53+
54+
Examples
55+
--------
56+
Deprecate passing `b` as positional argument:
57+
>>> def func(a, b=1):
58+
... pass
59+
>>> @_deprecate_positional_args("v0.1.0")
60+
... def func(a, *, b=2):
61+
... pass
62+
>>> func(1, 2)
63+
64+
Notes
65+
-----
66+
This function is adapted from scikit-learn under the terms of its license. See
67+
"""
68+
69+
def _decorator(func):
70+
71+
signature = inspect.signature(func)
72+
73+
pos_or_kw_args = []
74+
kwonly_args = []
75+
for name, param in signature.parameters.items():
76+
if param.kind in (POSITIONAL_OR_KEYWORD, POSITIONAL_ONLY):
77+
pos_or_kw_args.append(name)
78+
elif param.kind == KEYWORD_ONLY:
79+
kwonly_args.append(name)
80+
if param.default is EMPTY:
81+
# IMHO `def f(a, *, b):` does not make sense -> disallow it
82+
# if removing this constraint -> need to add these to kwargs as well
83+
raise TypeError("Keyword-only param without default disallowed.")
84+
85+
@wraps(func)
86+
def inner(*args, **kwargs):
87+
88+
name = func.__name__
89+
n_extra_args = len(args) - len(pos_or_kw_args)
90+
if n_extra_args > 0:
91+
92+
extra_args = ", ".join(kwonly_args[:n_extra_args])
93+
94+
warnings.warn(
95+
f"Passing '{extra_args}' as positional argument(s) to {name} "
96+
f"was deprecated in version {version} and will raise an error two "
97+
"releases later. Please pass them as keyword arguments."
98+
"",
99+
FutureWarning,
100+
stacklevel=2,
101+
)
102+
103+
zip_args = zip(kwonly_args[:n_extra_args], args[-n_extra_args:])
104+
kwargs.update({name: arg for name, arg in zip_args})
105+
106+
return func(*args[:-n_extra_args], **kwargs)
107+
108+
return func(*args, **kwargs)
109+
110+
return inner
111+
112+
return _decorator

mplotutils/cartopy_utils.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import shapely.geometry
77
from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER
88

9-
from .colormaps import _get_label_attr
9+
from mplotutils._deprecate import _deprecate_positional_args
1010

11-
# =============================================================================
11+
from .colormaps import _get_label_attr
1212

1313

1414
def sample_data_map(nlons, nlats):
@@ -115,7 +115,8 @@ def cyclic_dataarray(obj, coord="lon"):
115115
return obj.pad({coord: (0, 1)}, mode="wrap")
116116

117117

118-
def ylabel_map(s, labelpad=None, size=None, weight=None, y=0.5, ax=None, **kwargs):
118+
@_deprecate_positional_args("0.3")
119+
def ylabel_map(s, *, labelpad=None, size=None, weight=None, y=0.5, ax=None, **kwargs):
119120
"""
120121
add ylabel to cartopy plot
121122
@@ -180,7 +181,8 @@ def ylabel_map(s, labelpad=None, size=None, weight=None, y=0.5, ax=None, **kwarg
180181
# =============================================================================
181182

182183

183-
def xlabel_map(s, labelpad=None, size=None, weight=None, x=0.5, ax=None, **kwargs):
184+
@_deprecate_positional_args("0.3")
185+
def xlabel_map(s, *, labelpad=None, size=None, weight=None, x=0.5, ax=None, **kwargs):
184186
"""
185187
add xlabel to cartopy plot
186188
@@ -245,8 +247,10 @@ def xlabel_map(s, labelpad=None, size=None, weight=None, x=0.5, ax=None, **kwarg
245247
# =============================================================================
246248

247249

250+
@_deprecate_positional_args("0.3")
248251
def yticklabels(
249252
y_ticks,
253+
*,
250254
labelpad=None,
251255
size=None,
252256
weight=None,
@@ -347,8 +351,10 @@ def yticklabels(
347351
)
348352

349353

354+
@_deprecate_positional_args("0.3")
350355
def xticklabels(
351356
x_ticks,
357+
*,
352358
labelpad=None,
353359
size=None,
354360
weight=None,

mplotutils/map_layout.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import matplotlib.pyplot as plt
22
import numpy as np
33

4+
from mplotutils._deprecate import _deprecate_positional_args
45

5-
def set_map_layout(axes, width=17.0, nrow=None, ncol=None):
6+
7+
@_deprecate_positional_args("0.3")
8+
def set_map_layout(axes, width=17.0, *, nrow=None, ncol=None):
69
"""set figure height, given width, taking axes' aspect ratio into account
710
811
Needs to be called after all plotting is done.

mplotutils/tests/test_colorbar.py

+13
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def test_parse_size_aspect_pad():
8282
# =============================================================================
8383

8484

85+
@pytest.mark.parametrize("orientation", ["vertical", "horizontal"])
86+
def test_colorbar_deprecate_positional(orientation):
87+
88+
with subplots_context(1, 2) as (f, axs):
89+
90+
h = axs[0].pcolormesh([[0, 1]])
91+
92+
with pytest.warns(
93+
FutureWarning, match="Passing 'orientation' as positional argument"
94+
):
95+
mpu.colorbar(h, axs[0], axs[0], orientation)
96+
97+
8598
def test_colorbar_different_figures():
8699

87100
with figure_context() as f1, figure_context() as f2:

mplotutils/tests/test_deprecate.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import pytest
2+
3+
from mplotutils._deprecate import _deprecate_positional_args
4+
5+
6+
def test_deprecate_positional_args_warns_for_function():
7+
@_deprecate_positional_args("v0.1")
8+
def f1(a, b, *, c="c", d="d"):
9+
return a, b, c, d
10+
11+
result = f1(1, 2)
12+
assert result == (1, 2, "c", "d")
13+
14+
result = f1(1, 2, c=3, d=4)
15+
assert result == (1, 2, 3, 4)
16+
17+
with pytest.warns(FutureWarning, match=r".*v0.1"):
18+
result = f1(1, 2, 3)
19+
assert result == (1, 2, 3, "d")
20+
21+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
22+
result = f1(1, 2, 3)
23+
assert result == (1, 2, 3, "d")
24+
25+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
26+
result = f1(1, 2, 3, 4)
27+
assert result == (1, 2, 3, 4)
28+
29+
@_deprecate_positional_args("v0.1")
30+
def f2(a="a", *, b="b", c="c", d="d"):
31+
return a, b, c, d
32+
33+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
34+
result = f2(1, 2)
35+
assert result == (1, 2, "c", "d")
36+
37+
@_deprecate_positional_args("v0.1")
38+
def f3(a, *, b="b", **kwargs):
39+
return a, b, kwargs
40+
41+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
42+
result = f3(1, 2)
43+
assert result == (1, 2, {})
44+
45+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
46+
result = f3(1, 2, f="f")
47+
assert result == (1, 2, {"f": "f"})
48+
49+
# @_deprecate_positional_args("v0.1")
50+
# def f4(a, /, *, b="b", **kwargs):
51+
# return a, b, kwargs
52+
53+
# result = f4(1)
54+
# assert result == (1, "b", {})
55+
56+
# result = f4(1, b=2, f="f")
57+
# assert result == (1, 2, {"f": "f"})
58+
59+
# with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
60+
# result = f4(1, 2, f="f")
61+
# assert result == (1, 2, {"f": "f"})
62+
63+
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
64+
65+
@_deprecate_positional_args("v0.1")
66+
def f5(a, *, b, c=3, **kwargs):
67+
pass
68+
69+
70+
def test_deprecate_positional_args_warns_for_class():
71+
class A1:
72+
@_deprecate_positional_args("v0.1")
73+
def method(self, a, b, *, c="c", d="d"):
74+
return a, b, c, d
75+
76+
result = A1().method(1, 2)
77+
assert result == (1, 2, "c", "d")
78+
79+
result = A1().method(1, 2, c=3, d=4)
80+
assert result == (1, 2, 3, 4)
81+
82+
with pytest.warns(FutureWarning, match=r".*v0.1"):
83+
result = A1().method(1, 2, 3)
84+
assert result == (1, 2, 3, "d")
85+
86+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
87+
result = A1().method(1, 2, 3)
88+
assert result == (1, 2, 3, "d")
89+
90+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
91+
result = A1().method(1, 2, 3, 4)
92+
assert result == (1, 2, 3, 4)
93+
94+
class A2:
95+
@_deprecate_positional_args("v0.1")
96+
def method(self, a=1, b=1, *, c="c", d="d"):
97+
return a, b, c, d
98+
99+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
100+
result = A2().method(1, 2, 3)
101+
assert result == (1, 2, 3, "d")
102+
103+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
104+
result = A2().method(1, 2, 3, 4)
105+
assert result == (1, 2, 3, 4)
106+
107+
class A3:
108+
@_deprecate_positional_args("v0.1")
109+
def method(self, a, *, b="b", **kwargs):
110+
return a, b, kwargs
111+
112+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
113+
result = A3().method(1, 2)
114+
assert result == (1, 2, {})
115+
116+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
117+
result = A3().method(1, 2, f="f")
118+
assert result == (1, 2, {"f": "f"})
119+
120+
# class A4:
121+
# @_deprecate_positional_args("v0.1")
122+
# def method(self, a, /, *, b="b", **kwargs):
123+
# return a, b, kwargs
124+
125+
# result = A4().method(1)
126+
# assert result == (1, "b", {})
127+
128+
# result = A4().method(1, b=2, f="f")
129+
# assert result == (1, 2, {"f": "f"})
130+
131+
# with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
132+
# result = A4().method(1, 2, f="f")
133+
# assert result == (1, 2, {"f": "f"})
134+
135+
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
136+
137+
class A5:
138+
@_deprecate_positional_args("v0.1")
139+
def __init__(self, a, *, b, c=3, **kwargs):
140+
pass

0 commit comments

Comments
 (0)