Skip to content

Commit 72ca093

Browse files
authored
fix _deprecate_positional_args helper (#6967)
* fix _deprecate_positional_args helper * clean tests
1 parent 19b9e04 commit 72ca093

File tree

2 files changed

+93
-43
lines changed

2 files changed

+93
-43
lines changed

xarray/tests/test_deprecation_helpers.py

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,60 @@
55

66
def test_deprecate_positional_args_warns_for_function():
77
@_deprecate_positional_args("v0.1")
8-
def f1(a, b, *, c=1, d=1):
9-
pass
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)
1016

1117
with pytest.warns(FutureWarning, match=r".*v0.1"):
12-
f1(1, 2, 3)
18+
result = f1(1, 2, 3)
19+
assert result == (1, 2, 3, "d")
1320

1421
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
15-
f1(1, 2, 3)
22+
result = f1(1, 2, 3)
23+
assert result == (1, 2, 3, "d")
1624

1725
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
18-
f1(1, 2, 3, 4)
26+
result = f1(1, 2, 3, 4)
27+
assert result == (1, 2, 3, 4)
1928

2029
@_deprecate_positional_args("v0.1")
21-
def f2(a=1, *, b=1, c=1, d=1):
22-
pass
30+
def f2(a="a", *, b="b", c="c", d="d"):
31+
return a, b, c, d
2332

2433
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
25-
f2(1, 2)
34+
result = f2(1, 2)
35+
assert result == (1, 2, "c", "d")
2636

2737
@_deprecate_positional_args("v0.1")
28-
def f3(a, *, b=1, **kwargs):
29-
pass
38+
def f3(a, *, b="b", **kwargs):
39+
return a, b, kwargs
3040

3141
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
32-
f3(1, 2)
42+
result = f3(1, 2)
43+
assert result == (1, 2, {})
3344

34-
with pytest.raises(TypeError, match=r"Cannot handle positional-only params"):
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"})
3548

36-
@_deprecate_positional_args("v0.1")
37-
def f4(a, /, *, b=2, **kwargs):
38-
pass
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"})
3962

4063
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
4164

@@ -47,47 +70,71 @@ def f5(a, *, b, c=3, **kwargs):
4770
def test_deprecate_positional_args_warns_for_class():
4871
class A1:
4972
@_deprecate_positional_args("v0.1")
50-
def __init__(self, a, b, *, c=1, d=1):
51-
pass
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)
5281

5382
with pytest.warns(FutureWarning, match=r".*v0.1"):
54-
A1(1, 2, 3)
83+
result = A1().method(1, 2, 3)
84+
assert result == (1, 2, 3, "d")
5585

5686
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
57-
A1(1, 2, 3)
87+
result = A1().method(1, 2, 3)
88+
assert result == (1, 2, 3, "d")
5889

5990
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
60-
A1(1, 2, 3, 4)
91+
result = A1().method(1, 2, 3, 4)
92+
assert result == (1, 2, 3, 4)
6193

6294
class A2:
6395
@_deprecate_positional_args("v0.1")
64-
def __init__(self, a=1, b=1, *, c=1, d=1):
65-
pass
96+
def method(self, a=1, b=1, *, c="c", d="d"):
97+
return a, b, c, d
6698

6799
with pytest.warns(FutureWarning, match=r"Passing 'c' as positional"):
68-
A2(1, 2, 3)
100+
result = A2().method(1, 2, 3)
101+
assert result == (1, 2, 3, "d")
69102

70103
with pytest.warns(FutureWarning, match=r"Passing 'c, d' as positional"):
71-
A2(1, 2, 3, 4)
104+
result = A2().method(1, 2, 3, 4)
105+
assert result == (1, 2, 3, 4)
72106

73107
class A3:
74108
@_deprecate_positional_args("v0.1")
75-
def __init__(self, a, *, b=1, **kwargs):
76-
pass
109+
def method(self, a, *, b="b", **kwargs):
110+
return a, b, kwargs
77111

78112
with pytest.warns(FutureWarning, match=r"Passing 'b' as positional"):
79-
A3(1, 2)
113+
result = A3().method(1, 2)
114+
assert result == (1, 2, {})
80115

81-
with pytest.raises(TypeError, match=r"Cannot handle positional-only params"):
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"})
82119

83-
class A3:
84-
@_deprecate_positional_args("v0.1")
85-
def __init__(self, a, /, *, b=1, **kwargs):
86-
pass
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"})
87134

88135
with pytest.raises(TypeError, match=r"Keyword-only param without default"):
89136

90-
class A4:
137+
class A5:
91138
@_deprecate_positional_args("v0.1")
92139
def __init__(self, a, *, b, c=3, **kwargs):
93140
pass

xarray/util/deprecation_helpers.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,43 +71,46 @@ def func(a, *, b=2):
7171
licences/SCIKIT_LEARN_LICENSE
7272
"""
7373

74-
def _decorator(f):
74+
def _decorator(func):
7575

76-
signature = inspect.signature(f)
76+
signature = inspect.signature(func)
7777

7878
pos_or_kw_args = []
7979
kwonly_args = []
8080
for name, param in signature.parameters.items():
81-
if param.kind == POSITIONAL_OR_KEYWORD:
81+
if param.kind in (POSITIONAL_OR_KEYWORD, POSITIONAL_ONLY):
8282
pos_or_kw_args.append(name)
8383
elif param.kind == KEYWORD_ONLY:
8484
kwonly_args.append(name)
8585
if param.default is EMPTY:
8686
# IMHO `def f(a, *, b):` does not make sense -> disallow it
8787
# if removing this constraint -> need to add these to kwargs as well
8888
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
9289

93-
@wraps(f)
90+
@wraps(func)
9491
def inner(*args, **kwargs):
92+
93+
name = func.__name__
9594
n_extra_args = len(args) - len(pos_or_kw_args)
9695
if n_extra_args > 0:
9796

9897
extra_args = ", ".join(kwonly_args[:n_extra_args])
9998

10099
warnings.warn(
101-
f"Passing '{extra_args}' as positional argument(s) "
100+
f"Passing '{extra_args}' as positional argument(s) to {name} "
102101
f"was deprecated in version {version} and will raise an error two "
103102
"releases later. Please pass them as keyword arguments."
104103
"",
105104
FutureWarning,
105+
stacklevel=2,
106106
)
107107

108-
kwargs.update({name: arg for name, arg in zip(pos_or_kw_args, args)})
108+
zip_args = zip(kwonly_args[:n_extra_args], args[-n_extra_args:])
109+
kwargs.update({name: arg for name, arg in zip_args})
110+
111+
return func(*args[:-n_extra_args], **kwargs)
109112

110-
return f(**kwargs)
113+
return func(*args, **kwargs)
111114

112115
return inner
113116

0 commit comments

Comments
 (0)