diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index f54703cf37b2de5..6be2e471d1744e7 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -630,6 +630,25 @@ def test_code_hash_uses_bytecode(self): self.assertNotEqual(c, c1) self.assertNotEqual(hash(c), hash(c1)) + def test_code_equality_uses_consts(self): + c = compile("0.0", "example.py", "eval") + self.assertEqual(c.co_consts, (0.0,)) + self.assertEqual(c, c.replace(co_consts=(0.0,))) + for consts in [(-0.0,), (0,), (False,), ((0.0,),), (0.0, None)]: + with self.subTest(consts=consts): + self.assertNotEqual(c, c.replace(co_consts=consts)) + + def test_code_equality_nested(self): + # gh-160788: this took time exponential in the nesting depth. + depth = 30 + source = "".join(" " * i + f"class C{i}:\n" for i in range(depth)) + indent = " " * depth + code1 = compile(source + indent + "pass\n", "example.py", "exec") + code2 = compile(source + indent + "pass\n", "example.py", "exec") + code3 = compile(source + indent + "x = 1\n", "example.py", "exec") + self.assertEqual(code1, code2) + self.assertNotEqual(code1, code3) + @cpython_only def test_code_equal_with_instrumentation(self): """ GH-109052 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst new file mode 100644 index 000000000000000..61c3ae662c4923d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst @@ -0,0 +1,2 @@ +Comparing :ref:`code objects ` with ``==`` no longer takes +time exponential in the nesting depth of their classes and functions. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index eb390ab24ec13e6..a45f9e7a85a868d 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -2500,7 +2500,7 @@ code_richcompare(PyObject *self, PyObject *other, int op) { PyCodeObject *co, *cp; int eq; - PyObject *consts1, *consts2; + PyObject *key1, *key2; PyObject *res; if ((op != Py_EQ && op != Py_NE) || @@ -2537,19 +2537,25 @@ code_richcompare(PyObject *self, PyObject *other, int op) i += _PyOpcode_Caches[co_instr.op.code]; } - /* compare constants */ - consts1 = _PyCode_ConstantKey(co->co_consts); - if (!consts1) - return NULL; - consts2 = _PyCode_ConstantKey(cp->co_consts); - if (!consts2) { - Py_DECREF(consts1); - return NULL; + /* compare constants one by one: the key of the whole tuple also holds + the tuple itself, so comparing it would compare nested code objects + twice, which takes exponential time in the nesting depth */ + eq = PyTuple_GET_SIZE(co->co_consts) == PyTuple_GET_SIZE(cp->co_consts); + if (!eq) goto unequal; + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(co->co_consts); i++) { + key1 = _PyCode_ConstantKey(PyTuple_GET_ITEM(co->co_consts, i)); + if (!key1) + return NULL; + key2 = _PyCode_ConstantKey(PyTuple_GET_ITEM(cp->co_consts, i)); + if (!key2) { + Py_DECREF(key1); + return NULL; + } + eq = PyObject_RichCompareBool(key1, key2, Py_EQ); + Py_DECREF(key1); + Py_DECREF(key2); + if (eq <= 0) goto unequal; } - eq = PyObject_RichCompareBool(consts1, consts2, Py_EQ); - Py_DECREF(consts1); - Py_DECREF(consts2); - if (eq <= 0) goto unequal; eq = PyObject_RichCompareBool(co->co_names, cp->co_names, Py_EQ); if (eq <= 0) goto unequal;