Reject __abc_tpflags__ with both SEQUENCE and MAPPING bits (#7700)

CPython's _abc._abc_init validates that __abc_tpflags__ doesn't combine
Py_TPFLAGS_SEQUENCE and Py_TPFLAGS_MAPPING (Modules/_abc.c::_abc_init).
RustPython's check_abc_tpflags copied the bits onto slots.flags without
that validation, so a class declaring both produced an inconsistent
type silently.

Validate the masked __abc_tpflags__ value against COLLECTION_FLAGS and
return a String error (which the existing new_heap_inner caller turns
into a TypeError) when both bits are present. Move the inheritance-skip
check after the self-attrs branch so a child overriding __abc_tpflags__
is still validated even when the parent already set one of the bits.
This commit is contained in:
Changjoon authored and GitHub committed 2026-04-28 21:09:16 +09:00
1 parent 1d42ee565f
commit b8f7ae4265
2 files changed
+35 -14

No files matched your search

-1
View File
@@ -2028,7 +2028,6 @@ class TestCollectionABCs(ABCTestCase):
self.assertEqual(len(mss), len(mss2))
self.assertEqual(list(mss), list(mss2))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised
def test_illegal_patma_flags(self):
with self.assertRaises(TypeError):
class Both(Collection):
+35 -13
View File
@@ -604,38 +604,60 @@ impl PyType {
attrs: &PyAttributes,
bases: &[PyRef<Self>],
ctx: &Context,
) {
) -> Result<(), String> {
const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate(
PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(),
);
// Don't override if flags are already set
if slots.flags.intersects(COLLECTION_FLAGS) {
return;
}
// First check in our own attributes
// Always validate this class's own __abc_tpflags__ even when slot
// flags were already inherited, otherwise a child setting both
// Py_TPFLAGS_SEQUENCE and Py_TPFLAGS_MAPPING would slip through.
let abc_tpflags_name = ctx.intern_str("__abc_tpflags__");
if let Some(abc_tpflags_obj) = attrs.get(abc_tpflags_name)
&& let Some(int_obj) = abc_tpflags_obj.downcast_ref::<crate::builtins::int::PyInt>()
{
let flags_val = int_obj.as_bigint().to_i64().unwrap_or(0);
let abc_flags = PyTypeFlags::from_bits_truncate(flags_val as u64);
slots.flags |= abc_flags & COLLECTION_FLAGS;
return;
let masked = abc_flags & COLLECTION_FLAGS;
if masked == COLLECTION_FLAGS {
return Err(
"__abc_tpflags__ cannot be both Py_TPFLAGS_SEQUENCE and Py_TPFLAGS_MAPPING"
.to_owned(),
);
}
// Don't override flags already inherited from a base class.
if !slots.flags.intersects(COLLECTION_FLAGS) {
slots.flags |= masked;
}
return Ok(());
}
// Then check in base classes
// No __abc_tpflags__ on this class — inheritance already happened
// in inherit_patma_flags, so nothing more to do if those bits are set.
if slots.flags.intersects(COLLECTION_FLAGS) {
return Ok(());
}
// Then check in base classes (legacy path for cases that bypass
// inherit_patma_flags).
for base in bases {
if let Some(abc_tpflags_obj) = base.find_name_in_mro(abc_tpflags_name)
&& let Some(int_obj) = abc_tpflags_obj.downcast_ref::<crate::builtins::int::PyInt>()
{
let flags_val = int_obj.as_bigint().to_i64().unwrap_or(0);
let abc_flags = PyTypeFlags::from_bits_truncate(flags_val as u64);
slots.flags |= abc_flags & COLLECTION_FLAGS;
return;
let masked = abc_flags & COLLECTION_FLAGS;
if masked == COLLECTION_FLAGS {
return Err(
"__abc_tpflags__ cannot be both Py_TPFLAGS_SEQUENCE and Py_TPFLAGS_MAPPING"
.to_owned(),
);
}
slots.flags |= masked;
return Ok(());
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@@ -671,7 +693,7 @@ impl PyType {
Self::inherit_patma_flags(&mut slots, &bases);
// Check for __abc_tpflags__ from ABCMeta (for collections.abc.Sequence, Mapping, etc.)
Self::check_abc_tpflags(&mut slots, &attrs, &bases, ctx);
Self::check_abc_tpflags(&mut slots, &attrs, &bases, ctx)?;
if slots.basicsize == 0 {
slots.basicsize = base.slots.basicsize;