[Python-checkins] [3.11] GH-94822: Don't specialize when metaclasses are involved (GH-94892) (GH-94980)

brandtbucher webhook-mailer at python.org
Mon Jul 18 14:55:31 EDT 2022


https://github.com/python/cpython/commit/eda2f90094731a7a1a30bc4a50a6c1bc050a4b2c
commit: eda2f90094731a7a1a30bc4a50a6c1bc050a4b2c
branch: 3.11
author: Brandt Bucher <brandtbucher at microsoft.com>
committer: brandtbucher <brandtbucher at gmail.com>
date: 2022-07-18T11:55:07-07:00
summary:

[3.11] GH-94822: Don't specialize when metaclasses are involved (GH-94892) (GH-94980)

(cherry picked from commit daf68ba92f315bfd239a0c993f9f683fb90325fb)

Co-authored-by: Brandt Bucher <brandtbucher at microsoft.com>

files:
A Misc/NEWS.d/next/Core and Builtins/2022-07-15-22-16-08.gh-issue-94822.zRRzBN.rst
M Lib/test/test_opcache.py
M Python/specialize.c

diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py
index 61f337d70ea78..5c032d59b13f1 100644
--- a/Lib/test/test_opcache.py
+++ b/Lib/test/test_opcache.py
@@ -1,5 +1,6 @@
 import unittest
 
+
 class TestLoadAttrCache(unittest.TestCase):
     def test_descriptor_added_after_optimization(self):
         class Descriptor:
@@ -21,3 +22,346 @@ def f(o):
         Descriptor.__set__ = lambda *args: None
 
         self.assertEqual(f(o), 2)
