Add support for defining Python metaclasses in Rust via #[pyclass(extends = PyType)]#2
Add support for defining Python metaclasses in Rust via #[pyclass(extends = PyType)]#2Copilot wants to merge 7 commits into
#[pyclass(extends = PyType)]#2Conversation
…d tests Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/e175b5f4-84df-4473-9d22-6f3f123cb073 Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/e175b5f4-84df-4473-9d22-6f3f123cb073 Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
#[pyclass(metaclass)] support for defining Python metaclasses in Rust
MatthieuDartiailh
left a comment
There was a problem hiding this comment.
@copilot the implementation looks good for a first pass. I would like to see added:
- support for
__mro_entries__ - support for
__prepare__ - support for extending a metaclass defined in rust
- a safe helper to call type.new inside a metaclass new
- documentation around defining a metaclass and the caveat when writing
__new__
…metaclass Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/dc6007f2-bee3-4d2f-b054-d99f6cd9cf04 Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Done in commit c6b43e7. Here's what was added:
|
|
@copilot actually |
Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/2c097a56-8b25-448c-b85f-8f6637bea6c5 Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Removed in commit f41f022 — the |
MatthieuDartiailh
left a comment
There was a problem hiding this comment.
Reading the code it appears to me the metaclass arg to pyclass is not really necessary. Its value could be inferred from the fact that the class extend PyType or a class which itself extends PyType in which case the type marker IS_METACLASS is set allowing to easily identify we are building a metaclass without walking the mro.
@copilot Please remove the metaclass arg and update both tests and documentation accordingly.
…onst propagation Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/db9a9a72-ef3c-4242-a4cd-afa90cacba42 Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Done in commit a8e1903. The
Implementation: |
#[pyclass(metaclass)] support for defining Python metaclasses in Rust#[pyclass(extends = PyType)]
| let cls = self.cls_ident; | ||
| let attr = self.attr; | ||
| // If #cls is not extended type, we allow Self->PyObject conversion | ||
| // If the class is not an extended type, we allow Self->PyObject conversion |
There was a problem hiding this comment.
The original comment should be kept.
There was a problem hiding this comment.
Restored in commit 9196c60 — // If #cls is not extended type, we allow Self->PyObject conversion.
| let pyclass_base_type_impl = attr.options.subclass.map(|subclass| { | ||
| quote_spanned! { subclass.span() => | ||
| let pyclass_base_type_impl = if attr.options.subclass.is_some() || attr.options.extends.is_some() { | ||
| let span = attr.options.subclass | ||
| .map(|s| s.span()) | ||
| .or_else(|| attr.options.extends.as_ref().map(|e| e.span())) | ||
| .unwrap_or_else(|| cls.span()); | ||
| Some(quote_spanned! { span => |
There was a problem hiding this comment.
This breaks the current behavior of PyO3 and is not required to support metaclasses. That change should be reverted.
There was a problem hiding this comment.
Reverted in commit 9196c60 — pyclass_base_type_impl is back to attr.options.subclass.map(...) only. To extend a Rust metaclass the base must declare subclass; the test was updated accordingly (SimpleMeta now has subclass).
| .set_is_basetype(is_basetype) | ||
| // Metaclasses are always subclassable: Python's `type` itself has BASETYPE | ||
| // set, and any subclass of `type` should likewise be subclassable so that | ||
| // users can further specialize the metaclass hierarchy in Python. | ||
| .set_is_basetype(is_basetype || is_metaclass) |
There was a problem hiding this comment.
PyO3 allows to mark any class as final by setting is base type to false. What is the reasoning to force metaclasses to always be subclassable ? In the absence of a valid reason the change should be reverted.
There was a problem hiding this comment.
Reverted in commit 9196c60 — set_is_basetype(is_basetype) (no || is_metaclass). Metaclasses now follow the same subclass / final rules as any other class.
|
@copilot address latest review |
…pl and forced basetype, fix UI tests Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/58f828a7-7280-4d51-b9a0-6229399c988c Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Done in commit 9196c60. |
Adds the ability to define Python metaclasses directly in Rust. A struct that extends
PyType(i.e.#[pyclass(extends = PyType)]) becomes a subtype of Python'stype, allowing it to control class creation and customize type behavior for any Python class that uses it as a metaclass.Metaclass status is inferred automatically from the type hierarchy — no explicit
metaclasskeyword is needed. Any struct that extendsPyTypeor another metaclass hasIS_METACLASS = truepropagated through the chain at compile time viaPyClassBaseType::IS_METACLASS.Changes
Macro / code generation (
pyo3-macros-backend)pyclass.rs:IS_METACLASSis now computed as<BaseType as PyClassBaseType>::IS_METACLASS— a plain const expression that propagates transitively; no explicitmetaclassattribute neededPyClassBaseTypeimpl is generated for classes withsubclass(original behaviour preserved); the generated impl now also forwardsIS_METACLASSso the flag propagates correctly through metaclass chainssubclass(same rule as any other subclassable PyO3 class)Runtime type construction (
src/)types/typeobject.rs:PyClassBaseTypeimpl forPyTypenow includesconst IS_METACLASS: bool = true, making it the root of metaclass detectionPyType::metaclass_type_new()— a safe public helper that invokes CPython'stype_newC slot directly, bypassing the Python-leveltype.__new__safety check that rejects PyO3 metaclassesimpl_/pyclass.rs:IS_METACLASSconstant to bothPyClassImpltrait (defaultfalse) andPyClassBaseTypetrait (defaultfalse)pyclass/create_type_object.rs:no_constructor_definedstub sotype.__new__is inherited when no#[new]is definedsubclass/ finality rules as any other class (no forcedPy_TPFLAGS_BASETYPE)Documentation
guide/src/class.md: Added "Defining a metaclass in Rust" section covering all hooks,__prepare__, custom__new__with the safe helper, extending a Rust metaclass (requiressubclasson the base), and known limitationsTests
tests/test_metaclass.rs: 9 integration tests covering the full surface areaUsage
Custom
__new__— safe helperWhen defining
__new__, use#[new] #[classmethod]and returnPy<Self>. Use the newPyType::metaclass_type_newsafe helper instead of calling raw FFI — it bypasses CPython'smetatype->tp_new != type_newsafety check that would otherwise reject PyO3 metaclasses:Extending a Rust metaclass
Add
subclassto the base metaclass to make it eligible, then extend it with a normalextends:Out of scope
#[pyclass]via#[pyclass(metaclass = MyMeta)]