Skip to content

Commit 5268374

Browse files
committed
[GR-25788] GraalPython allows to put multiple values for keyword argument.
1 parent 1804c9b commit 5268374

File tree

10 files changed

+325
-19
lines changed

10 files changed

+325
-19
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_call.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -167,6 +167,8 @@ def f24(*args, a, b=5):
167167
def f25(*args, a, b=5, **kwds):
168168
pass
169169

170+
def f26(**kw):
171+
return kw
170172

171173
def assert_parses(call_expr):
172174
raised = False
@@ -234,8 +236,7 @@ def test_parse_args():
234236

235237
assert_parses("f11(a=1, b=2)")
236238
assert_parses("f11(a=1, b=2, c=3)")
237-
# TODO
238-
# assert_call_raises(SyntaxError, "f11(a=1, b=2, a=3)") # SyntaxError: keyword argument repeated
239+
assert_call_raises(SyntaxError, "f11(a=1, b=2, a=3)") # SyntaxError: keyword argument repeated
239240
assert_call_raises(TypeError, "f11(1, b=2, a=3)") # TypeError: f11() got multiple values for argument 'a'
240241

241242
assert_parses("f12(1,2)")
@@ -286,8 +287,7 @@ def test_parse_args():
286287
assert_parses("f20(a=1)")
287288
assert_parses("f20(a=1, b=2)")
288289
assert_parses("f20(a=1, b=2, c=3)")
289-
# TODO
290-
# assert_call_raises(SyntaxError, "f20(a=1, b=2, a=3)") # SyntaxError: keyword argument repeated
290+
assert_call_raises(SyntaxError, "f20(a=1, b=2, a=3)") # SyntaxError: keyword argument repeated
291291
assert_call_raises(TypeError, "f20(1, b=2)") # TypeError: f20() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
292292
assert_call_raises(TypeError, "f20(1)") # TypeError: f20() takes 0 positional arguments but 1 was given
293293

@@ -324,3 +324,36 @@ def test_parse_args():
324324
assert_call_raises(TypeError, "f25(1,2,3,c=6)") # TypeError: f25() missing 1 required keyword-only argument: 'a'
325325
assert_parses("f25(a=4,c=6)")
326326
assert_parses("f25(a=4)")
327+
328+
def test_multiple_values_for_keyword_argument():
329+
assert_call_raises(TypeError, "f26(a=1, **{'a' : 2})") # TypeError: f26() got multiple values for keyword argument 'a'
330+
assert_call_raises(TypeError, "f26(**{'a' : 4}, **{'a': 3})") # TypeError: f26() got multiple values for keyword argument 'a'
331+
332+
b = 1
333+
def test_argument_must_be_mapping():
334+
assert_call_raises(TypeError, "f26(a=1, **b)") # TypeError: f26() argument after ** must be a mapping, not int
335+
assert_call_raises(TypeError, "f26(**b)") # TypeError: f26() argument after ** must be a mapping, not int
336+
class MyDict(dict):
337+
pass
338+
d = MyDict()
339+
assert f26(**d) == {}
340+
341+
fooo = f26
342+
def test_multiple_values_with_callable_name():
343+
def assert_get_message(err, fn, *args, **kwargs):
344+
raised = False
345+
try:
346+
fn(*args, **kwargs)
347+
except err as ex:
348+
return str(ex)
349+
assert raised
350+
351+
def assert_call_raises_get_message(exception, call_expr):
352+
return assert_get_message(exception, eval, call_expr)
353+
354+
msg = assert_call_raises_get_message(TypeError, "fooo(a=1, **b)") # TypeError: f26() argument after ** must be a mapping, not int
355+
assert msg == "f26() argument after ** must be a mapping, not int"
356+
357+
msg = assert_call_raises_get_message(TypeError, "fooo(**{'a' : 4}, **{'a': 3})") # TypeError: f26() got multiple values for keyword argument 'a'
358+
assert msg == "f26() got multiple values for keyword argument 'a'"
359+

