diff options
-rw-r--r-- | sources/pyside2/libpyside/pysideproperty.cpp | 225 | ||||
-rw-r--r-- | sources/pyside2/libpyside/pysideproperty_p.h | 1 | ||||
-rw-r--r-- | sources/pyside2/tests/QtCore/qproperty_decorator.py | 4 | ||||
-rw-r--r-- | sources/pyside2/tests/pysidetest/CMakeLists.txt | 1 | ||||
-rw-r--r-- | sources/pyside2/tests/pysidetest/property_python_test.py | 232 |
5 files changed, 410 insertions, 53 deletions
diff --git a/sources/pyside2/libpyside/pysideproperty.cpp b/sources/pyside2/libpyside/pysideproperty.cpp index 8aaa81205..443098f2e 100644 --- a/sources/pyside2/libpyside/pysideproperty.cpp +++ b/sources/pyside2/libpyside/pysideproperty.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2020 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt for Python. @@ -47,6 +47,8 @@ #include <shiboken.h> #include <signature.h> +using namespace Shiboken; + extern "C" { @@ -55,25 +57,41 @@ static int qpropertyTpInit(PyObject *, PyObject *, PyObject *); static void qpropertyDeAlloc(PyObject *self); //methods -static PyObject *qPropertyCall(PyObject *, PyObject *, PyObject *); -static PyObject *qPropertySetter(PyObject *, PyObject *); static PyObject *qPropertyGetter(PyObject *, PyObject *); +static PyObject *qPropertySetter(PyObject *, PyObject *); +static PyObject *qPropertyResetter(PyObject *, PyObject *); +static PyObject *qPropertyDeleter(PyObject *, PyObject *); +static PyObject *qPropertyCall(PyObject *, PyObject *, PyObject *); static int qpropertyTraverse(PyObject *self, visitproc visit, void *arg); static int qpropertyClear(PyObject *self); // Attributes static PyObject *qPropertyDocGet(PyObject *, void *); +static int qPropertyDocSet(PyObject *, PyObject *, void *); +static PyObject *qProperty_fget(PyObject *, void *); +static PyObject *qProperty_fset(PyObject *, void *); +static PyObject *qProperty_freset(PyObject *, void *); +static PyObject *qProperty_fdel(PyObject *, void *); static PyMethodDef PySidePropertyMethods[] = { - {"setter", (PyCFunction)qPropertySetter, METH_O, 0}, - {"write", (PyCFunction)qPropertySetter, METH_O, 0}, {"getter", (PyCFunction)qPropertyGetter, METH_O, 0}, + {"setter", (PyCFunction)qPropertySetter, METH_O, 0}, + {"resetter", (PyCFunction)qPropertyResetter, METH_O, 0}, + {"deleter", (PyCFunction)qPropertyDeleter, METH_O, 0}, + // Synonyms from Qt {"read", (PyCFunction)qPropertyGetter, METH_O, 0}, + {"write", (PyCFunction)qPropertySetter, METH_O, 0}, {0, 0, 0, 0} }; static PyGetSetDef PySidePropertyType_getset[] = { - {const_cast<char *>("__doc__"), qPropertyDocGet, nullptr, nullptr, nullptr}, + // Note: we could not use `PyMemberDef` like Python's properties, + // because of the indirection of PySidePropertyPrivate. + {const_cast<char *>("fget"), qProperty_fget, nullptr, nullptr, nullptr}, + {const_cast<char *>("fset"), qProperty_fset, nullptr, nullptr, nullptr}, + {const_cast<char *>("freset"), qProperty_freset, nullptr, nullptr, nullptr}, + {const_cast<char *>("fdel"), qProperty_fdel, nullptr, nullptr, nullptr}, + {const_cast<char *>("__doc__"), qPropertyDocGet, qPropertyDocSet, nullptr, nullptr}, {nullptr, nullptr, nullptr, nullptr, nullptr} }; @@ -175,21 +193,26 @@ static int qpropertyTpInit(PyObject *self, PyObject *args, PyObject *kwds) pData->metaCallHandler = &qpropertyMetaCall; static const char *kwlist[] = {"type", "fget", "fset", "freset", "fdel", "doc", "notify", - "designable", "scriptable", "stored", "user", - "constant", "final", 0}; + "designable", "scriptable", "stored", + "user", "constant", "final", 0}; char *doc{}; if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O|OOOOsObbbbbb:QtCore.QProperty", + "O|OOOOsObbbbbb:QtCore.Property", const_cast<char **>(kwlist), /*OO*/ &type, &(pData->fget), /*OOO*/ &(pData->fset), &(pData->freset), &(pData->fdel), /*s*/ &doc, /*O*/ &(pData->notify), - /*bbbbbb*/ &(pData->designable), &(pData->scriptable), &(pData->stored), &(pData->user), &(pData->constant), &(pData->final))) { + /*bbb*/ &(pData->designable), &(pData->scriptable), &(pData->stored), + /*bbb*/ &(pData->user), &(pData->constant), &(pData->final))) { return -1; } + // PYSIDE-1019: Fetching the default `__doc__` from fget would fail for inherited functions + // because we don't initialize the mro with signatures (and we will not!). + // But it is efficient and in-time to do that on demand in qPropertyDocGet. + pData->getter_doc = false; if (doc) pData->doc = doc; else @@ -229,53 +252,107 @@ static void qpropertyDeAlloc(PyObject *self) Py_TYPE(self)->tp_free(self); } -static PyObject *qPropertyCall(PyObject *self, PyObject *args, PyObject * /* kw */) +static PyObject * +_property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *reset, PyObject *del) { - PyObject *callback = PyTuple_GetItem(args, 0); - if (PyFunction_Check(callback)) { - auto prop = reinterpret_cast<PySideProperty *>(self); - PySidePropertyPrivate *pData = prop->d; + PySideProperty *pold = reinterpret_cast<PySideProperty *>(old); + PySidePropertyPrivate *pData = pold->d; - Py_INCREF(callback); - pData->fget = callback; + AutoDecRef type(PyObject_Type(old)); + QByteArray doc{}; + if (type.isNull()) + return nullptr; - Py_INCREF(self); - return self; + if (get == nullptr || get == Py_None) { + Py_XDECREF(get); + get = pData->fget ? pData->fget : Py_None; + } + if (set == nullptr || set == Py_None) { + Py_XDECREF(set); + set = pData->fset ? pData->fset : Py_None; + } + if (reset == nullptr || reset == Py_None) { + Py_XDECREF(reset); + reset = pData->freset ? pData->freset : Py_None; + } + if (del == nullptr || del == Py_None) { + Py_XDECREF(del); + del = pData->fdel ? pData->fdel : Py_None; + } + if (pData->getter_doc && get != Py_None) { + /* make _init use __doc__ from getter */ + doc = ""; + } + else { + doc = !pData->doc.isEmpty() ? pData->doc : ""; } - PyErr_SetString(PyExc_TypeError, "Invalid property usage."); - return nullptr; + auto notify = pData->notify ? pData->notify : Py_None; + + PyObject *typeName = String::fromCString(pData->typeName); + PyObject *obNew = PyObject_CallFunction(type, const_cast<char *>("OOOOOsO" "bbb" "bbb"), + typeName, get, set, reset, del, doc.data(), notify, + pData->designable, pData->scriptable, pData->stored, + pData->user, pData->constant, pData->final); + + return obNew; } -static PyObject *qPropertySetter(PyObject *self, PyObject *callback) +static PyObject *qPropertyGetter(PyObject *self, PyObject *getter) { - if (PyFunction_Check(callback)) { - PySideProperty *prop = reinterpret_cast<PySideProperty *>(self); - PySidePropertyPrivate *pData = prop->d; + return _property_copy(self, getter, nullptr, nullptr, nullptr); +} - Py_INCREF(callback); - pData->fset = callback; +static PyObject *qPropertySetter(PyObject *self, PyObject *setter) +{ + return _property_copy(self, nullptr, setter, nullptr, nullptr); +} - Py_INCREF(callback); - return callback; - } - PyErr_SetString(PyExc_TypeError, "Invalid property setter agument."); - return nullptr; +static PyObject *qPropertyResetter(PyObject *self, PyObject *resetter) +{ + return _property_copy(self, nullptr, nullptr, resetter, nullptr); } -static PyObject *qPropertyGetter(PyObject *self, PyObject *callback) +static PyObject *qPropertyDeleter(PyObject *self, PyObject *deleter) { - if (PyFunction_Check(callback)) { - PySideProperty *prop = reinterpret_cast<PySideProperty *>(self); - PySidePropertyPrivate *pData = prop->d; + return _property_copy(self, nullptr, nullptr, nullptr, deleter); +} - Py_INCREF(callback); - pData->fget = callback; +static PyObject *qPropertyCall(PyObject *self, PyObject *args, PyObject * /* kw */) +{ + PyObject *getter = PyTuple_GetItem(args, 0); + return _property_copy(self, getter, nullptr, nullptr, nullptr); +} - Py_INCREF(callback); - return callback; - } - PyErr_SetString(PyExc_TypeError, "Invalid property getter agument."); - return nullptr; +// PYSIDE-1019: Provide the same getters as Pythons `PyProperty`. +static PyObject *_property_func(PyObject *self, ssize_t offset) +{ + auto data = reinterpret_cast<PySideProperty *>(self); + PySidePropertyPrivate *pData = data->d; + auto funcptr = reinterpret_cast<char *>(pData) + offset; + auto func = *reinterpret_cast<PyObject **>(funcptr); + auto ret = func != nullptr ? func : Py_None; + Py_INCREF(ret); + return ret; +} + +static PyObject *qProperty_fget(PyObject *self, void *) +{ + return _property_func(self, offsetof(PySidePropertyPrivate, fget)); +} + +static PyObject *qProperty_fset(PyObject *self, void *) +{ + return _property_func(self, offsetof(PySidePropertyPrivate, fset)); +} + +static PyObject *qProperty_freset(PyObject *self, void *) +{ + return _property_func(self, offsetof(PySidePropertyPrivate, freset)); +} + +static PyObject *qProperty_fdel(PyObject *self, void *) +{ + return _property_func(self, offsetof(PySidePropertyPrivate, fdel)); } static PyObject *qPropertyDocGet(PyObject *self, void *) @@ -291,9 +368,40 @@ static PyObject *qPropertyDocGet(PyObject *self, void *) return PyString_FromString(doc); #endif } + if (pData->fget != nullptr) { + // PYSIDE-1019: Fetch the default `__doc__` from fget. We do it late. + AutoDecRef get_doc(PyObject_GetAttr(pData->fget, PyMagicName::doc())); + if (!get_doc.isNull()) { + pData->doc = String::toCString(get_doc); + pData->getter_doc = true; + if (Py_TYPE(self) == PySidePropertyTypeF()) + return qPropertyDocGet(self, nullptr); + /* + * If this is a property subclass, put __doc__ in dict of the + * subclass instance instead, otherwise it gets shadowed by + * __doc__ in the class's dict. + */ + auto get_doc_obj = get_doc.object(); + int err = PyObject_SetAttr(self, PyMagicName::doc(), get_doc); + return err < 0 ? nullptr : (Py_INCREF(get_doc_obj), get_doc_obj); + } + PyErr_Clear(); + } Py_RETURN_NONE; } +static int qPropertyDocSet(PyObject *self, PyObject *value, void *) +{ + auto data = reinterpret_cast<PySideProperty *>(self); + PySidePropertyPrivate *pData = data->d; + + if (String::check(value)) { + pData->doc = String::toCString(value); + return 0; + } + PyErr_SetString(PyExc_TypeError, "String argument expected."); + return -1; +} static int qpropertyTraverse(PyObject *self, visitproc visit, void *arg) { @@ -354,14 +462,20 @@ static PyObject *getFromType(PyTypeObject *type, PyObject *name) namespace PySide { namespace Property { static const char *Property_SignatureStrings[] = { - "PySide2.QtCore.Property(type:type,fget:typing.Callable=None,fset:typing.Callable=None," + "PySide2.QtCore.Property(self,type:type,fget:typing.Callable=None,fset:typing.Callable=None," "freset:typing.Callable=None,fdel:typing.Callable=None,doc:str=None," "notify:typing.Callable=None,designable:bool=True,scriptable:bool=True," - "stored:bool=True,user:bool=False,constant:bool=False,final:bool=False)", - "PySide2.QtCore.Property.getter(func:typing.Callable)", - "PySide2.QtCore.Property.read(func:typing.Callable)", - "PySide2.QtCore.Property.setter(func:typing.Callable)", - "PySide2.QtCore.Property.write(func:typing.Callable)", + "stored:bool=True,user:bool=False,constant:bool=False,final:bool=False)" + "->PySide2.QtCore.Property", + "PySide2.QtCore.Property.deleter(self,func:typing.Callable)", + "PySide2.QtCore.Property.fdel(self)->typing.Callable", + "PySide2.QtCore.Property.fget(self)->typing.Callable", + "PySide2.QtCore.Property.freset(self)->typing.Callable", + "PySide2.QtCore.Property.fset(self)->typing.Callable", + "PySide2.QtCore.Property.getter(self,func:typing.Callable)", + "PySide2.QtCore.Property.read(self,func:typing.Callable)", + "PySide2.QtCore.Property.setter(self,func:typing.Callable)", + "PySide2.QtCore.Property.write(self,func:typing.Callable)", nullptr}; // Sentinel void init(PyObject *module) @@ -384,7 +498,7 @@ bool checkType(PyObject *pyObj) int setValue(PySideProperty *self, PyObject *source, PyObject *value) { PyObject *fset = self->d->fset; - if (fset) { + if (fset && value) { Shiboken::AutoDecRef args(PyTuple_New(2)); PyTuple_SET_ITEM(args, 0, source); PyTuple_SET_ITEM(args, 1, value); @@ -392,9 +506,16 @@ int setValue(PySideProperty *self, PyObject *source, PyObject *value) Py_INCREF(value); Shiboken::AutoDecRef result(PyObject_CallObject(fset, args)); return (result.isNull() ? -1 : 0); - } else { - PyErr_SetString(PyExc_AttributeError, "Attibute read only"); } + PyObject *fdel = self->d->fdel; + if (fdel) { + Shiboken::AutoDecRef args(PyTuple_New(1)); + PyTuple_SET_ITEM(args, 0, source); + Py_INCREF(source); + Shiboken::AutoDecRef result(PyObject_CallObject(fdel, args)); + return (result.isNull() ? -1 : 0); + } + PyErr_SetString(PyExc_AttributeError, "Attibute read only"); return -1; } diff --git a/sources/pyside2/libpyside/pysideproperty_p.h b/sources/pyside2/libpyside/pysideproperty_p.h index 4db638021..e7b6e4d77 100644 --- a/sources/pyside2/libpyside/pysideproperty_p.h +++ b/sources/pyside2/libpyside/pysideproperty_p.h @@ -56,6 +56,7 @@ struct PySidePropertyPrivate PyObject *freset = nullptr; PyObject *fdel = nullptr; PyObject *notify = nullptr; + bool getter_doc = false; QByteArray notifySignature; QByteArray doc; bool designable = true; diff --git a/sources/pyside2/tests/QtCore/qproperty_decorator.py b/sources/pyside2/tests/QtCore/qproperty_decorator.py index aa31e59c4..c845ac6d3 100644 --- a/sources/pyside2/tests/QtCore/qproperty_decorator.py +++ b/sources/pyside2/tests/QtCore/qproperty_decorator.py @@ -47,7 +47,9 @@ class MyObject(QObject): return self._value @value.setter - def valueSet(self, value): + # Note: The name of property and setter must be the same, because the + # object changes its identity all the time. `valueSet` no longer works. + def value(self, value): self._value = value diff --git a/sources/pyside2/tests/pysidetest/CMakeLists.txt b/sources/pyside2/tests/pysidetest/CMakeLists.txt index 577b51815..0dbd2b7cf 100644 --- a/sources/pyside2/tests/pysidetest/CMakeLists.txt +++ b/sources/pyside2/tests/pysidetest/CMakeLists.txt @@ -161,6 +161,7 @@ PYSIDE_TEST(modelview_test.py) PYSIDE_TEST(new_inherited_functions_test.py) PYSIDE_TEST(notify_id.py) PYSIDE_TEST(properties_test.py) +PYSIDE_TEST(property_python_test.py) PYSIDE_TEST(qapp_like_a_macro_test.py) PYSIDE_TEST(qvariant_test.py) PYSIDE_TEST(repr_test.py) diff --git a/sources/pyside2/tests/pysidetest/property_python_test.py b/sources/pyside2/tests/pysidetest/property_python_test.py new file mode 100644 index 000000000..ec2940b14 --- /dev/null +++ b/sources/pyside2/tests/pysidetest/property_python_test.py @@ -0,0 +1,232 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the test suite of Qt for Python. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +""" +Test for PySide's Property +========================== + +This test is copied from Python's `test_property.py` and adapted to +the PySide Property implementation. + +This test is to ensure maximum compatibility. +""" + +# Test case for property +# more tests are in test_descr + +import os +import sys +import unittest + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide2.QtCore import Property, QObject +#from PyQt5.QtCore import pyqtProperty as Property, QObject + +# This are the original imports. +import sys +import unittest +has_test = False +try: + if sys.version_info[0] >= 3: # This test has no support in Python 2 + from test import support + has_test = True +except: + pass + +class PropertyBase(Exception): + pass + +class PropertyGet(PropertyBase): + pass + +class PropertySet(PropertyBase): + pass + +class PropertyDel(PropertyBase): + pass + +class BaseClass(QObject): + def __init__(self): + QObject.__init__(self) + + self._spam = 5 + + @Property(object) + def spam(self): + """BaseClass.getter""" + return self._spam + + @spam.setter + def spam(self, value): + self._spam = value + + @spam.deleter + def spam(self): + del self._spam + +class SubClass(BaseClass): + + @BaseClass.spam.getter + def spam(self): + """SubClass.getter""" + raise PropertyGet(self._spam) + + @spam.setter + def spam(self, value): + raise PropertySet(self._spam) + + @spam.deleter + def spam(self): + raise PropertyDel(self._spam) + +class PropertyDocBase(object): + _spam = 1 + def _get_spam(self): + return self._spam + spam = Property(object, _get_spam, doc="spam spam spam") + +class PropertyDocSub(PropertyDocBase): + @PropertyDocBase.spam.getter + def spam(self): + """The decorator does not use this doc string""" + return self._spam + +class PropertySubNewGetter(BaseClass): + @BaseClass.spam.getter + def spam(self): + """new docstring""" + return 5 + +class PropertyNewGetter(QObject): + def __init__(self): + QObject.__init__(self) + + @Property(object) + def spam(self): + """original docstring""" + return 1 + @spam.getter + def spam(self): + """new docstring""" + return 8 + +class PropertyTests(unittest.TestCase): + def test_property_decorator_baseclass(self): + # see #1620 + base = BaseClass() + self.assertEqual(base.spam, 5) + self.assertEqual(base._spam, 5) + base.spam = 10 + self.assertEqual(base.spam, 10) + self.assertEqual(base._spam, 10) + delattr(base, "spam") + self.assertTrue(not hasattr(base, "spam")) + self.assertTrue(not hasattr(base, "_spam")) + base.spam = 20 + self.assertEqual(base.spam, 20) + self.assertEqual(base._spam, 20) + + def test_property_decorator_subclass(self): + # see #1620 + sub = SubClass() + self.assertRaises(PropertyGet, getattr, sub, "spam") + self.assertRaises(PropertySet, setattr, sub, "spam", None) + self.assertRaises(PropertyDel, delattr, sub, "spam") + + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_property_decorator_subclass_doc(self): + sub = SubClass() + self.assertEqual(sub.__class__.spam.__doc__, "SubClass.getter") + + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_property_decorator_baseclass_doc(self): + base = BaseClass() + self.assertEqual(base.__class__.spam.__doc__, "BaseClass.getter") + + def test_property_decorator_doc(self): + base = PropertyDocBase() + sub = PropertyDocSub() + self.assertEqual(base.__class__.spam.__doc__, "spam spam spam") + self.assertEqual(sub.__class__.spam.__doc__, "spam spam spam") + + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_property_getter_doc_override(self): + newgettersub = PropertySubNewGetter() + self.assertEqual(newgettersub.spam, 5) + self.assertEqual(newgettersub.__class__.spam.__doc__, "new docstring") + newgetter = PropertyNewGetter() + self.assertEqual(newgetter.spam, 8) + self.assertEqual(newgetter.__class__.spam.__doc__, "new docstring") + + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_property_builtin_doc_writable(self): + p = Property(object, doc='basic') + self.assertEqual(p.__doc__, 'basic') + p.__doc__ = 'extended' + self.assertEqual(p.__doc__, 'extended') + + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_property_decorator_doc_writable(self): + class PropertyWritableDoc(object): + + @Property(object) + def spam(self): + """Eggs""" + return "eggs" + + sub = PropertyWritableDoc() + self.assertEqual(sub.__class__.spam.__doc__, 'Eggs') + sub.__class__.spam.__doc__ = 'Spam' + self.assertEqual(sub.__class__.spam.__doc__, 'Spam') + + if has_test: # This test has no support in Python 2 + @support.refcount_test + def test_refleaks_in___init__(self): + gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount') + fake_prop = Property(object, 'fget', 'fset', "freset", 'fdel', 'doc') + refs_before = gettotalrefcount() + for i in range(100): + fake_prop.__init__(object, 'fget', 'fset', "freset", 'fdel', 'doc') + self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10) + + +# Note: We ignore the whole subclass tests concerning __doc__ strings. +# See the original Python test starting with: +# "Issue 5890: subclasses of property do not preserve method __doc__ strings" + + +if __name__ == '__main__': + unittest.main() |