Skip to content

Commit 0df498f

Browse files
authored
Improve handling of __classcell__ (#1520)
* Copy class namespace when using metaclass * Fix preconditions for type constructor * Improve handling of __classcell__ * Reorder class __doc__ and __module__ * Add tests * Change nullabiliy of CallPrepare return type * Pass __classcell__ tests from StdLib 3.6 * Move __classcell__ tests from test_class to test_super
1 parent eea91cb commit 0df498f

File tree

7 files changed

+329
-169
lines changed

7 files changed

+329
-169
lines changed

Src/IronPython/Compiler/Ast/ClassDefinition.cs

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,9 @@ internal override bool HasLateBoundVariableSets {
9999

100100
internal override bool ExposesLocalVariable(PythonVariable variable) => true;
101101

102-
private bool needClassCell { get; set; }
103102

104103
internal override bool TryBindOuter(ScopeStatement from, PythonReference reference, out PythonVariable variable) {
105104
if (reference.Name == "__class__") {
106-
needClassCell = true;
107105
ClassCellVariable = EnsureVariable("__classcell__");
108106
ClassVariable = variable = EnsureVariable(reference.Name);
109107
variable.AccessedInNestedScope = true;
@@ -260,44 +258,41 @@ private Microsoft.Scripting.Ast.LightExpression<Func<CodeContext, CodeContext>>
260258
var createLocal = CreateLocalContext(_parentContextParam);
261259

262260
init.Add(Ast.Assign(LocalCodeContextVariable, createLocal));
263-
// __classcell__ == ClosureCell(__class__)
264-
if (needClassCell) {
265-
var exp = (ClosureExpression) GetVariableExpression(ClassVariable!);
266-
MSAst.Expression assignClassCell = AssignValue(GetVariableExpression(ClassCellVariable!), exp.ClosureCell);
267-
init.Add(assignClassCell);
268-
}
269-
270-
List<MSAst.Expression> statements = new List<MSAst.Expression>();
271-
// Create the body
272-
MSAst.Expression bodyStmt = Body;
273-
274261

275262
// __module__ = __name__
276263
MSAst.Expression modStmt = AssignValue(GetVariableExpression(ModVariable!), GetVariableExpression(ModuleNameVariable!));
277264

265+
// TODO: set __qualname__
266+
267+
// __doc__ = """..."""
268+
MSAst.Expression? docStmt = null;
278269
string doc = GetDocumentation(Body);
279-
if (doc != null) {
280-
statements.Add(
281-
AssignValue(
282-
GetVariableExpression(DocVariable!),
283-
AstUtils.Constant(doc)
284-
)
285-
);
270+
if (doc is not null) {
271+
docStmt = AssignValue(GetVariableExpression(DocVariable!), AstUtils.Constant(doc));
286272
}
287273

274+
// Create the body
275+
MSAst.Expression bodyStmt = Body;
288276
if (Body.CanThrow && GlobalParent.PyContext.PythonOptions.Frames) {
289277
bodyStmt = AddFrame(LocalContext, FuncCodeExpr, bodyStmt);
290278
locals.Add(FunctionStackVariable);
291279
}
292280

281+
// __classcell__ == ClosureCell(__class__)
282+
MSAst.Expression? assignClassCellStmt = null;
283+
if (ClassCellVariable is not null) {
284+
var exp = (ClosureExpression)GetVariableExpression(ClassVariable!);
285+
assignClassCellStmt = AssignValue(GetVariableExpression(ClassCellVariable), exp.ClosureCell);
286+
}
287+
293288
bodyStmt = WrapScopeStatements(
294289
Ast.Block(
295290
Ast.Block(init),
296-
statements.Count == 0 ?
297-
EmptyBlock :
298-
Ast.Block(new ReadOnlyCollection<MSAst.Expression>(statements)),
299291
modStmt,
292+
// __qualname__
293+
docStmt is not null ? docStmt : AstUtils.Empty(),
300294
bodyStmt,
295+
assignClassCellStmt is not null ? assignClassCellStmt : AstUtils.Empty(),
301296
LocalContext
302297
),
303298
Body.CanThrow

Src/IronPython/Compiler/Ast/PythonNameBinder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,20 @@ private void Bind(PythonAst unboundAst) {
118118
// Find all scopes and variables
119119
unboundAst.Walk(this);
120120

121-
// Bind
121+
// Bind scopes
122122
foreach (ScopeStatement scope in _scopes) {
123123
scope.Bind(this);
124124
}
125125

126-
// Finish the globals
126+
// Bind globals
127127
unboundAst.Bind(this);
128128

129-
// Finish Binding w/ outer most scopes first.
129+
// Finish binding w/ outer most scopes first.
130130
for (int i = _scopes.Count - 1; i >= 0; i--) {
131131
_scopes[i].FinishBind(this);
132132
}
133133

134-
// Finish the globals
134+
// Finish globals
135135
unboundAst.FinishBind(this);
136136

137137
// Run flow checker

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ public static void InitializeForFinalization(CodeContext/*!*/ context, object ne
14141414
iwr.SetFinalizer(new WeakRefTracker(iwr, nif, nif));
14151415
}
14161416

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

14201420
// Check and normalize bases
@@ -1458,6 +1458,14 @@ public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeCont
14581458
}
14591459
} // else metaclass is expected to be a callable and overrides any inherited metaclass through any bases
14601460

1461+
// Call class body lambda
1462+
CodeContext classContext = func(parentContext);
1463+
PythonDictionary vars = classContext.Dict;
1464+
1465+
// Prepare classdict
1466+
// TODO: prepared classdict should be used by `func` (PEP 3115)
1467+
object classdict = CallPrepare(parentContext, metaclass, name, bases, keywords, vars);
1468+
14611469
// Fasttrack for metaclass == `type`
14621470
if (metaclass is null) {
14631471
if (keywords != null && keywords.Count > 0) {
@@ -1467,26 +1475,18 @@ public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeCont
14671475
if (bases.Count == 0) {
14681476
bases = PythonTuple.MakeTuple(TypeCache.Object);
14691477
}
1470-
PythonDictionary vars = func(parentContext).Dict;
1471-
return PythonType.__new__(parentContext, TypeCache.PythonType, name, bases, vars, selfNames);
1478+
1479+
return PythonType.__new__(parentContext, TypeCache.PythonType, name, bases, vars, selfNames)!;
14721480
}
14731481

1474-
CodeContext classContext = func(parentContext);
1475-
// If __classcell__ is defined, verify later that it makes all the way to type.__new__
1476-
var classCell = (ClosureCell?)classContext.Dict.get("__classcell__");
1477-
1478-
// Prepare classdict
1479-
// TODO: prepared classdict should be used by `func` (PEP 3115)
1480-
object? classdict = CallPrepare(parentContext, metaclass, name, bases, keywords, classContext.Dict);
1481-
14821482
// Dispatch to the metaclass to do class creation and initialization
14831483
// metaclass could be simply a callable, eg:
14841484
// >>> def foo(*args): print(args)
14851485
// >>> class bar(metaclass=foo): pass
14861486
// calls our function...
14871487
PythonContext pc = parentContext.LanguageContext;
14881488

1489-
object obj = pc.MetaClassCallSite.Target(
1489+
object? obj = pc.MetaClassCallSite.Target(
14901490
pc.MetaClassCallSite,
14911491
parentContext,
14921492
metaclass,
@@ -1496,11 +1496,24 @@ public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeCont
14961496
keywords ?? MakeEmptyDict()
14971497
);
14981498

1499-
if (classCell is not null && classCell.Value == Uninitialized.Instance) {
1500-
// Python 3.8: RuntimeError
1501-
Warn(parentContext, PythonExceptions.DeprecationWarning,
1502-
"__class__ not set defining '{0}' as {1}. Was __classcell__ propagated to type.__new__?", name, Repr(parentContext, obj));
1503-
classCell.Value = obj;
1499+
// If __class__ is used, verify that it has been set
1500+
if (vars._storage is RuntimeVariablesDictionaryStorage storage) {
1501+
int pos = Array.IndexOf(storage.Names, "__class__");
1502+
if (pos >= 0) {
1503+
ClosureCell classCell = storage.GetCell(pos);
1504+
if (!ReferenceEquals(classCell.Value, obj)) {
1505+
if (classCell.Value == Uninitialized.Instance) {
1506+
if (obj is not null) {
1507+
// Python 3.8: RuntimeError
1508+
Warn(parentContext, PythonExceptions.DeprecationWarning,
1509+
"__class__ not set defining '{0}' as {1}. Was __classcell__ propagated to type.__new__?", name, Repr(parentContext, obj));
1510+
}
1511+
classCell.Value = obj; // Fill in the cell, since type.__new__ didn't do it
1512+
} else {
1513+
throw TypeError("__class__ set to {2} defining '{0}' as {1}", name, Repr(parentContext, obj), Repr(parentContext, classCell.Value));
1514+
}
1515+
}
1516+
}
15041517
}
15051518

15061519
// Ensure the class derives from `object`
@@ -1526,8 +1539,10 @@ static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, Fun
15261539
}
15271540
}
15281541

1529-
static object? CallPrepare(CodeContext/*!*/ context, object meta, string name, PythonTuple bases, PythonDictionary? keywords, PythonDictionary dict) {
1530-
object? classdict = dict;
1542+
static object CallPrepare(CodeContext/*!*/ context, object? meta, string name, PythonTuple bases, PythonDictionary? keywords, PythonDictionary dict) {
1543+
if (meta is null) return dict;
1544+
1545+
object? classdict = null;
15311546

15321547
object? prepareFunc = null;
15331548
// if available, call the __prepare__ method to get the classdict (PEP 3115)
@@ -1552,7 +1567,7 @@ static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, Fun
15521567
context.LanguageContext.SetIndex(classdict, pair.Key, pair.Value);
15531568
}
15541569

1555-
return classdict;
1570+
return classdict ?? new PythonDictionary(dict);
15561571
}
15571572
}
15581573

Src/IronPython/Runtime/Types/PythonType.cs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -245,25 +245,18 @@ internal BuiltinFunction Ctor {
245245

246246
#region Public API
247247

248-
public static object __new__(CodeContext/*!*/ context, PythonType cls, string name, PythonTuple bases, PythonDictionary dict) {
248+
#nullable enable
249+
250+
public static object? __new__(CodeContext/*!*/ context, [NotNone] PythonType cls, [NotNone] string name, [NotNone] PythonTuple bases, [NotNone] PythonDictionary dict) {
249251
return __new__(context, cls, name, bases, dict, string.Empty);
250252
}
251253

252-
internal static object __new__(CodeContext/*!*/ context, PythonType cls, string name, PythonTuple bases, PythonDictionary dict, string selfNames) {
253-
if (name == null) {
254-
throw PythonOps.TypeError("type() argument 1 must be string, not None");
255-
}
256-
if (bases == null) {
257-
throw PythonOps.TypeError("type() argument 2 must be tuple, not None");
258-
}
259-
if (dict == null) {
260-
throw PythonOps.TypeError("TypeError: type() argument 3 must be dict, not None");
261-
}
254+
internal static object? __new__(CodeContext/*!*/ context, PythonType cls, string name, PythonTuple bases, PythonDictionary dict, string selfNames) {
262255

263256
EnsureModule(context, dict);
264257

265258
PythonType meta = FindMetaClass(cls, bases);
266-
object type;
259+
object? type;
267260

268261
if (meta != TypeCache.PythonType) {
269262
if (meta != cls // the user has a custom __new__ which picked the wrong meta class, call the correct metaclass __new__
@@ -280,16 +273,20 @@ internal static object __new__(CodeContext/*!*/ context, PythonType cls, string
280273
// no custom user type for __new__
281274
type = new PythonType(context, name, bases, dict, selfNames);
282275
}
283-
object cell = dict.get("__classcell__");
284-
if (cell is ClosureCell pycell)
285-
{
286-
pycell.Value = type;
287-
dict.RemoveDirect("__classcell__");
276+
277+
// set class cell, if any
278+
if (dict.TryGetValueNoMissing("__classcell__", out object? classCellObject)) {
279+
if (classCellObject is not ClosureCell classCell) {
280+
throw PythonOps.TypeErrorForBadInstance("__classcell__ must be a nonlocal cell, not <class '{0}'>", classCellObject);
281+
}
282+
classCell.Value = type;
288283
}
289-
return type;
290284

285+
return type;
291286
}
292287

288+
#nullable restore
289+
293290
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
294291
public void __init__(string name, PythonTuple bases, PythonDictionary dict, [ParamDictionary] IDictionary<object, object> kwargs) {
295292
}
@@ -2096,6 +2093,7 @@ private void PopulateDictionary(CodeContext/*!*/ context, string name, PythonTup
20962093

20972094
List<string> slots = GetSlots(vars);
20982095
if (slots != null) {
2096+
slots.Remove("__classcell__");
20992097
_slots = slots.ToArray();
21002098

21012099
int index = _originalSlotCount;
@@ -2142,8 +2140,8 @@ private void PopulateDictionary(CodeContext/*!*/ context, string name, PythonTup
21422140
}
21432141

21442142
foreach (var kvp in vars) {
2145-
if (kvp.Key is string) {
2146-
PopulateSlot((string)kvp.Key, kvp.Value);
2143+
if (kvp.Key is string skey && skey != "__classcell__") {
2144+
PopulateSlot(skey, kvp.Value);
21472145
}
21482146
}
21492147

Tests/test_class.py

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import sys
88
import unittest
9-
import warnings
109

1110
from iptest import IronPythonTestCase, is_cli, is_mono, myint, big, run_test, skipUnlessIronPython
1211

@@ -2622,104 +2621,6 @@ class y(object): pass
26222621
self.assertEqual(x.__bases__, (object, ))
26232622
self.assertEqual(x.__name__, 'x')
26242623

2625-
def test_class_attribute(self):
2626-
class C:
2627-
def f(self):
2628-
return self.__class__
2629-
def g(self):
2630-
return __class__
2631-
def h():
2632-
return __class__
2633-
@staticmethod
2634-
def j():
2635-
return __class__
2636-
@classmethod
2637-
def k(cls):
2638-
return __class__
2639-
2640-
self.assertEqual(C().f(), C)
2641-
self.assertEqual(C().g(), C)
2642-
self.assertEqual(C.h(), C)
2643-
self.assertEqual(C.j(), C)
2644-
self.assertEqual(C.k(), C)
2645-
self.assertEqual(C().k(), C)
2646-
2647-
# Test that a metaclass implemented as a function sets __class__ at a proper moment
2648-
def makeclass(name, bases, attrs):
2649-
attrNames = set(attrs.keys())
2650-
self.assertRaisesMessage(NameError, "free variable '__class__' referenced before assignment in enclosing scope", attrs['getclass'], None)
2651-
if (is_cli or sys.version_info >= (3, 6)):
2652-
self.assertIn('__classcell__', attrNames)
2653-
2654-
t = type(name, bases, attrs)
2655-
2656-
if (is_cli or sys.version_info >= (3, 6)):
2657-
self.assertEqual(t.getclass(None), t) # __class__ is set right after the type is created
2658-
else: # CPython 3.5-
2659-
self.assertRaisesMessage(NameError, "free variable '__class__' referenced before assignment in enclosing scope", attrs['getclass'], None)
2660-
if not is_cli:
2661-
self.assertEqual(set(attrs.keys()), attrNames) # set of attrs is not modified by type creation
2662-
else:
2663-
# TODO: prevent modification of attrs in IronPython
2664-
self.assertEqual(set(attrs.keys()) | {'__classcell__'}, attrNames | {'__class__'})
2665-
2666-
return t
2667-
2668-
class A(metaclass=makeclass):
2669-
def getclass(self):
2670-
return __class__
2671-
2672-
self.assertEquals(A.getclass(None), A)
2673-
self.assertEquals(A().getclass(), A)
2674-
2675-
dirA = dir(A)
2676-
self.assertIn('getclass', dirA)
2677-
self.assertIn('getclass', A.__dict__)
2678-
self.assertIn('__class__', dirA)
2679-
self.assertNotIn('__class__', A.__dict__)
2680-
if not is_cli:
2681-
self.assertNotIn('__classcell__', dirA)
2682-
self.assertNotIn('__classcell__', A.__dict__)
2683-
else:
2684-
# TODO: filter out __classcell__ in IronPython
2685-
self.assertIn('__classcell__', dirA)
2686-
self.assertIn('__classcell__', A.__dict__)
2687-
2688-
def test_classcell_propagation(self):
2689-
with warnings.catch_warnings(record=True) as ws:
2690-
warnings.simplefilter("always")
2691-
2692-
with self.assertWarnsRegex(DeprecationWarning, r"^__class__ not set defining 'MyClass' as <class '.*\.MyClass'>\. Was __classcell__ propagated to type\.__new__\?$"):
2693-
class MyDict(dict):
2694-
def __setitem__(self, key, value):
2695-
pass
2696-
2697-
class MetaClass(type):
2698-
@classmethod
2699-
def __prepare__(metacls, name, bases):
2700-
return MyDict()
2701-
2702-
class MyClass(metaclass=MetaClass):
2703-
def test(self):
2704-
return __class__
2705-
2706-
self.assertEqual(len(ws), 0) # no unchecked warnings
2707-
2708-
with warnings.catch_warnings(record=True) as ws:
2709-
warnings.simplefilter("always")
2710-
2711-
with self.assertWarnsRegex(DeprecationWarning, r"^__class__ not set defining 'bar' as <class '.*\.gez'>\. Was __classcell__ propagated to type\.__new__\?$"):
2712-
class gez: pass
2713-
2714-
def foo(*args):
2715-
return gez
2716-
2717-
class bar(metaclass=foo):
2718-
def barfun(self):
2719-
return __class__
2720-
2721-
self.assertEqual(len(ws), 0) # no unchecked warnings
2722-
27232624
def test_issubclass(self):
27242625
# first argument doesn't need to be new-style or old-style class if it defines __bases__
27252626
class C(object):

0 commit comments

Comments
 (0)