graalpython/com.oracle.graal.python.test/testData/goldenFiles/BasicTests/call12.tast

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ModuleRootNode Name: <module 'call12'> SourceSection: [0,25]`foo(**mydict1, **my
1010
KeywordArgumentsNodeGen SourceSection: None
1111
CompactKeywordsNodeGen SourceSection: None
1212
ExecuteKeywordStarargsNodeGen SourceSection: None
13-
DictConcatNodeGen SourceSection: None
13+
ConcatKeywordsNodeGen SourceSection: None
1414
ReadNameNodeGen SourceSection: [6,13]`mydict1`
1515
Identifier: mydict1
1616
IsBuiltinClassProfile SourceSection: None

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
public abstract class ErrorMessages {
4444
public static final String ABSOLUTE_VALUE_TOO_LARGE = "absolute value too large";
4545
public static final String ARG_AFTER_MUST_BE_ITERABLE = "argument after * must be an iterable, not %p";
46+
public static final String ARG_AFTER_MUST_BE_MAPPING = "%s() argument after ** must be a mapping, not %p";
4647
public static final String ARG_CONVERTED_NOT_EXECUTABLE = "argument converted is not executable";
4748
public static final String ARG_D_MUST_BE_S = "%s arg %d must be a %s";
4849
public static final String ARG_D_MUST_BE_S_NOT_P = "%s argument %d must be %s, not %p";
@@ -252,7 +253,7 @@ public abstract class ErrorMessages {
252253
public static final String GETATTR_ATTRIBUTE_NAME_MUST_BE_STRING = "getattr(): attribute name must be string";
253254
public static final String GETTING_THER_SOURCE_NOT_SUPPORTED_FOR_P = "getting the source is not supported for '%p'";
254255
public static final String GLOBALS_MUST_BE_DICT = "%s() globals must be a dict, not %p";
255-
public static final String GOT_MULTIPLE_VALUES_FOR_ARG = "%s() got multiple values for argument '%s'";
256+
public static final String GOT_MULTIPLE_VALUES_FOR_ARG = "%s() got multiple values for keyword argument '%s'";
256257
public static final String GOT_SOME_POS_ONLY_ARGS_PASSED_AS_KEYWORD = "%s() got some positional-only arguments passed as keyword arguments: '%s'";
257258
public static final String GOT_UNEXPECTED_KEYWORD_ARG = "%s() got an unexpected keyword argument '%s'";
258259
public static final String HAS_NO_ATTR = "%s has no attribute %s";

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/argument/CreateArgumentsNode.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ Object[] applyCached(Object callee, @SuppressWarnings("unused") Signature callee
500500
if (kwIdx != -1) {
501501
if (positionalOnlyArgIndex > -1 && kwIdx < positionalOnlyArgIndex) {
502502
if (unusedKeywords != null) {
503-
unusedKeywords[k++] = kwArg;
503+
addKeyword(unusedKeywords, k++, kwArg, callee, raise);
504504
} else {
505505
posArgOnlyPassedAsKeywordProfile.enter();
506506
posArgOnlyPassedAsKeywordNames = addPosArgOnlyPassedAsKeyword(posArgOnlyPassedAsKeywordNames, name);
@@ -512,7 +512,7 @@ Object[] applyCached(Object callee, @SuppressWarnings("unused") Signature callee
512512
PArguments.setArgument(arguments, kwIdx, kwArg.getValue());
513513
}
514514
} else if (unusedKeywords != null) {
515-
unusedKeywords[k++] = kwArg;
515+
addKeyword(unusedKeywords, k++, kwArg, callee, raise);
516516
} else {
517517
additionalKwds++;
518518
lastWrongKeyword = name;
@@ -553,7 +553,7 @@ Object[] applyUncached(Object callee, Signature calleeSignature, Object[] argume
553553
if (kwIdx != -1) {
554554
if (positionalOnlyArgIndex > -1 && kwIdx < positionalOnlyArgIndex) {
555555
if (unusedKeywords != null) {
556-
unusedKeywords[k++] = kwArg;
556+
addKeyword(unusedKeywords, k++, kwArg, callee, raise);
557557
} else {
558558
posArgOnlyPassedAsKeywordProfile.enter();
559559
posArgOnlyPassedAsKeywordNames = addPosArgOnlyPassedAsKeyword(posArgOnlyPassedAsKeywordNames, name);
@@ -565,7 +565,7 @@ Object[] applyUncached(Object callee, Signature calleeSignature, Object[] argume
565565
PArguments.setArgument(arguments, kwIdx, kwArg.getValue());
566566
}
567567
} else if (unusedKeywords != null) {
568-
unusedKeywords[k++] = kwArg;
568+
addKeyword(unusedKeywords, k++, kwArg, callee, raise);
569569
} else {
570570
additionalKwds++;
571571
lastWrongKeyword = name;
@@ -575,6 +575,16 @@ Object[] applyUncached(Object callee, Signature calleeSignature, Object[] argume
575575
return arguments;
576576
}
577577

578+
private static void addKeyword(PKeyword[] keywords, int where, PKeyword keyword, Object callee, PRaiseNode raise) {
579+
String name = keyword.getName();
580+
for (int i = 0; i < where; i++) {
581+
if (keywords[i].getName().equals(name)) {
582+
throw raise.raise(PythonBuiltinClassType.TypeError, ErrorMessages.GOT_MULTIPLE_VALUES_FOR_ARG, CreateArgumentsNode.getName(callee), name);
583+
}
584+
}
585+
keywords[where] = keyword;
586+
}
587+
578588
@TruffleBoundary
579589
private static List<String> addPosArgOnlyPassedAsKeyword(List<String> names, String name) {
580590
if (names == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.nodes.argument.keywords;
42+
43+
import com.oracle.graal.python.builtins.objects.common.HashingCollectionNodes.GetDictStorageNode;
44+
import com.oracle.graal.python.builtins.objects.common.HashingStorage;
45+
import com.oracle.graal.python.builtins.objects.common.HashingStorageLibrary;
46+
import com.oracle.graal.python.builtins.objects.dict.PDict;
47+
import com.oracle.graal.python.nodes.expression.ExpressionNode;
48+
import com.oracle.truffle.api.CompilerDirectives;
49+
import com.oracle.truffle.api.dsl.Cached;
50+
import com.oracle.truffle.api.dsl.GenerateNodeFactory;
51+
import com.oracle.truffle.api.dsl.Specialization;
52+
import com.oracle.truffle.api.frame.VirtualFrame;
53+
import com.oracle.truffle.api.library.CachedLibrary;
54+
import com.oracle.truffle.api.nodes.ExplodeLoop;
55+
import com.oracle.truffle.api.nodes.Node;
56+
import com.oracle.truffle.api.profiles.ConditionProfile;
57+
58+
@GenerateNodeFactory
59+
public abstract class ConcatKeywordsNode extends ExpressionNode {
60+
61+
@Node.Children final ExpressionNode[] mappables;
62+
63+
protected ConcatKeywordsNode(ExpressionNode... mappablesNodes) {
64+
this.mappables = mappablesNodes;
65+
}
66+
67+
@ExplodeLoop
68+
@Specialization
69+
public Object concat(VirtualFrame frame,
70+
@Cached("createBinaryProfile()") ConditionProfile hasFrame,
71+
@Cached GetDictStorageNode getStore,
72+
@CachedLibrary(limit = "2") HashingStorageLibrary firstlib,
73+
@CachedLibrary(limit = "1") HashingStorageLibrary otherlib,
74+
@Cached("createBinaryProfile()") ConditionProfile sameKeyProfile) {
75+
PDict first = null;
76+
PDict other;
77+
for (ExpressionNode n : mappables) {
78+
if (first == null) {
79+
first = expectDict(n.execute(frame));
80+
} else {
81+
other = expectDict(n.execute(frame));
82+
addAllToDict(frame, first, other, hasFrame, firstlib, otherlib, getStore, sameKeyProfile);
83+
}
84+
}
85+
return first;
86+
}
87+
88+
private static void addAllToDict(VirtualFrame frame, PDict dict, PDict other, ConditionProfile hasFrame,
89+
HashingStorageLibrary firstlib, HashingStorageLibrary otherlib, GetDictStorageNode getStore, ConditionProfile sameKeyProfile) {
90+
HashingStorage dictStorage = getStore.execute(dict);
91+
HashingStorage otherStorage = getStore.execute(other);
92+
for (Object key : otherlib.keys(otherStorage)) {
93+
Object value = otherlib.getItemWithFrame(otherStorage, key, hasFrame, frame);
94+
if (sameKeyProfile.profile(firstlib.hasKey(dictStorage, key))) {
95+
throw createSameDictKeyException(key);
96+
}
97+
dictStorage = firstlib.setItemWithFrame(dictStorage, key, value, hasFrame, frame);
98+
}
99+
dict.setDictStorage(dictStorage);
100+
}
101+
102+
@CompilerDirectives.TruffleBoundary
103+
private static SameDictKeyException createSameDictKeyException(Object key) {
104+
return new SameDictKeyException(key.toString());
105+
}
106+
107+
private static PDict expectDict(Object first) {
108+
if (!(first instanceof PDict)) {
109+
CompilerDirectives.transferToInterpreter();
110+
throw new NonMappingException(first);
111+
}
112+
return (PDict) first;
113+
}
114+
115+
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/argument/keywords/ExecuteKeywordStarargsNode.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
*/
4141
package com.oracle.graal.python.nodes.argument.keywords;
4242

43+
import com.oracle.graal.python.builtins.objects.PNone;
4344
import com.oracle.graal.python.builtins.objects.common.EmptyStorage;
4445
import com.oracle.graal.python.builtins.objects.common.HashingStorageLibrary;
4546
import com.oracle.graal.python.builtins.objects.common.KeywordsStorage;
@@ -110,7 +111,10 @@ static PKeyword[] doDict(PDict starargs,
110111

111112
@Specialization(guards = "!isDict(object)")
112113
static PKeyword[] doNonMapping(@SuppressWarnings("unused") Object object) {
113-
return PKeyword.EMPTY_KEYWORDS;
114+
if (object instanceof PNone) {
115+
return PKeyword.EMPTY_KEYWORDS;
116+
}
117+
throw new NonMappingException(object);
114118
}
115119

116120
@Specialization(replaces = {"doKeywordsArray", "doKeywordsStorage", "doEmptyStorage", "doDictCached", "doDict", "doNonMapping"})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.nodes.argument.keywords;
42+
43+
import com.oracle.truffle.api.nodes.ControlFlowException;
44+
45+
public class NonMappingException extends ControlFlowException {
46+
private static final long serialVersionUID = 1L;
47+
48+
private final Object object;
49+
50+
public NonMappingException(Object object) {
51+
super();
52+
this.object = object;
53+
}
54+
55+
public Object getObject() {
56+
return object;
57+
}
58+
59+
}

0 commit comments

Comments
 (0)