Skip to content

Commit 340dca1

Browse files
kszucscpcloud
authored andcommitted
feat(common): support positional only and keyword only arguments in annotations
1 parent baea1fa commit 340dca1

File tree

4 files changed

+65
-29
lines changed

4 files changed

+65
-29
lines changed

ibis/common/annotations.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,23 @@ def __init__(self, validator=None, default=EMPTY, kind=POSITIONAL_OR_KEYWORD):
7878
self._validator = validator
7979

8080
@classmethod
81-
def required(cls, validator=None):
81+
def required(cls, validator=None, kind=POSITIONAL_OR_KEYWORD):
8282
"""Annotation to mark a mandatory argument."""
83-
return cls(validator)
83+
return cls(validator=validator, kind=kind)
8484

8585
@classmethod
86-
def default(cls, default, validator=None):
86+
def default(cls, default, validator=None, kind=POSITIONAL_OR_KEYWORD):
8787
"""Annotation to allow missing arguments with a default value."""
88-
return cls(validator, default=default)
88+
return cls(validator=validator, default=default, kind=kind)
8989

9090
@classmethod
91-
def optional(cls, validator=None, default=None):
91+
def optional(cls, validator=None, default=None, kind=POSITIONAL_OR_KEYWORD):
9292
"""Annotation to allow and treat `None` values as missing arguments."""
9393
if validator is None:
9494
validator = option(any_, default=default)
9595
else:
9696
validator = option(validator, default=default)
97-
return cls(validator, default=None)
97+
return cls(validator=validator, default=None, kind=kind)
9898

9999
@classmethod
100100
def varargs(cls, validator=None):
@@ -174,8 +174,12 @@ def merge(cls, *signatures, **annotations):
174174

175175
for name, param in params.items():
176176
if param.kind == VAR_POSITIONAL:
177+
if var_args:
178+
raise TypeError('only one variadic *args parameter is allowed')
177179
var_args.append(param)
178180
elif param.kind == VAR_KEYWORD:
181+
if var_kwargs:
182+
raise TypeError('only one variadic **kwargs parameter is allowed')
179183
var_kwargs.append(param)
180184
elif name in inherited:
181185
if param.default is EMPTY:
@@ -188,11 +192,6 @@ def merge(cls, *signatures, **annotations):
188192
else:
189193
new_kwargs.append(param)
190194

191-
if len(var_args) > 1:
192-
raise TypeError('only one variadic positional *args parameter is allowed')
193-
if len(var_kwargs) > 1:
194-
raise TypeError('only one variadic keywords **kwargs parameter is allowed')
195-
196195
return cls(
197196
old_args + new_args + var_args + new_kwargs + old_kwargs + var_kwargs
198197
)
@@ -229,9 +228,6 @@ def from_callable(cls, fn, validators=None, return_validator=None):
229228

230229
parameters = []
231230
for param in sig.parameters.values():
232-
if param.kind in {POSITIONAL_ONLY, KEYWORD_ONLY}:
233-
raise TypeError(f"unsupported parameter kind {param.kind} in {fn}")
234-
235231
if param.name in validators:
236232
validator = validators[param.name]
237233
elif param.annotation is not EMPTY:
@@ -246,9 +242,9 @@ def from_callable(cls, fn, validators=None, return_validator=None):
246242
elif param.kind is VAR_KEYWORD:
247243
annot = Argument.varkwds(validator)
248244
elif param.default is EMPTY:
249-
annot = Argument.required(validator)
245+
annot = Argument.required(validator, kind=param.kind)
250246
else:
251-
annot = Argument.default(param.default, validator)
247+
annot = Argument.default(param.default, validator, kind=param.kind)
252248

253249
parameters.append(Parameter(param.name, annot))
254250

@@ -288,6 +284,10 @@ def unbind(self, this: Any):
288284
args.extend(value)
289285
elif param.kind is VAR_KEYWORD:
290286
kwargs.update(value)
287+
elif param.kind is KEYWORD_ONLY:
288+
kwargs[name] = value
289+
elif param.kind is POSITIONAL_ONLY:
290+
args.append(value)
291291
else:
292292
raise TypeError(f"unsupported parameter kind {param.kind}")
293293
return tuple(args), kwargs
@@ -435,9 +435,13 @@ def annotated(_1=None, _2=None, _3=None, **kwargs):
435435

436436
@functools.wraps(func)
437437
def wrapped(*args, **kwargs):
438+
# 1. Validate the passed arguments
438439
values = sig.validate(*args, **kwargs)
440+
# 2. Reconstruction of the original arguments
439441
args, kwargs = sig.unbind(values)
442+
# 3. Call the function with the validated arguments
440443
result = func(*args, **kwargs)
444+
# 4. Validate the return value
441445
return sig.validate_return(result)
442446

443447
wrapped.__signature__ = sig

