From ebcd4f5a3312d81688d9946ffd3a1728917fe825 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 27 Jul 2017 23:05:15 +0800 Subject: [PATCH] subclass support (#64) --- CHANGES.txt | 2 ++ guide/src/class.md | 53 +++++++++++++++++++++-------------------- pyo3cls/src/py_class.rs | 4 ++++ src/typeob.rs | 9 +++++++ tests/test_class.rs | 15 ++++++++++++ 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b3edfcc6..a780238d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,8 @@ Changes * Refactor `PyErr` implementation. Drop `py` parameter from constructor. +* Added subclass support #64 + 0.1.0 (07-23-2017) ^^^^^^^^^^^^^^^^^^ diff --git a/guide/src/class.md b/guide/src/class.md index 2dfbe4cf..a204aaf9 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1,7 +1,7 @@ # Python Class Python class generation is powered by unstable [Procedural Macros](https://doc.rust-lang.org/book/first-edition/procedural-macros.html) and -[Specialization](https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md) and [Const fn](https://github.com/rust-lang/rfcs/blob/master/text/0911-const-fn.md) +[Specialization](https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md) and [Const fn](https://github.com/rust-lang/rfcs/blob/master/text/0911-const-fn.md) features, so you need to turn on `proc_macro` and `specialization` features: ```rust @@ -61,6 +61,7 @@ python object that can be collector `PyGCProtocol` trait has to be implemented. * `weakref` - adds support for python weak references * `base=xxx.YYY` - use custom base class. It is not possible to call constructor of base class at the moment. `xxx.YYY`, `xxx` - module name, `YYY` class name. +* `subclass` - adds subclass support so that Python classes can inherit from this class ## Constructor @@ -277,7 +278,7 @@ impl MyClass { By default pyo3 library uses function signature to determine which arguments are required. Then it scans incoming `args` parameter and then incoming `kwargs` parameter. If it can not -find all required parameters, it raises `TypeError` exception. +find all required parameters, it raises `TypeError` exception. It is possible to override default bahavior with `#[args(...)]` attribute. `args` attribute accept comma separated list of parameters in form `attr_name="default value"`. Each parameter has to match method parameter by name. @@ -291,15 +292,15 @@ Each parameter could one of following type: * kwargs="\*\*": "kwargs" is kwyword arguments, coresponds to python's `def meth(**kwargs)`. Type of `kwargs` parameter has to be `Option<&PyDict>`. * arg="Value": arguments with default value. coresponds to python's `def meth(arg=Value)`. - if `arg` argument is defined after var arguments it is treated as keyword argument. - Note that `Value` has to be valid rust code, pyo3 just inserts it into generated + if `arg` argument is defined after var arguments it is treated as keyword argument. + Note that `Value` has to be valid rust code, pyo3 just inserts it into generated code unmodified. - + Example: ```rust #[py::methods] impl MyClass { - + #[args(arg1=true, args="*", arg2=10, kwargs="**")] fn method(&self, arg1: bool, args: &PyTuple, arg2: i32, kwargs: Option<&PyTuple>) -> PyResult { Ok(1) @@ -310,10 +311,10 @@ impl MyClass { ## Class customizations -Python object model defines several protocols for different object behavior, +Python object model defines several protocols for different object behavior, like sequence, mapping or number protocols. pyo3 library defines separate trait for each of them. To provide specific python object behavior you need to implement specific trait -for your struct. Important note, each protocol implementation block has to be annotated +for your struct. Important note, each protocol implementation block has to be annotated with `#[py::proto]` attribute. ### Basic object customization @@ -330,49 +331,49 @@ To customize object attribute access define following methods: Each methods coresponds to python's `self.attr`, `self.attr = value` and `del self.attr` code. -#### String Conversions +#### String Conversions * `fn __repr__(&self) -> PyResult>` * `fn __str__(&self) -> PyResult>` - + Possible return types for `__str__` and `__repr__` are `PyResult` or `PyResult`. In Python 2.7, Unicode strings returned by `__str__` and `__repr__` will be converted to byte strings by the Python runtime, which results in an exception if the string contains non-ASCII characters. - + * `fn __bytes__(&self) -> PyResult` - + On Python 3.x, provides the conversion to `bytes`. On Python 2.7, `__bytes__` is allowed but has no effect. - + * `fn __unicode__(&self) -> PyResult` - + On Python 2.7, provides the conversion to `unicode`. On Python 3.x, `__unicode__` is allowed but has no effect. - + * `fn __format__(&self, format_spec: &str) -> PyResult>` - + Special method that is used by the `format()` builtin and the `str.format()` method. Possible return types are `PyResult` or `PyResult`. #### Comparison operators * `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult` - + Overloads Python comparison operations (`==`, `!=`, `<`, `<=`, `>`, and `>=`). The `op` argument indicates the comparison operation being performed. The return type will normally be `PyResult`, but any Python object can be returned. If `other` is not of the type specified in the signature, the generated code will automatically `return NotImplemented`. - + * `fn __hash__(&self) -> PyResult` - + Objects that compare equal must have the same hash value. The return type must be `PyResult` where `T` is one of Rust's primitive integer types. #### Other methods * `fn __bool__(&self) -> PyResult` - + Determines the "truthyness" of the object. This method works for both python 3 and python 2, even on Python 2.7 where the Python spelling was `__nonzero__`. @@ -400,7 +401,7 @@ use pyo3::{py, PyObject, PyGCProtocol, PyVisit, PyTraverseError}; struct ClassWithGCSupport { obj: Option, } - + #[py::proto] impl PyGCProtocol for ClassWithGCSupport { fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { @@ -409,7 +410,7 @@ impl PyGCProtocol for ClassWithGCSupport { } Ok(()) } - + fn __clear__(&mut self) { if let Some(obj) = self.obj.take() { // Release reference, this decrements ref counter. @@ -421,18 +422,18 @@ impl PyGCProtocol for ClassWithGCSupport { Special protocol trait implementation has to be annotated with `#[py::proto]` attribute. -It is also possible to enable gc for custom class using `gc` parameter for `py::class` annotation. +It is also possible to enable gc for custom class using `gc` parameter for `py::class` annotation. i.e. `#[py::class(gc)]`. In that case instances of custom class participate in python garbage collector, and it is possible to track them with `gc` module methods. ### Iterator Types -Iterators can be defined using the -[`PyIterProtocol`](https://pyo3.github.io/pyo3/pyo3/class/iter/trait.PyIterProtocol.html) trait. +Iterators can be defined using the +[`PyIterProtocol`](https://pyo3.github.io/pyo3/pyo3/class/iter/trait.PyIterProtocol.html) trait. It includes two methods `__iter__` and `__next__`: * `fn __iter__(&mut self) -> PyResult` * `fn __next__(&mut self) -> PyResult>` - + Returning `Ok(None)` from `__next__` indicates that that there are no further items. Example: diff --git a/pyo3cls/src/py_class.rs b/pyo3cls/src/py_class.rs index d40ce599..5ded6e13 100644 --- a/pyo3cls/src/py_class.rs +++ b/pyo3cls/src/py_class.rs @@ -440,6 +440,10 @@ fn parse_attribute(attr: String) -> (HashMap<&'static str, syn::Ident>, flags.push(syn::Ident::from("_pyo3::typeob::PY_TYPE_FLAG_WEAKREF")); continue } + "subclass" => { + flags.push(syn::Ident::from("_pyo3::typeob::PY_TYPE_FLAG_BASETYPE")); + continue + } _ => { println!("Unsupported parameter: {:?}", key); } diff --git a/src/typeob.rs b/src/typeob.rs index e61422e1..9c01d65e 100644 --- a/src/typeob.rs +++ b/src/typeob.rs @@ -59,6 +59,9 @@ pub const PY_TYPE_FLAG_GC: usize = 1<<0; /// Type object supports python weak references pub const PY_TYPE_FLAG_WEAKREF: usize = 1<<1; +/// Type object can be used as the base type of another type +pub const PY_TYPE_FLAG_BASETYPE: usize = 1<<2; + impl<'a, T: ?Sized> PyTypeInfo for &'a T where T: PyTypeInfo { type Type = T::Type; @@ -307,6 +310,9 @@ fn py_class_flags(type_object: &mut ffi::PyTypeObject) { } else { type_object.tp_flags = ffi::Py_TPFLAGS_DEFAULT; } + if T::FLAGS & PY_TYPE_FLAG_BASETYPE != 0 { + type_object.tp_flags |= ffi::Py_TPFLAGS_BASETYPE; + } } #[cfg(not(Py_3))] @@ -321,6 +327,9 @@ fn py_class_flags(type_object: &mut ffi::PyTypeObject) { if !type_object.tp_as_buffer.is_null() { type_object.tp_flags = type_object.tp_flags | ffi::Py_TPFLAGS_HAVE_NEWBUFFER; } + if T::FLAGS & PY_TYPE_FLAG_BASETYPE != 0 { + type_object.tp_flags |= ffi::Py_TPFLAGS_BASETYPE; + } } fn py_class_method_defs() -> PyResult<(Option, diff --git a/tests/test_class.rs b/tests/test_class.rs index 99620726..f6cb29f3 100644 --- a/tests/test_class.rs +++ b/tests/test_class.rs @@ -1239,3 +1239,18 @@ fn meth_args() { py_run!(py, inst, "assert inst.get_kwargs(1,2,3,t=1,n=2) == [(1,2,3), {'t': 1, 'n': 2}]"); // py_expect_exception!(py, inst, "inst.get_kwarg(100)", TypeError); } + +#[py::class(subclass)] +struct SubclassAble {} + +#[test] +fn subclass() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = PyDict::new(py); + d.set_item("SubclassAble", py.get_type::()).unwrap(); + py.run("class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", None, Some(d)) + .map_err(|e| e.print(py)) + .unwrap(); +}