Skip to content

Commit c3f0499

Browse files
authored
Handle keyword arguments in class definitions (#1173)
* Handle keyword arguments in class definitions * Add tests
1 parent 8241bd0 commit c3f0499

File tree

9 files changed

+225
-30
lines changed

9 files changed

+225
-30
lines changed

Src/IronPython/Compiler/Ast/AstMethods.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal static class AstMethods {
2020
public static readonly MethodInfo IsTrue = GetMethod((Func<object, bool>)PythonOps.IsTrue);
2121
public static readonly MethodInfo RaiseAssertionError = GetMethod((Action<CodeContext, object>)PythonOps.RaiseAssertionError);
2222
public static readonly MethodInfo RaiseAssertionErrorNoMessage = GetMethod((Action<CodeContext>)PythonOps.RaiseAssertionError);
23-
public static readonly MethodInfo MakeClass = GetMethod((Func<FunctionCode, Func<CodeContext, CodeContext>, CodeContext, string, PythonTuple, object, string, object>)PythonOps.MakeClass);
23+
public static readonly MethodInfo MakeClass = GetMethod((Func<FunctionCode, Func<CodeContext, CodeContext>, CodeContext, string, PythonTuple, PythonDictionary, string, object>)PythonOps.MakeClass);
2424
public static readonly MethodInfo PrintExpressionValue = GetMethod((Action<CodeContext, object>)PythonOps.PrintExpressionValue);
2525
public static readonly MethodInfo ImportWithNames = GetMethod((Func<CodeContext, string, string[], int, object>)PythonOps.ImportWithNames);
2626
public static readonly MethodInfo ImportFrom = GetMethod((Func<CodeContext, object, string, object>)PythonOps.ImportFrom);
@@ -31,6 +31,7 @@ internal static class AstMethods {
3131
public static readonly MethodInfo ListAddForComprehension = GetMethod((Action<PythonList, object>)PythonOps.ListAddForComprehension);
3232
public static readonly MethodInfo SetAddForComprehension = GetMethod((Action<SetCollection, object>)PythonOps.SetAddForComprehension);
3333
public static readonly MethodInfo DictAddForComprehension = GetMethod((Action<PythonDictionary, object, object>)PythonOps.DictAddForComprehension);
34+
public static readonly MethodInfo DictMergeOne = GetMethod((Action<CodeContext, PythonDictionary, object, object>)PythonOps.DictMergeOne);
3435
public static readonly MethodInfo CheckUninitializedFree = GetMethod((Func<object, string, object>)PythonOps.CheckUninitializedFree);
3536
public static readonly MethodInfo CheckUninitializedLocal = GetMethod((Func<object, string, object>)PythonOps.CheckUninitializedLocal);
3637
public static readonly MethodInfo PublishModule = GetMethod((Func<CodeContext, string, object>)PythonOps.PublishModule);

Src/IronPython/Compiler/Ast/CallExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ static MSAst.Expression UnpackListHelper(IReadOnlyList<Arg> args, int firstListP
156156
return Expression.Block(typeof(PythonList), new MSAst.ParameterExpression[] { varExpr }, expressions);
157157
}
158158

159+
// Compare to: ClassDefinition.Reduce.__UnpackKeywordsHelper
159160
static MSAst.Expression UnpackDictHelper(MSAst.Expression context, IReadOnlyList<Arg> kwargs, int numDict, int firstDictPos) {
160161
Debug.Assert(kwargs.Count > 0);
161162
Debug.Assert(0 < numDict && numDict <= kwargs.Count);

Src/IronPython/Compiler/Ast/ClassDefinition.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ public ClassDefinition(string name, IReadOnlyList<Arg>? bases, IReadOnlyList<Arg
4242
_bases = bases?.ToArray() ?? Array.Empty<Arg>();
4343
_keywords = keywords?.ToArray() ?? Array.Empty<Arg>();
4444
Body = body ?? EmptyStatement.PreCompiledInstance;
45-
Metaclass = _keywords.Where(arg => arg.Name == "metaclass").Select(arg => arg.Expression).FirstOrDefault();
4645
}
4746

4847
public SourceLocation Header => GlobalParent.IndexToLocation(HeaderIndex);
@@ -55,8 +54,6 @@ public ClassDefinition(string name, IReadOnlyList<Arg>? bases, IReadOnlyList<Arg
5554

5655
public IReadOnlyList<Arg> Keywords => _keywords;
5756

58-
public Expression? Metaclass { get; }
59-
6057
public Statement Body { get; set; }
6158

6259
public IList<Expression>? Decorators { get; internal set; }
@@ -190,7 +187,7 @@ public override MSAst.Expression Reduce() {
190187
Parent.LocalContext,
191188
AstUtils.Constant(_name),
192189
UnpackBasesHelper(_bases),
193-
Metaclass is null ? AstUtils.Constant(null, typeof(object)) : AstUtils.Convert(Metaclass, typeof(object)),
190+
UnpackKeywordsHelper(Parent.LocalContext, _keywords),
194191
AstUtils.Constant(FindSelfNames())
195192
);
196193

@@ -230,6 +227,26 @@ static MSAst.Expression UnpackBasesHelper(IReadOnlyList<Arg> bases) {
230227
return Expression.Block(typeof(PythonTuple), new MSAst.ParameterExpression[] { varExpr }, expressions);
231228
}
232229
}
230+
231+
// Compare to: CallExpression.Reduce.__UnpackDictHelper
232+
static MSAst.Expression UnpackKeywordsHelper(MSAst.Expression context, IReadOnlyList<Arg> kwargs) {
233+
if (kwargs.Count == 0) {
234+
return AstUtils.Constant(null, typeof(PythonDictionary));
235+
}
236+
237+
var expressions = new ReadOnlyCollectionBuilder<MSAst.Expression>(kwargs.Count + 2);
238+
var varExpr = Expression.Variable(typeof(PythonDictionary), "$dict");
239+
expressions.Add(Expression.Assign(varExpr, Expression.Call(AstMethods.MakeEmptyDict)));
240+
foreach (var arg in kwargs) {
241+
if (arg.ArgumentInfo.Kind == ArgumentType.Dictionary) {
242+
expressions.Add(Expression.Call(AstMethods.DictMerge, context, varExpr, AstUtils.Convert(arg.Expression, typeof(object))));
243+
} else {
244+
expressions.Add(Expression.Call(AstMethods.DictMergeOne, context, varExpr, AstUtils.Constant(arg.Name, typeof(object)), AstUtils.Convert(arg.Expression, typeof(object))));
245+
}
246+
}
247+
expressions.Add(varExpr);
248+
return Expression.Block(typeof(PythonDictionary), new MSAst.ParameterExpression[] { varExpr }, expressions);
249+
}
233250
}
234251

235252
private Microsoft.Scripting.Ast.LightExpression<Func<CodeContext, CodeContext>> MakeClassBody() {

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,13 +1305,18 @@ public static void InitializeForFinalization(CodeContext/*!*/ context, object ne
13051305
iwr.SetFinalizer(new WeakRefTracker(iwr, nif, nif));
13061306
}
13071307

1308-
internal static object? CallPrepare(CodeContext/*!*/ context, PythonType meta, string name, PythonTuple bases, PythonDictionary dict) {
1308+
internal static object? CallPrepare(CodeContext/*!*/ context, PythonType meta, string name, PythonTuple bases, PythonDictionary? keywords, PythonDictionary dict) {
13091309
object? classdict = dict;
13101310

13111311
// if available, call the __prepare__ method to get the classdict (PEP 3115)
13121312
if (meta.TryLookupSlot(context, "__prepare__", out PythonTypeSlot pts)) {
13131313
if (pts.TryGetValue(context, null, meta, out object value)) {
1314-
classdict = PythonOps.CallWithContext(context, value, name, bases);
1314+
if (keywords is null || keywords.Count == 0) {
1315+
classdict = PythonOps.CallWithContext(context, value, name, bases);
1316+
} else {
1317+
var args = new object[] { name, bases };
1318+
classdict = PythonCalls.CallWithKeywordArgs(context, value, args, keywords);
1319+
}
13151320
// copy the contents of dict to the classdict
13161321
foreach (var pair in dict)
13171322
context.LanguageContext.SetIndex(classdict, pair.Key, pair.Value);
@@ -1321,10 +1326,15 @@ public static void InitializeForFinalization(CodeContext/*!*/ context, object ne
13211326
return classdict;
13221327
}
13231328

1324-
public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeContext> body, CodeContext/*!*/ parentContext, string name, PythonTuple bases, object metaclass, string selfNames) {
1329+
public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeContext> body, CodeContext/*!*/ parentContext, string name, PythonTuple bases, PythonDictionary? keywords, string selfNames) {
13251330
Func<CodeContext, CodeContext> func = GetClassCode(parentContext, funcCode, body);
13261331

1327-
return MakeClass(parentContext, name, bases, metaclass, selfNames, func(parentContext).Dict);
1332+
object? metaclass = null;
1333+
if (keywords is not null && keywords.TryGetValueNoMissing("metaclass", out metaclass)) {
1334+
keywords.RemoveDirect("metaclass"); // keyword argument consumed
1335+
}
1336+
1337+
return MakeClass(parentContext, name, bases, metaclass, keywords, selfNames, func(parentContext).Dict);
13281338
}
13291339

13301340
private static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, FunctionCode funcCode, Func<CodeContext, CodeContext> body) {
@@ -1342,7 +1352,9 @@ private static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ cont
13421352
}
13431353
}
13441354

1345-
private static object MakeClass(CodeContext/*!*/ context, string name, PythonTuple bases, object metaclass, string selfNames, PythonDictionary vars) {
1355+
private static object MakeClass(CodeContext/*!*/ context, string name, PythonTuple bases, object? metaclass, PythonDictionary? keywords, string selfNames, PythonDictionary vars) {
1356+
Debug.Assert(metaclass is null || keywords is not null);
1357+
13461358
foreach (object? dt in bases) {
13471359
if (dt is TypeGroup) {
13481360
object?[] newBases = new object[bases.Count];
@@ -1368,6 +1380,9 @@ private static object MakeClass(CodeContext/*!*/ context, string name, PythonTup
13681380
}
13691381

13701382
if (metaclass is null) {
1383+
if (keywords != null && keywords.Count > 0) {
1384+
throw TypeError("type() takes no keyword arguments");
1385+
}
13711386
// this makes sure that object is a base
13721387
if (bases.Count == 0) {
13731388
bases = PythonTuple.MakeTuple(DynamicHelpers.GetPythonTypeFromType(typeof(object)));
@@ -1377,14 +1392,13 @@ private static object MakeClass(CodeContext/*!*/ context, string name, PythonTup
13771392

13781393
object? classdict = vars;
13791394

1380-
if (metaclass is PythonType) {
1381-
classdict = CallPrepare(context, (PythonType)metaclass, name, bases, vars);
1395+
if (metaclass is PythonType metatype) {
1396+
classdict = CallPrepare(context, metatype, name, bases, keywords, vars);
13821397
}
13831398

13841399
// eg:
1385-
// def foo(*args): print args
1386-
// __metaclass__ = foo
1387-
// class bar: pass
1400+
// def foo(*args): print(args)
1401+
// class bar(metaclass=foo): pass
13881402
// calls our function...
13891403
PythonContext pc = context.LanguageContext;
13901404

@@ -1394,7 +1408,8 @@ private static object MakeClass(CodeContext/*!*/ context, string name, PythonTup
13941408
metaclass,
13951409
name,
13961410
bases,
1397-
classdict
1411+
classdict,
1412+
keywords
13981413
);
13991414

14001415
if (obj is PythonType newType && newType.BaseTypes.Count == 0) {
@@ -1506,12 +1521,21 @@ public static void DictMerge(CodeContext context, PythonDictionary dict, object?
15061521
// enumerate the keys getting their values
15071522
IEnumerator enumerator = GetEnumerator(keys);
15081523
while (enumerator.MoveNext()) {
1509-
object? o = enumerator.Current;
1510-
if (dict.ContainsKey(o) && (o is string || o is Extensible<string>)) {
1511-
throw TypeError($"function got multiple values for keyword argument '{o}'");
1512-
}
1513-
dict[o] = PythonOps.GetIndex(context, item, o);
1524+
object? key = enumerator.Current;
1525+
object? value = PythonOps.GetIndex(context, item, key);
1526+
DictMergeOne(context, dict, key, value);
1527+
}
1528+
}
1529+
1530+
/// <summary>
1531+
/// Like DictMerge but for a single key/value element
1532+
/// </summary>
1533+
[EditorBrowsable(EditorBrowsableState.Never)]
1534+
public static void DictMergeOne(CodeContext context, PythonDictionary dict, object? key, object? value) {
1535+
if (dict.ContainsKey(key) && (key is string || key is Extensible<string>)) {
1536+
throw TypeError($"function got multiple values for keyword argument '{key}'");
15141537
}
1538+
dict[key] = value;
15151539
}
15161540

15171541
/// <summary>

Src/IronPython/Runtime/PythonContext.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public sealed partial class PythonContext : LanguageContext {
7070

7171
private Dictionary<AttrKey, CallSite<Func<CallSite, object, object, object>>> _setAttrSites;
7272
private Dictionary<AttrKey, CallSite<Action<CallSite, object>>> _deleteAttrSites;
73-
private CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, object>> _metaClassSite;
73+
private CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, PythonDictionary, object>> _metaClassSite;
7474
private CallSite<Func<CallSite, CodeContext, object, string, object>> _writeSite;
7575
private CallSite<Func<CallSite, object, object, object>> _getIndexSite, _equalSite;
7676
private CallSite<Action<CallSite, object, object>> _delIndexSite;
@@ -2197,14 +2197,19 @@ internal void DeleteAttr(CodeContext/*!*/ context, object o, string name) {
21972197
site.Target(site, o);
21982198
}
21992199

2200-
internal CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, object>> MetaClassCallSite {
2200+
internal CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, PythonDictionary, object>> MetaClassCallSite {
22012201
get {
22022202
if (_metaClassSite == null) {
22032203
Interlocked.CompareExchange(
22042204
ref _metaClassSite,
2205-
CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, object>>.Create(
2205+
CallSite<Func<CallSite, CodeContext, object, string, PythonTuple, object, PythonDictionary, object>>.Create(
22062206
Invoke(
2207-
new CallSignature(3)
2207+
new CallSignature(
2208+
ArgumentType.Simple, // name
2209+
ArgumentType.Simple, // bases
2210+
ArgumentType.Simple, // classdict
2211+
ArgumentType.Dictionary // keywords
2212+
)
22082213
)
22092214
),
22102215
null

Src/IronPython/Runtime/Types/PythonType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ internal static object __new__(CodeContext/*!*/ context, PythonType cls, string
267267
object type;
268268

269269
if (meta != TypeCache.PythonType) {
270-
object classdict = PythonOps.CallPrepare(context, meta, name, bases, dict);
270+
object classdict = PythonOps.CallPrepare(context, meta, name, bases, null, dict);
271271

272272
if (meta != cls) {
273273
// the user has a custom __new__ which picked the wrong meta class, call the correct metaclass

Tests/interop/net/type/test___clrtype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ class X(object, metaclass=MyType):
835835
self.fail("type.__new__ signature is wrong")
836836
except TypeError as e:
837837
self.assertEqual(str(e),
838-
"__new__() takes exactly 1 argument (4 given)")
838+
"__new__() takes exactly 1 non-keyword argument (4 given)")
839839
finally:
840840
self.assertEqual(called, False)
841841

0 commit comments

Comments
 (0)