Skip to content

Commit 223eede

Browse files
authored
DeprecationWarning on failed __classcell__ propagation (#1517)
* DeprecationWarning on failed __classcell__ propagation * Pick up __classcell__ before call to __prepare__
1 parent 1714735 commit 223eede

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1471,9 +1471,13 @@ public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeCont
14711471
return PythonType.__new__(parentContext, TypeCache.PythonType, name, bases, vars, selfNames);
14721472
}
14731473

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+
14741478
// Prepare classdict
14751479
// TODO: prepared classdict should be used by `func` (PEP 3115)
1476-
object? classdict = CallPrepare(parentContext, metaclass, name, bases, keywords, func(parentContext).Dict);
1480+
object? classdict = CallPrepare(parentContext, metaclass, name, bases, keywords, classContext.Dict);
14771481

14781482
// Dispatch to the metaclass to do class creation and initialization
14791483
// metaclass could be simply a callable, eg:
@@ -1492,6 +1496,13 @@ public static object MakeClass(FunctionCode funcCode, Func<CodeContext, CodeCont
14921496
keywords ?? MakeEmptyDict()
14931497
);
14941498

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;
1504+
}
1505+
14951506
// Ensure the class derives from `object`
14961507
if (obj is PythonType newType && newType.BaseTypes.Count == 0) {
14971508
newType.BaseTypes.Add(TypeCache.Object);

Tests/test_class.py

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

1011
from iptest import IronPythonTestCase, is_cli, is_mono, myint, big, run_test, skipUnlessIronPython
1112

@@ -2621,6 +2622,103 @@ class y(object): pass
26212622
self.assertEqual(x.__bases__, (object, ))
26222623
self.assertEqual(x.__name__, 'x')
26232624

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
26242722

26252723
def test_issubclass(self):
26262724
# first argument doesn't need to be new-style or old-style class if it defines __bases__

0 commit comments

Comments
 (0)