Skip to content

Commit d1d5895

Browse files
author
Sylvain MARIE
committed
Fixed yet another nasty varpositional-related bug :). Fixes #38
1 parent 06ea266 commit d1d5895

File tree

4 files changed

+90
-20
lines changed

4 files changed

+90
-20
lines changed

makefun/main.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,18 @@ def create_function(func_signature, # type: Union[str, Signature]
184184
raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature))
185185

186186
# extract all information needed from the `Signature`
187-
posonly_names, kwonly_names, varpos_names, varkw_names, unrestricted_names = get_signature_params(func_signature)
188-
params_names = posonly_names + unrestricted_names + varpos_names + kwonly_names + varkw_names
187+
params_to_kw_assignment_mode = get_signature_params(func_signature)
188+
params_names = list(params_to_kw_assignment_mode.keys())
189189

190190
# Note: in decorator the annotations were extracted using getattr(func_impl, '__annotations__') instead.
191191
# This seems equivalent but more general (provided by the signature, not the function), but to check
192192
annotations, defaults, kwonlydefaults = get_signature_details(func_signature)
193193

194194
# create the body of the function to compile
195-
assignments = posonly_names + [("%s=%s" % (k, k)) if k[0] != '*' else k
196-
for k in unrestricted_names + varpos_names + kwonly_names + varkw_names]
195+
# The generated function body should dispatch its received arguments to the inner function.
196+
# For this we will pass as much as possible the arguments as keywords.
197+
# However if there are varpositional arguments we cannot
198+
assignments = [("%s=%s" % (k, k)) if is_kw else k for k, is_kw in params_to_kw_assignment_mode.items()]
197199
params_str = ', '.join(assignments)
198200
if inject_as_first_arg:
199201
params_str = "%s, %s" % (func_name, params_str)
@@ -433,22 +435,26 @@ def get_signature_params(s):
433435
:param s:
434436
:return:
435437
"""
436-
posonly_names, kwonly_names, varpos_names, varkw_names, unrestricted_names = [], [], [], [], []
438+
# this ordered dictionary will contain parameters and True/False whether we should use keyword assignment or not
439+
params_to_assignment_mode = OrderedDict()
437440
for p_name, p in s.parameters.items():
438441
if p.kind is Parameter.POSITIONAL_ONLY:
439-
posonly_names.append(p_name)
442+
params_to_assignment_mode[p_name] = False
440443
elif p.kind is Parameter.KEYWORD_ONLY:
441-
kwonly_names.append(p_name)
444+
params_to_assignment_mode[p_name] = True
442445
elif p.kind is Parameter.POSITIONAL_OR_KEYWORD:
443-
unrestricted_names.append(p_name)
446+
params_to_assignment_mode[p_name] = True
444447
elif p.kind is Parameter.VAR_POSITIONAL:
445-
varpos_names.append("*" + p_name)
448+
# We have to pass all the arguments that were here in previous positions, as positional too.
449+
for k in params_to_assignment_mode.keys():
450+
params_to_assignment_mode[k] = False
451+
params_to_assignment_mode["*" + p_name] = False
446452
elif p.kind is Parameter.VAR_KEYWORD:
447-
varkw_names.append("**" + p_name)
453+
params_to_assignment_mode["**" + p_name] = False
448454
else:
449455
raise ValueError("Unknown kind: %s" % p.kind)
450456

451-
return posonly_names, kwonly_names, varpos_names, varkw_names, unrestricted_names
457+
return params_to_assignment_mode
452458

453459

454460
def get_signature_details(s):

makefun/tests/test_create_from_string_cases.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def case_simple_with_star_args1():
4040
# param_names = ['b', 'a']
4141

4242
inputs = "12"
43-
args = ()
44-
kwargs = {'a': 0, 'b': 12}
43+
# args = ()
44+
# kwargs = {'a': 0, 'b': 12}
45+
args = (12,)
46+
kwargs = {'a': 0}
4547
return params_str, inputs, (args, kwargs)
4648

4749

makefun/tests/test_doc.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,50 @@ def test_var_length():
151151
"""Demonstrates how variable-length arguments are passed to the handler """
152152

153153
# define the handler that should be called
154-
def func_impl(*args, **kwargs):
155-
"""This docstring will be used in the generated function by default"""
156-
print("func_impl called !")
157-
return args, kwargs
158154

159-
func_sig = "foo(a=0, *args, **kwargs)"
160-
gen_func = create_function(func_sig, func_impl)
155+
def generate_function(func_sig, dummy_call):
156+
def func_impl(*args, **kwargs):
157+
"""This docstring will be used in the generated function by default"""
158+
print("func_impl called !")
159+
dummy_call(*args, **kwargs)
160+
return args, kwargs
161+
162+
return create_function(func_sig, func_impl)
163+
164+
func_sig = "foo(a, b=0, *args, **kwargs)"
165+
166+
def dummy_call(a, b=0, *args, **kwargs):
167+
print()
168+
169+
gen_func = generate_function(func_sig, dummy_call)
170+
161171
print(gen_func.__source__)
162-
assert gen_func(0, 1) == ((1,), {'a': 0})
172+
# unfortunately we can not have this because as soon as users provide a bit more positional args they there
173+
# are TypeErrors "got multiple values for argument 'a'"
174+
# assert gen_func(0, 1, 2) == ((2), {'a': 0, 'b': 1})
175+
assert gen_func(0, 1, 2) == ((0, 1, 2), {})
176+
assert gen_func(0, b=1) == ((0, 1), {})
177+
# checks that the order is correctly set
178+
assert gen_func(b=1, a=0) == ((0, 1), {})
179+
180+
with pytest.raises(TypeError):
181+
gen_func(2, a=0, b=1)
182+
# --
183+
184+
func_sig = "foo(b=0, *args, **kwargs)"
185+
186+
def dummy_call(b=0, *args, **kwargs):
187+
print()
188+
189+
gen_func = generate_function(func_sig, dummy_call)
190+
191+
print(gen_func.__source__)
192+
193+
assert gen_func(1, 0) == ((1, 0), {})
194+
assert gen_func(b=1) == ((1, ), {})
195+
196+
with pytest.raises(TypeError):
197+
gen_func(1, b=0)
163198

164199

165200
def test_positional_only():

makefun/tests/test_issues.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ def foo(*args, **kwargs):
1818
foo('hello', 12)
1919

2020

21+
def test_varpositional2():
22+
""" test for https://github.com/smarie/python-makefun/issues/38 """
23+
24+
@with_signature("(a, *args)")
25+
def foo(a, *args):
26+
assert a == 'hello'
27+
assert args == (12, )
28+
29+
foo('hello', 12)
30+
31+
2132
def test_invalid_signature_str():
2233
"""Test for https://github.com/smarie/python-makefun/issues/36"""
2334

@@ -36,3 +47,19 @@ def test_invalid_signature_str_py3():
3647
@with_signature(sig)
3748
def foo(a):
3849
pass
50+
51+
52+
def test_init_replaced():
53+
54+
class Foo(object):
55+
@with_signature("(self, a)")
56+
def __init__(self, *args, **kwargs):
57+
pass
58+
59+
f = Foo(1)
60+
61+
class Bar(Foo):
62+
def __init__(self, *args, **kwargs):
63+
super(Bar, self).__init__(*args, **kwargs)
64+
65+
b = Bar(2)

0 commit comments

Comments
 (0)