Skip to content

Commit 98deab5

Browse files
authored
decorator to deprecate positional arguments (#6910)
* decorator to deprecate positional arguments * disallow positional-only without default * add copyright notice [skip-ci]
1 parent 63d7eb9 commit 98deab5

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

licenses/SCIKIT_LEARN_LICENSE

Lines changed: 29 additions & 0 deletions
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
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import pytest
2+
3+
from xarray.util.deprecation_helpers 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=1, d=1):
9+
pass
10+
11+
with pytest.warns(FutureWarning, match=r".*v0.1"):
12+
f1(1, 2, 3)
13+
14+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
15+
f1(1, 2, 3)
16+
17+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
18+
f1(1, 2, 3, 4)
19+
20+
@_deprecate_positional_args("v0.1")
21+
def f2(a=1, *, b=1, c=1, d=1):
22+
pass
23+
24+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
25+
f2(1, 2)
26+
27+
@_deprecate_positional_args("v0.1")
28+
def f3(a, *, b=1, **kwargs):
29+
pass
30+
31+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
32+
f3(1, 2)
33+
34+
with pytest.raises(TypeError, match=r"Cannot handle positional-only params"):
35+
36+
@_deprecate_positional_args("v0.1")
37+
def f4(a, /, *, b=2, **kwargs):
38+
pass
39+
40+
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
41+
42+
@_deprecate_positional_args("v0.1")
43+
def f5(a, *, b, c=3, **kwargs):
44+
pass
45+
46+
47+
def test_deprecate_positional_args_warns_for_class():
48+
class A1:
49+
@_deprecate_positional_args("v0.1")
50+
def __init__(self, a, b, *, c=1, d=1):
51+
pass
52+
53+
with pytest.warns(FutureWarning, match=r".*v0.1"):
54+
A1(1, 2, 3)
55+
56+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
57+
A1(1, 2, 3)
58+
59+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
60+
A1(1, 2, 3, 4)
61+
62+
class A2:
63+
@_deprecate_positional_args("v0.1")
64+
def __init__(self, a=1, b=1, *, c=1, d=1):
65+
pass
66+
67+
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
68+
A2(1, 2, 3)
69+
70+
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
71+
A2(1, 2, 3, 4)
72+
73+
class A3:
74+
@_deprecate_positional_args("v0.1")
75+
def __init__(self, a, *, b=1, **kwargs):
76+
pass
77+
78+
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
79+
A3(1, 2)
80+
81+
with pytest.raises(TypeError, match=r"Cannot handle positional-only params"):
82+
83+
class A3:
84+
@_deprecate_positional_args("v0.1")
85+
def __init__(self, a, /, *, b=1, **kwargs):
86+
pass
87+
88+
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
89+
90+
class A4:
91+
@_deprecate_positional_args("v0.1")
92+
def __init__(self, a, *, b, c=3, **kwargs):
93+
pass

xarray/util/deprecation_helpers.py

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

0 commit comments

Comments
 (0)