+
+    def test_metaclass_descriptor_added_after_optimization(self):
+        class Descriptor:
+            pass
+
+        class Metaclass(type):
+            attribute = Descriptor()
+
+        class Class(metaclass=Metaclass):
+            attribute = True
+
+        def __get__(self, instance, owner):
+            return False
+
+        def __set__(self, instance, value):
+            return None
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Descriptor.__get__ = __get__
+        Descriptor.__set__ = __set__
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_metaclass_descriptor_shadows_class_attribute(self):
+        class Metaclass(type):
+            @property
+            def attribute(self):
+                return True
+
+        class Class(metaclass=Metaclass):
+            attribute = False
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+    def test_metaclass_set_descriptor_after_optimization(self):
+        class Metaclass(type):
+            pass
+
+        class Class(metaclass=Metaclass):
+            attribute = True
+
+        @property
+        def attribute(self):
+            return False
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Metaclass.attribute = attribute
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_metaclass_del_descriptor_after_optimization(self):
+        class Metaclass(type):
+            @property
+            def attribute(self):
+                return True
+
+        class Class(metaclass=Metaclass):
+            attribute = False
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        del Metaclass.attribute
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_type_descriptor_shadows_attribute_method(self):
+        class Class:
+            mro = None
+
+        def f():
+            return Class.mro
+
+        for _ in range(1025):
+            self.assertIsNone(f())
+
+    def test_type_descriptor_shadows_attribute_member(self):
+        class Class:
+            __base__ = None
+
+        def f():
+            return Class.__base__
+
+        for _ in range(1025):
+            self.assertIs(f(), object)
+
+    def test_type_descriptor_shadows_attribute_getset(self):
+        class Class:
+            __name__ = "Spam"
+
+        def f():
+            return Class.__name__
+
+        for _ in range(1025):
+            self.assertEqual(f(), "Class")
+
+    def test_metaclass_getattribute(self):
+        class Metaclass(type):
+            def __getattribute__(self, name):
+                return True
+
+        class Class(metaclass=Metaclass):
+            attribute = False
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+    def test_metaclass_swap(self):
+        class OldMetaclass(type):
+            @property
+            def attribute(self):
+                return True
+
+        class NewMetaclass(type):
+            @property
+            def attribute(self):
+                return False
+
+        class Class(metaclass=OldMetaclass):
+            pass
+
+        def f():
+            return Class.attribute
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Class.__class__ = NewMetaclass
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+
+class TestLoadMethodCache(unittest.TestCase):
+    def test_descriptor_added_after_optimization(self):
+        class Descriptor:
+            pass
+
+        class Class:
+            attribute = Descriptor()
+
+        def __get__(self, instance, owner):
+            return lambda: False
+
+        def __set__(self, instance, value):
+            return None
+
+        def attribute():
+            return True
+
+        instance = Class()
+        instance.attribute = attribute
+
+        def f():
+            return instance.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Descriptor.__get__ = __get__
+        Descriptor.__set__ = __set__
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_metaclass_descriptor_added_after_optimization(self):
+        class Descriptor:
+            pass
+
+        class Metaclass(type):
+            attribute = Descriptor()
+
+        class Class(metaclass=Metaclass):
+            def attribute():
+                return True
+
+        def __get__(self, instance, owner):
+            return lambda: False
+
+        def __set__(self, instance, value):
+            return None
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Descriptor.__get__ = __get__
+        Descriptor.__set__ = __set__
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_metaclass_descriptor_shadows_class_attribute(self):
+        class Metaclass(type):
+            @property
+            def attribute(self):
+                return lambda: True
+
+        class Class(metaclass=Metaclass):
+            def attribute():
+                return False
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+    def test_metaclass_set_descriptor_after_optimization(self):
+        class Metaclass(type):
+            pass
+
+        class Class(metaclass=Metaclass):
+            def attribute():
+                return True
+
+        @property
+        def attribute(self):
+            return lambda: False
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Metaclass.attribute = attribute
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_metaclass_del_descriptor_after_optimization(self):
+        class Metaclass(type):
+            @property
+            def attribute(self):
+                return lambda: True
+
+        class Class(metaclass=Metaclass):
+            def attribute():
+                return False
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        del Metaclass.attribute
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+    def test_type_descriptor_shadows_attribute_method(self):
+        class Class:
+            def mro():
+                return ["Spam", "eggs"]
+
+        def f():
+            return Class.mro()
+
+        for _ in range(1025):
+            self.assertEqual(f(), ["Spam", "eggs"])
+
+    def test_type_descriptor_shadows_attribute_member(self):
+        class Class:
+            def __base__():
+                return "Spam"
+
+        def f():
+            return Class.__base__()
+
+        for _ in range(1025):
+            self.assertNotEqual(f(), "Spam")
+
+    def test_metaclass_getattribute(self):
+        class Metaclass(type):
+            def __getattribute__(self, name):
+                return lambda: True
+
+        class Class(metaclass=Metaclass):
+            def attribute():
+                return False
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+    def test_metaclass_swap(self):
+        class OldMetaclass(type):
+            @property
+            def attribute(self):
+                return lambda: True
+
+        class NewMetaclass(type):
+            @property
+            def attribute(self):
+                return lambda: False
+
+        class Class(metaclass=OldMetaclass):
+            pass
+
+        def f():
+            return Class.attribute()
+
+        for _ in range(1025):
+            self.assertTrue(f())
+
+        Class.__class__ = NewMetaclass
+
+        for _ in range(1025):
+            self.assertFalse(f())
+
+
+if __name__ == "__main__":
+    import unittest
+    unittest.main()
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-07-15-22-16-08.gh-issue-94822.zRRzBN.rst b/Misc/NEWS.d/next/Core and Builtins/2022-07-15-22-16-08.gh-issue-94822.zRRzBN.rst
new file mode 100644
index 0000000000000..5b24918e49770
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-07-15-22-16-08.gh-issue-94822.zRRzBN.rst	
@@ -0,0 +1,2 @@
+Fix an issue where lookups of metaclass descriptors may be ignored when an
+identically-named attribute also exists on the class itself.
diff --git a/Python/specialize.c b/Python/specialize.c
index cf7bc32a205be..5f2c6b390633e 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -871,6 +871,10 @@ specialize_class_load_method(PyObject *owner, _Py_CODEUNIT *instr,
                              PyObject *name)
 {
     _PyLoadMethodCache *cache = (_PyLoadMethodCache *)(instr + 1);
+    if (!PyType_CheckExact(owner) || _PyType_Lookup(Py_TYPE(owner), name)) {
+        SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_LOAD_METHOD_METACLASS_ATTRIBUTE);
+        return -1;
+    }
     PyObject *descr = NULL;
     DescriptorClassification kind = 0;
     kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0);
@@ -883,12 +887,7 @@ specialize_class_load_method(PyObject *owner, _Py_CODEUNIT *instr,
             return 0;
 #ifdef Py_STATS
         case ABSENT:
-            if (_PyType_Lookup(Py_TYPE(owner), name) != NULL) {
-                SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_LOAD_METHOD_METACLASS_ATTRIBUTE);
-            }
-            else {
-                SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_EXPECTED_ERROR);
-            }
+            SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_EXPECTED_ERROR);
             return -1;
 #endif
         default:



More information about the Python-checkins mailing list