colesbury · GitHub

Bug report

For example the following program fails with either assert WeirdClass.value == 2 or assert WeirdClass.value == 3 in recent Python versions:

import sys
class Base:
    value = 1
class Meta(type):
    def mro(cls):
        return (cls, Base, object)
class WeirdClass(metaclass=Meta):
    pass
assert Base.value == 1
assert WeirdClass.value == 1
Base.value = 2
assert Base.value == 2
assert WeirdClass.value == 2
Base.value = 3
assert Base.value == 3
assert WeirdClass.value == 3

Adding intervening calls to sys _clear_internal_caches() makes the test pass.

Version Result
3.7 OK
3.8 OK
3.9 OK
3.10 AssertionError: assert WeirdClass.value == 2
3.11 AssertionError: assert WeirdClass.value == 2
3.12 AssertionError: assert WeirdClass.value == 2
3.13 AssertionError: assert WeirdClass.value == 3
main AssertionError: assert WeirdClass.value == 3

We have code that checks for this case, but it hasn't worked properly in Python 3.10+:

static void
type_mro_modified(PyTypeObject *type, PyObject *bases) {
/*
Check that all base classes or elements of the MRO of type are
able to be cached. This function is called after the base
classes or mro of the type are altered.

We also have a test that partly covers this case, but doesn't detect the bug:

def test_freeze_meta(self):
"""test PyType_Freeze() with overridden MRO"""
type_freeze = _testcapi.type_freeze
class Base:
value = 1
class Meta(type):
def mro(cls):
return (cls, Base, object)
class FreezeThis(metaclass=Meta):
"""This has `Base` in the MRO, but not tp_bases"""
self.assertEqual(FreezeThis.value, 1)
with self.assertRaises(TypeError):
type_freeze(FreezeThis)
Base.value = 2
self.assertEqual(FreezeThis.value, 2)
type_freeze(Base)
with self.assertRaises(TypeError):
Base.value = 3
type_freeze(FreezeThis)
self.assertEqual(FreezeThis.value, 2)

Linked PRs

Read the original on github.com ↗