Skip to content

Commit d492fd8

Browse files
authored
Improve metaclass logic (#1189)
* Improve metaclass logic * Add tests * Update after review
1 parent 32986af commit d492fd8

3 files changed

Lines changed: 266 additions & 87 deletions

File tree

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,66 +1305,17 @@ 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? keywords, PythonDictionary dict) {
1309-
object? classdict = dict;
1310-
1311-
// if available, call the __prepare__ method to get the classdict (PEP 3115)
1312-
if (meta.TryLookupSlot(context, "__prepare__", out PythonTypeSlot pts)) {
1313-
if (pts.TryGetValue(context, null, meta, out object value)) {
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-
}
1320-
// copy the contents of dict to the classdict
1321-
foreach (var pair in dict)
1322-
context.LanguageContext.SetIndex(classdict, pair.Key, pair.Value);
1323-
}
1324-
}
1325-
1326-
return classdict;
1327-
}
1328-
13291308
public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeContext> body, CodeContext/*!*/ parentContext, string name, PythonTuple bases, PythonDictionary? keywords, string selfNames) {
13301309
Func<CodeContext, CodeContext> func = GetClassCode(parentContext, funcCode, body);
13311310

1332-
object? metaclass = null;
1333-
if (keywords is not null && keywords.TryGetValueNoMissing("metaclass", out metaclass)) {
1334-
keywords.RemoveDirect("metaclass"); // keyword argument consumed
1335-
if (metaclass is null) {
1336-
throw TypeError("metaclass cannot be 'None'"); // CPython: 'NoneType' object is not callable
1337-
}
1338-
}
1339-
1340-
return MakeClass(parentContext, name, bases, metaclass, keywords, selfNames, func(parentContext).Dict);
1341-
}
1342-
1343-
private static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, FunctionCode funcCode, Func<CodeContext, CodeContext> body) {
1344-
if (body == null) {
1345-
if (funcCode.Target == null) {
1346-
funcCode.UpdateDelegate(context.LanguageContext, true);
1347-
}
1348-
return (Func<CodeContext, CodeContext>)funcCode.Target!;
1349-
} else {
1350-
if (funcCode.Target == null) {
1351-
funcCode.SetTarget(body);
1352-
funcCode._normalDelegate = body;
1353-
}
1354-
return body;
1355-
}
1356-
}
1357-
1358-
private static object MakeClass(CodeContext/*!*/ context, string name, PythonTuple bases, object? metaclass, PythonDictionary? keywords, string selfNames, PythonDictionary vars) {
1359-
Debug.Assert(metaclass is null || keywords is not null);
1360-
1311+
// Check and normalize bases
13611312
foreach (object? dt in bases) {
13621313
if (dt is TypeGroup) {
13631314
object?[] newBases = new object[bases.Count];
13641315
for (int i = 0; i < bases.Count; i++) {
13651316
if (bases[i] is TypeGroup tc) {
13661317
if (!tc.TryGetNonGenericType(out Type nonGenericType)) {
1367-
throw PythonOps.TypeError("cannot derive from open generic types {0}", Repr(context, tc));
1318+
throw PythonOps.TypeError("cannot derive from open generic types {0}", Repr(parentContext, tc));
13681319
}
13691320
newBases[i] = DynamicHelpers.GetPythonTypeFromType(nonGenericType);
13701321
} else {
@@ -1382,6 +1333,23 @@ private static object MakeClass(CodeContext/*!*/ context, string name, PythonTup
13821333
}
13831334
}
13841335

1336+
// Discover metaclass
1337+
object? metaclass = null;
1338+
if (keywords is not null && keywords.TryGetValueNoMissing("metaclass", out metaclass)) {
1339+
keywords.RemoveDirect("metaclass"); // keyword argument consumed
1340+
if (metaclass is null) {
1341+
throw TypeError("metaclass cannot be 'None'"); // CPython: 'NoneType' object is not callable
1342+
}
1343+
}
1344+
1345+
if (metaclass is null or PythonType) {
1346+
metaclass = PythonType.FindMetaClass((PythonType?)metaclass ?? TypeCache.PythonType, bases);
1347+
if (metaclass == TypeCache.PythonType) {
1348+
metaclass = null; // enables fasttrack below
1349+
}
1350+
} // else metaclass is expected to be a callable and overrides any inherited metaclass through any bases
1351+
1352+
// Fasttrack for metaclass == `type`
13851353
if (metaclass is null) {
13861354
if (keywords != null && keywords.Count > 0) {
13871355
throw TypeError("type() takes no keyword arguments");
@@ -1390,36 +1358,82 @@ private static object MakeClass(CodeContext/*!*/ context, string name, PythonTup
13901358
if (bases.Count == 0) {
13911359
bases = PythonTuple.MakeTuple(DynamicHelpers.GetPythonTypeFromType(typeof(object)));
13921360
}
1393-
return PythonType.__new__(context, TypeCache.PythonType, name, bases, vars, selfNames);
1361+
PythonDictionary vars = func(parentContext).Dict;
1362+
return PythonType.__new__(parentContext, TypeCache.PythonType, name, bases, vars, selfNames);
13941363
}
13951364

1396-
object? classdict = vars;
1397-
1398-
if (metaclass is PythonType metatype) {
1399-
classdict = CallPrepare(context, metatype, name, bases, keywords, vars);
1400-
}
1365+
// Prepare classdict
1366+
// TODO: prepared classdict should be used by `func` (PEP 3115)
1367+
object? classdict = CallPrepare(parentContext, metaclass, name, bases, keywords, func(parentContext).Dict);
14011368

1402-
// eg:
1403-
// def foo(*args): print(args)
1404-
// class bar(metaclass=foo): pass
1369+
// Dispatch to the metaclass to do class creation and initialization
1370+
// metaclass could be simply a callable, eg:
1371+
// >>> def foo(*args): print(args)
1372+
// >>> class bar(metaclass=foo): pass
14051373
// calls our function...
1406-
PythonContext pc = context.LanguageContext;
1374+
PythonContext pc = parentContext.LanguageContext;
14071375

14081376
object obj = pc.MetaClassCallSite.Target(
14091377
pc.MetaClassCallSite,
1410-
context,
1378+
parentContext,
14111379
metaclass,
14121380
name,
14131381
bases,
14141382
classdict,
1415-
keywords
1383+
keywords ?? MakeEmptyDict()
14161384
);
14171385

1386+
// Ensure the class derives from `object`
14181387
if (obj is PythonType newType && newType.BaseTypes.Count == 0) {
14191388
newType.BaseTypes.Add(DynamicHelpers.GetPythonTypeFromType(typeof(object)));
14201389
}
14211390

14221391
return obj;
1392+
// ------------------------------------------------------------------------
1393+
1394+
static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, FunctionCode funcCode, Func<CodeContext, CodeContext> body) {
1395+
if (body == null) {
1396+
if (funcCode.Target == null) {
1397+
funcCode.UpdateDelegate(context.LanguageContext, true);
1398+
}
1399+
return (Func<CodeContext, CodeContext>)funcCode.Target!;
1400+
} else {
1401+
if (funcCode.Target == null) {
1402+
funcCode.SetTarget(body);
1403+
funcCode._normalDelegate = body;
1404+
}
1405+
return body;
1406+
}
1407+
}
1408+
1409+
static object? CallPrepare(CodeContext/*!*/ context, object meta, string name, PythonTuple bases, PythonDictionary? keywords, PythonDictionary dict) {
1410+
object? classdict = dict;
1411+
1412+
object? prepareFunc = null;
1413+
// if available, call the __prepare__ method to get the classdict (PEP 3115)
1414+
if (meta is PythonType metatype) {
1415+
if (metatype.TryResolveSlot(context, "__prepare__", out PythonTypeSlot pts)) {
1416+
bool ok = pts.TryGetValue(context, null, metatype, out prepareFunc);
1417+
Debug.Assert(ok);
1418+
}
1419+
} else if (!TryGetBoundAttr(context, meta, "__prepare__", out prepareFunc)) {
1420+
prepareFunc = null;
1421+
}
1422+
1423+
if (prepareFunc is not null) {
1424+
if (keywords is null || keywords.Count == 0) {
1425+
classdict = PythonCalls.Call(context, prepareFunc, name, bases);
1426+
} else {
1427+
var args = new object[] { name, bases };
1428+
classdict = PythonCalls.CallWithKeywordArgs(context, prepareFunc, args, keywords);
1429+
}
1430+
// copy the contents of dict to the classdict
1431+
foreach (var pair in dict)
1432+
context.LanguageContext.SetIndex(classdict, pair.Key, pair.Value);
1433+
}
1434+
1435+
return classdict;
1436+
}
14231437
}
14241438

14251439
public static void RaiseAssertionError(CodeContext context) {

Src/IronPython/Runtime/Types/PythonType.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,15 @@ 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, null, dict);
271-
272-
if (meta != cls) {
273-
// the user has a custom __new__ which picked the wrong meta class, call the correct metaclass
274-
type = PythonCalls.Call(context, meta, name, bases, classdict);
270+
if (meta != cls // the user has a custom __new__ which picked the wrong meta class, call the correct metaclass __new__
271+
&& meta.TryResolveSlot(context, "__new__", out PythonTypeSlot pts)
272+
&& pts.TryGetValue(context, null, meta, out object value)
273+
) {
274+
type = PythonCalls.Call(context, value, meta, name, bases, dict);
275275
} else {
276276
// we have the right user __new__, call our ctor method which will do the actual
277277
// creation.
278-
type = meta.CreateInstance(context, name, bases, classdict);
278+
type = meta.CreateInstance(context, name, bases, dict);
279279
}
280280
} else {
281281
// no custom user type for __new__
@@ -292,7 +292,7 @@ internal static object __new__(CodeContext/*!*/ context, PythonType cls, string
292292
}
293293

294294
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
295-
public void __init__(string name, PythonTuple bases, PythonDictionary dict) {
295+
public void __init__(string name, PythonTuple bases, PythonDictionary dict, [ParamDictionary] IDictionary<object, object> kwargs) {
296296
}
297297

298298
internal static PythonType FindMetaClass(PythonType cls, PythonTuple bases) {

0 commit comments

Comments
 (0)