ibis/common/tests/test_annotations.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ def test(a: int, b: int, c: int = 1):
123123
with pytest.raises(TypeError):
124124
sig.validate(2, 3, "4")
125125

126+
args, kwargs = sig.unbind(sig.validate(2, 3))
127+
assert args == (2, 3, 1)
128+
assert kwargs == {}
129+
126130

127131
def test_signature_from_callable_with_varargs():
128132
def test(a: int, b: int, *args: int):
@@ -136,19 +140,45 @@ def test(a: int, b: int, *args: int):
136140
with pytest.raises(TypeError):
137141
sig.validate(2, 3, 4, "5")
138142

143+
args, kwargs = sig.unbind(sig.validate(2, 3, 4, 5))
144+
assert args == (2, 3, 4, 5)
145+
assert kwargs == {}
139146

140-
def test_signature_from_callable_unsupported_argument_kinds():
141-
def test(a: int, b: int, *, c: int):
142-
pass
143-
144-
with pytest.raises(TypeError, match="unsupported parameter kind KEYWORD_ONLY"):
145-
Signature.from_callable(test)
146147

148+
def test_signature_from_callable_with_positional_only_arguments():
147149
def test(a: int, b: int, /, c: int = 1):
148-
pass
150+
return a + b + c
151+
152+
sig = Signature.from_callable(test)
153+
assert sig.validate(2, 3) == {'a': 2, 'b': 3, 'c': 1}
154+
assert sig.validate(2, 3, 4) == {'a': 2, 'b': 3, 'c': 4}
155+
assert sig.validate(2, 3, c=4) == {'a': 2, 'b': 3, 'c': 4}
156+
157+
msg = "'b' parameter is positional only, but was passed as a keyword"
158+
with pytest.raises(TypeError, match=msg):
159+
sig.validate(1, b=2)
160+
161+
args, kwargs = sig.unbind(sig.validate(2, 3))
162+
assert args == (2, 3, 1)
163+
assert kwargs == {}
164+
165+
166+
def test_signature_from_callable_with_keyword_only_arguments():
167+
def test(a: int, b: int, *, c: float, d: float = 0.0):
168+
return a + b + c
169+
170+
sig = Signature.from_callable(test)
171+
assert sig.validate(2, 3, c=4.0) == {'a': 2, 'b': 3, 'c': 4.0, 'd': 0.0}
172+
assert sig.validate(2, 3, c=4.0, d=5.0) == {'a': 2, 'b': 3, 'c': 4.0, 'd': 5.0}
173+
174+
with pytest.raises(TypeError, match="missing a required argument: 'c'"):
175+
sig.validate(2, 3)
176+
with pytest.raises(TypeError, match="too many positional arguments"):
177+
sig.validate(2, 3, 4)
149178

150-
with pytest.raises(TypeError, match="unsupported parameter kind POSITIONAL_ONLY"):
151-
Signature.from_callable(test)
179+
args, kwargs = sig.unbind(sig.validate(2, 3, c=4.0))
180+
assert args == (2, 3)
181+
assert kwargs == {'c': 4.0, 'd': 0.0}
152182

153183

154184
def test_signature_unbind():

ibis/common/tests/test_grounds.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ class Test2(Test):
343343
assert b.c == 2
344344
assert b.args == (3, 4)
345345

346-
msg = "only one variadic positional \\*args parameter is allowed"
346+
msg = "only one variadic \\*args parameter is allowed"
347347
with pytest.raises(TypeError, match=msg):
348348

349349
class Test3(Test):
@@ -375,7 +375,7 @@ class Test2(Test):
375375
assert b.c == 3
376376
assert b.options == {'d': 4, 'e': 5}
377377

378-
msg = "only one variadic keywords \\*\\*kwargs parameter is allowed"
378+
msg = "only one variadic \\*\\*kwargs parameter is allowed"
379379
with pytest.raises(TypeError, match=msg):
380380

381381
class Test3(Test):

ibis/common/tests/test_validators.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,12 @@ def func_with_kwargs(a, b, c=1, **kwargs):
190190
def func_with_mandatory_kwargs(*, c):
191191
return c
192192

193-
with pytest.raises(TypeError, match="Argument must be a callable"):
193+
msg = "Argument must be a callable"
194+
with pytest.raises(TypeError, match=msg):
194195
callable_with([instance_of(int), instance_of(str)], 10, "string")
195196

196-
with pytest.raises(TypeError, match="unsupported parameter kind KEYWORD_ONLY"):
197+
msg = "Callable has mandatory keyword-only arguments which cannot be specified"
198+
with pytest.raises(TypeError, match=msg):
197199
callable_with([instance_of(int)], instance_of(str), func_with_mandatory_kwargs)
198200

199201
msg = "Callable has more positional arguments than expected"

0 commit comments

Comments
 (0)