From 3d4d91334dc608a41963d9acb5080139202f33c5 Mon Sep 17 00:00:00 2001 From: Christian Tismer Date: Sat, 13 Jun 2020 00:41:37 +0200 Subject: signature: Clean up and improve readability There were some heavy changes to the signature module when the switchable feature framework was developed. The principle underneath that framework took a number of iterations with many changed ideas. Most of these changes were reverted, but a few improvements should stay, although they have nothing to do with features any longer. Change-Id: I0804082510c3805ba6015925d23afb3ef5d149a4 Reviewed-by: Cristian Maureira-Fredes --- sources/shiboken2/libshiboken/signature.cpp | 58 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/sources/shiboken2/libshiboken/signature.cpp b/sources/shiboken2/libshiboken/signature.cpp index ba1fc119e..b09b68d47 100644 --- a/sources/shiboken2/libshiboken/signature.cpp +++ b/sources/shiboken2/libshiboken/signature.cpp @@ -74,7 +74,8 @@ typedef struct safe_globals_struc { PyObject *helper_module; PyObject *arg_dict; PyObject *map_dict; - PyObject *value_dict; // for writing signatures + PyObject *value_dict; // for writing signatures + PyObject *feature_dict; // registry for PySide.__feature__ // init part 2: run module PyObject *pyside_type_init_func; PyObject *create_signature_func; @@ -572,6 +573,12 @@ init_phase_1(void) if (p->value_dict == nullptr) goto error; + // PYSIDE-1019: build a __feature__ dict + p->feature_dict = PyDict_New(); + if (p->feature_dict == nullptr + || PyObject_SetAttrString(p->helper_module, "pyside_feature_dict", p->feature_dict) < 0) + goto error; + // This function will be disabled until phase 2 is done. p->finish_import_func = nullptr; @@ -646,17 +653,22 @@ _fixup_getset(PyTypeObject *type, const char *name, PyGetSetDef *new_gsp) } } } - // staticmethod has just a __doc__ in the class - assert(strcmp(type->tp_name, "staticmethod") == 0); + PyMemberDef *md = type->tp_members; + if (md != nullptr) + for (; md->name != nullptr; md++) + if (strcmp(md->name, name) == 0) + return 1; + // staticmethod has just a `__doc__` in the class + assert(strcmp(type->tp_name, "staticmethod") == 0 && strcmp(name, "__doc__") == 0); return 0; } static int -add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **old_descr) +add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **doc_descr) { /* - * This function is used to assign a new __signature__ attribute, - * and also to override a __doc__ attribute. + * This function is used to assign a new `__signature__` attribute, + * and also to override a `__doc__` or `__name__` attribute. */ assert(PyType_Check(type)); PyType_Ready(type); @@ -664,9 +676,11 @@ add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **old_descr) for (; gsp->name != nullptr; gsp++) { PyObject *have_descr = PyDict_GetItemString(dict, gsp->name); if (have_descr != nullptr) { - assert(strcmp(gsp->name, "__doc__") == 0); Py_INCREF(have_descr); - *old_descr = have_descr; + if (strcmp(gsp->name, "__doc__") == 0) + *doc_descr = have_descr; + else + assert(false); if (!_fixup_getset(type, gsp->name, gsp)) continue; } @@ -817,7 +831,7 @@ static PyGetSetDef new_PyWrapperDescr_getsets[] = { // // Additionally to the interface via __signature__, we also provide // a general function, which allows for different signature layouts. -// The "modifier" argument is a string that is passed in from loader.py . +// The "modifier" argument is a string that is passed in from 'loader.py'. // Configuration what the modifiers mean is completely in Python. // @@ -902,13 +916,25 @@ PySide_PatchTypes(void) reinterpret_cast(&PyString_Type), "split")); Shiboken::AutoDecRef wrap_descr(PyObject_GetAttrString( reinterpret_cast(Py_TYPE(Py_True)), "__add__")); + // abbreviations for readability + auto md_gs = new_PyMethodDescr_getsets; + auto md_doc = &old_md_doc_descr; + auto cf_gs = new_PyCFunction_getsets; + auto cf_doc = &old_cf_doc_descr; + auto sm_gs = new_PyStaticMethod_getsets; + auto sm_doc = &old_sm_doc_descr; + auto tp_gs = new_PyType_getsets; + auto tp_doc = &old_tp_doc_descr; + auto wd_gs = new_PyWrapperDescr_getsets; + auto wd_doc = &old_wd_doc_descr; + if (meth_descr.isNull() || wrap_descr.isNull() || PyType_Ready(Py_TYPE(meth_descr)) < 0 - || add_more_getsets(PepMethodDescr_TypePtr, new_PyMethodDescr_getsets, &old_md_doc_descr) < 0 - || add_more_getsets(&PyCFunction_Type, new_PyCFunction_getsets, &old_cf_doc_descr) < 0 - || add_more_getsets(PepStaticMethod_TypePtr, new_PyStaticMethod_getsets, &old_sm_doc_descr) < 0 - || add_more_getsets(&PyType_Type, new_PyType_getsets, &old_tp_doc_descr) < 0 - || add_more_getsets(Py_TYPE(wrap_descr), new_PyWrapperDescr_getsets, &old_wd_doc_descr) < 0 + || add_more_getsets(PepMethodDescr_TypePtr, md_gs, md_doc) < 0 + || add_more_getsets(&PyCFunction_Type, cf_gs, cf_doc) < 0 + || add_more_getsets(PepStaticMethod_TypePtr, sm_gs, sm_doc) < 0 + || add_more_getsets(&PyType_Type, tp_gs, tp_doc) < 0 + || add_more_getsets(Py_TYPE(wrap_descr), wd_gs, wd_doc) < 0 ) return -1; #ifndef _WIN32 @@ -1207,8 +1233,8 @@ FinishSignatureInitialization(PyObject *module, const char *signatures[]) * Still, it is not possible to call init phase 2 from here, * because the import is still running. Do it from Python! */ - PySide_PatchTypes(); - if (PySide_FinishSignatures(module, signatures) < 0) { + if ( PySide_PatchTypes() < 0 + || PySide_FinishSignatures(module, signatures) < 0) { PyErr_Print(); PyErr_SetNone(PyExc_ImportError); } -- cgit v1.2.3 From 9a8beeeccf8c097cc4e6a216813353e67ac95ecc Mon Sep 17 00:00:00 2001 From: Christian Tismer Date: Sat, 13 Jun 2020 00:41:37 +0200 Subject: feature-select: Implement a selectable feature framework This is the framework for selectable features. There are no real features implemented. Planned is a maximum of 8 features. They are all implemented as a dummy for now. The decision depends of the following setting at the beginning of a module after PySide2 import: from __feature__ import For more info, see the Jira issue, section The Principle Of Selectable Features In PySide Task-number: PYSIDE-1019 Change-Id: If355e9294b5c16090b39d30422a90ea9c8523390 Reviewed-by: Christian Tismer --- sources/pyside2/libpyside/CMakeLists.txt | 2 + sources/pyside2/libpyside/feature_select.cpp | 395 +++++++++++++++++++++ sources/pyside2/libpyside/feature_select.h | 53 +++ sources/pyside2/libpyside/pyside.cpp | 2 + sources/pyside2/tests/QtCore/CMakeLists.txt | 1 + sources/pyside2/tests/QtCore/feature_test.py | 105 ++++++ sources/shiboken2/libshiboken/basewrapper.cpp | 39 ++ sources/shiboken2/libshiboken/basewrapper.h | 3 + sources/shiboken2/libshiboken/pep384impl.cpp | 53 +-- sources/shiboken2/libshiboken/pep384impl.h | 7 +- sources/shiboken2/libshiboken/sbkstaticstrings.cpp | 3 + sources/shiboken2/libshiboken/sbkstaticstrings.h | 3 + sources/shiboken2/libshiboken/shiboken.h | 1 + sources/shiboken2/libshiboken/signature.cpp | 59 ++- sources/shiboken2/libshiboken/signature.h | 1 + sources/shiboken2/shibokenmodule/CMakeLists.txt | 2 + .../files.dir/shibokensupport/__feature__.py | 111 ++++++ .../files.dir/shibokensupport/signature/loader.py | 16 +- 18 files changed, 796 insertions(+), 60 deletions(-) create mode 100644 sources/pyside2/libpyside/feature_select.cpp create mode 100644 sources/pyside2/libpyside/feature_select.h create mode 100644 sources/pyside2/tests/QtCore/feature_test.py create mode 100644 sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py diff --git a/sources/pyside2/libpyside/CMakeLists.txt b/sources/pyside2/libpyside/CMakeLists.txt index 31f68749a..d6b20c45a 100644 --- a/sources/pyside2/libpyside/CMakeLists.txt +++ b/sources/pyside2/libpyside/CMakeLists.txt @@ -40,6 +40,7 @@ endif() set(libpyside_SRC dynamicqmetaobject.cpp + feature_select.cpp signalmanager.cpp globalreceiverv2.cpp pysideclassinfo.cpp @@ -125,6 +126,7 @@ endif() set(libpyside_HEADERS dynamicqmetaobject.h + feature_select.h pysideclassinfo.h pysideqenum.h pysidemacros.h diff --git a/sources/pyside2/libpyside/feature_select.cpp b/sources/pyside2/libpyside/feature_select.cpp new file mode 100644 index 000000000..b7234ad75 --- /dev/null +++ b/sources/pyside2/libpyside/feature_select.cpp @@ -0,0 +1,395 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt for Python. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** 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-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "feature_select.h" + +#include +#include + +#include + +////////////////////////////////////////////////////////////////////////////// +// +// PYSIDE-1019: Support switchable extensions +// +// This functionality is no longer implemented in the signature module, since +// the PyCFunction getsets do not have to be modified any longer. +// Instead, we simply exchange the complete class dicts. This is done in the +// basewrapper.cpp file. +// +// This is the general framework of the switchable extensions. +// A maximum of eight features is planned so far. This seems to be enough. +// More features are possible, but then we must somehow register the +// extra `select_id`s above 255. +// + +/***************************************************************************** + + How Does This Feature Selection Work? + ------------------------------------- + +The basic idea is to replace the `tp_dict` of a QObject derived type. +This way, we can replace the methods of the dict in no time. + +The crucial point to understand is how the `tp_dict` is actually accessed: +When you type "QObject.__dict__", the descriptor of SbkObjectType_Type +is called. This descriptor is per default unassigned, so the base class +PyType_Type provides the tp_getset method `type_dict`: + + static PyObject * + type_dict(PyTypeObject *type, void *context) + { + if (type->tp_dict == NULL) { + Py_RETURN_NONE; + } + return PyDictProxy_New(type->tp_dict); + } + +In order to change that, we need to insert our own version into SbkObjectType: + + static PyObject *Sbk_TypeGet___dict__(PyTypeObject *type, void *context) + { + auto dict = type->tp_dict; + if (dict == NULL) + Py_RETURN_NONE; + if (SelectFeatureSet != nullptr) + dict = SelectFeatureSet(type); + return PyDictProxy_New(dict); + } + +This way, the Python function `type_ready()` does not fill in the default, +but uses our modified version. It a similar way, we overwrite type_getattro +with our own version, again in SbkObjectType, replacing the default of +PyType_Type. + +Now we can exchange the dict with a customized version. +We have our own derived type `ChameleonDict` with additional attributes. +These allow us to create a ring of dicts which can be rotated to the actual +needed dict version: + +Every dict has a field `select_id` which is selected by the `from __feature__` +import. The dicts are cyclic connected by the `dict_ring` field. + +When a class dict is required, now always `SelectFeatureSet` is called, which +looks into the `__name__` attribute of the active module and decides which +version of `tp_dict` is needed. Then the right dict is searched in the ring +and created if not already there. + +This is everything that the following code does. + +*****************************************************************************/ + + +namespace PySide { namespace FeatureSelector { + +using namespace Shiboken; + +static PyObject *getFeatureSelectID() +{ + static PyObject *zero = PyInt_FromLong(0); + static PyObject *feature_dict = GetFeatureDict(); + // these things are all borrowed + PyObject *globals = PyEval_GetGlobals(); + if (globals == nullptr) + return zero; + PyObject *modname = PyDict_GetItem(globals, PyMagicName::name()); + if (modname == nullptr) + return zero; + PyObject *flag = PyDict_GetItem(feature_dict, modname); + if (flag == nullptr || !PyInt_Check(flag)) // int/long cheating + return zero; + return flag; +} + +// Create a derived dict class +static PyTypeObject * +createDerivedDictType() +{ + // It is not easy to create a compatible dict object with the + // limited API. Easier is to use Python to create a derived + // type and to modify that a bit from the C code. + PyObject *ChameleonDict = PepRun_GetResult(R"CPP(if True: + + class ChameleonDict(dict): + __slots__ = ("dict_ring", "select_id") + + result = ChameleonDict + + )CPP"); + return reinterpret_cast(ChameleonDict); +} + +static PyTypeObject *old_dict_type = Py_TYPE(PyType_Type.tp_dict); +static PyTypeObject *new_dict_type = nullptr; + +static void ensureNewDictType() +{ + if (new_dict_type == nullptr) { + new_dict_type = createDerivedDictType(); + if (new_dict_type == nullptr) + Py_FatalError("PySide2: Problem creating ChameleonDict"); + } +} + +static inline PyObject *nextInCircle(PyObject *dict) +{ + // returns a borrowed ref + assert(Py_TYPE(dict) != old_dict_type); + AutoDecRef next_dict(PyObject_GetAttr(dict, PyName::dict_ring())); + return next_dict; +} + +static inline void setNextDict(PyObject *dict, PyObject *next_dict) +{ + assert(Py_TYPE(dict) != old_dict_type); + PyObject_SetAttr(dict, PyName::dict_ring(), next_dict); +} + +static inline void setSelectId(PyObject *dict, PyObject *select_id) +{ + assert(Py_TYPE(dict) != old_dict_type); + PyObject_SetAttr(dict, PyName::select_id(), select_id); +} + +static inline PyObject *getSelectId(PyObject *dict) +{ + assert(Py_TYPE(dict) != old_dict_type); + auto select_id = PyObject_GetAttr(dict, PyName::select_id()); + return select_id; +} + +static bool replaceClassDict(PyTypeObject *type) +{ + /* + * Replace the type dict by the derived ChameleonDict. + * This is mandatory for all type dicts when they are touched. + */ + ensureNewDictType(); + PyObject *dict = type->tp_dict; + auto ob_ndt = reinterpret_cast(new_dict_type); + PyObject *new_dict = PyObject_CallObject(ob_ndt, nullptr); + if (new_dict == nullptr || PyDict_Update(new_dict, dict) < 0) + return false; + // Insert the default id. Cannot fail for small numbers. + AutoDecRef select_id(PyInt_FromLong(0)); + setSelectId(new_dict, select_id); + // insert the dict into itself as ring + setNextDict(new_dict, new_dict); + // We have now an exact copy of the dict with a new type. + // Replace `__dict__` which usually has refcount 1 (but see cyclic_test.py) + Py_DECREF(type->tp_dict); + type->tp_dict = new_dict; + return true; +} + +static bool addNewDict(PyTypeObject *type, PyObject *select_id) +{ + /* + * Add a new dict to the ring and set it as `type->tp_dict`. + * A 'false' return is fatal. + */ + auto dict = type->tp_dict; + auto ob_ndt = reinterpret_cast(new_dict_type); + auto new_dict = PyObject_CallObject(ob_ndt, nullptr); + if (new_dict == nullptr) + return false; + setSelectId(new_dict, select_id); + // insert the dict into the ring + auto next_dict = nextInCircle(dict); + setNextDict(dict, new_dict); + setNextDict(new_dict, next_dict); + type->tp_dict = new_dict; + return true; +} + +static bool moveToFeatureSet(PyTypeObject *type, PyObject *select_id) +{ + /* + * Rotate the ring to the given `select_id` and return `true`. + * If not found, stay at the current position and return `false`. + */ + auto initial_dict = type->tp_dict; + auto dict = initial_dict; + do { + dict = nextInCircle(dict); + AutoDecRef current_id(getSelectId(dict)); + // This works because small numbers are singleton objects. + if (current_id == select_id) { + type->tp_dict = dict; + return true; + } + } while (dict != initial_dict); + type->tp_dict = initial_dict; + return false; +} + +typedef bool(*FeatureProc)(PyTypeObject *type, PyObject *prev_dict); + +static FeatureProc *featurePointer = nullptr; + +static bool createNewFeatureSet(PyTypeObject *type, PyObject *select_id) +{ + /* + * Create a new feature set. + * A `false` return value is a fatal error. + * + * A FeatureProc sees an empty `type->tp_dict` and the previous dict + * content in `prev_dict`. It is responsible of filling `type->tp_dict` + * with modified content. + */ + static auto small_1 = PyInt_FromLong(255); + Q_UNUSED(small_1); + static auto small_2 = PyInt_FromLong(255); + Q_UNUSED(small_2); + // make sure that small integers are cached + assert(small_1 != nullptr && small_1 == small_2); + + static auto zero = PyInt_FromLong(0); + bool ok = moveToFeatureSet(type, zero); + Q_UNUSED(ok); + assert(ok); + + AutoDecRef prev_dict(type->tp_dict); + Py_INCREF(prev_dict); + if (!addNewDict(type, select_id)) + return false; + int id = PyInt_AsSsize_t(select_id); + if (id == -1) + return false; + FeatureProc *proc = featurePointer; + for (int idx = id; *proc != nullptr; ++proc, idx >>= 1) { + if (idx & 1) { + // clear the tp_dict that will get new content + PyDict_Clear(type->tp_dict); + // let the proc re-fill the tp_dict + if (!(*proc)(type, prev_dict)) + return false; + // if there is still a step, prepare `prev_dict` + if (idx >> 1) { + prev_dict.reset(PyDict_Copy(type->tp_dict)); + if (prev_dict.isNull()) + return false; + } + } + } + return true; +} + +static PyObject *SelectFeatureSet(PyTypeObject *type) +{ + /* + * This is the main function of the module. + * It just makes no sense to make the function public, because + * Shiboken will assign it via a public hook of `basewrapper.cpp`. + */ + if (Py_TYPE(type->tp_dict) == old_dict_type) { + // PYSIDE-1019: On first touch, we initialize the dynamic naming. + // The dict type will be replaced after the first call. + if (!replaceClassDict(type)) + return nullptr; + } + PyObject *select_id = getFeatureSelectID(); // borrowed + AutoDecRef current_id(getSelectId(type->tp_dict)); + if (select_id != current_id) { + if (!moveToFeatureSet(type, select_id)) + if (!createNewFeatureSet(type, select_id)) { + Py_FatalError("failed to create a new feature set!"); + return nullptr; + } + } + return type->tp_dict; +} + +static bool feature_01_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_02_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_04_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_08_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_10_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_20_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_40_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +static bool feature_80_addDummyNames(PyTypeObject *type, PyObject *prev_dict); + +static FeatureProc featureProcArray[] = { + feature_01_addDummyNames, + feature_02_addDummyNames, + feature_04_addDummyNames, + feature_08_addDummyNames, + feature_10_addDummyNames, + feature_20_addDummyNames, + feature_40_addDummyNames, + feature_80_addDummyNames, + nullptr +}; + +void init() +{ + featurePointer = featureProcArray; + initSelectableFeature(SelectFeatureSet); +} + +////////////////////////////////////////////////////////////////////////////// +// +// PYSIDE-1019: Support switchable extensions +// +// Feature 0x01..0x80: A fake switchable option for testing +// + +#define SIMILAR_FEATURE(xx) \ +static bool feature_##xx##_addDummyNames(PyTypeObject *type, PyObject *prev_dict) \ +{ \ + PyObject *dict = type->tp_dict; \ + if (PyDict_Update(dict, prev_dict) < 0) \ + return false; \ + Py_INCREF(Py_None); \ + if (PyDict_SetItemString(dict, "fake_feature_" #xx, Py_None) < 0) \ + return false; \ + return true; \ +} + +SIMILAR_FEATURE(01) +SIMILAR_FEATURE(02) +SIMILAR_FEATURE(04) +SIMILAR_FEATURE(08) +SIMILAR_FEATURE(10) +SIMILAR_FEATURE(20) +SIMILAR_FEATURE(40) +SIMILAR_FEATURE(80) + +} // namespace PySide +} // namespace FeatureSelector diff --git a/sources/pyside2/libpyside/feature_select.h b/sources/pyside2/libpyside/feature_select.h new file mode 100644 index 000000000..68e29292d --- /dev/null +++ b/sources/pyside2/libpyside/feature_select.h @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt for Python. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** 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-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef FEATURE_SELECT_H +#define FEATURE_SELECT_H + +#include "pysidemacros.h" + +namespace PySide { +namespace FeatureSelector { + +PYSIDE_API void init(); + +} // namespace PySide +} // namespace FeatureSelector + +#endif // FEATURE_SELECT_H diff --git a/sources/pyside2/libpyside/pyside.cpp b/sources/pyside2/libpyside/pyside.cpp index 66e931164..074fa764b 100644 --- a/sources/pyside2/libpyside/pyside.cpp +++ b/sources/pyside2/libpyside/pyside.cpp @@ -50,6 +50,7 @@ #include "pysidemetafunction_p.h" #include "pysidemetafunction.h" #include "dynamicqmetaobject.h" +#include "feature_select.h" #include #include @@ -93,6 +94,7 @@ void init(PyObject *module) MetaFunction::init(module); // Init signal manager, so it will register some meta types used by QVariant. SignalManager::instance(); + FeatureSelector::init(); initQApp(); } diff --git a/sources/pyside2/tests/QtCore/CMakeLists.txt b/sources/pyside2/tests/QtCore/CMakeLists.txt index 7973a51ca..771e1aeef 100644 --- a/sources/pyside2/tests/QtCore/CMakeLists.txt +++ b/sources/pyside2/tests/QtCore/CMakeLists.txt @@ -37,6 +37,7 @@ PYSIDE_TEST(deletelater_test.py) PYSIDE_TEST(destroysignal_test.py) PYSIDE_TEST(duck_punching_test.py) PYSIDE_TEST(emoji_string_test.py) +PYSIDE_TEST(feature_test.py) PYSIDE_TEST(hash_test.py) PYSIDE_TEST(inherits_test.py) PYSIDE_TEST(max_signals.py) diff --git a/sources/pyside2/tests/QtCore/feature_test.py b/sources/pyside2/tests/QtCore/feature_test.py new file mode 100644 index 000000000..cf1e8c3f2 --- /dev/null +++ b/sources/pyside2/tests/QtCore/feature_test.py @@ -0,0 +1,105 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## 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 Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## 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-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +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 import QtCore +from PySide2.support.__feature__ import _really_all_feature_names +from textwrap import dedent + +""" +feature_test.py +-------------- + +This tests the selectable features in PySide. + +There are no real features implemented. They will be added, later. +""" + +class FeaturesTest(unittest.TestCase): + + def testAllFeatureCombinations(self): + """ + Test for all 256 possible combinations of `__feature__` imports. + """ + global __name__ + + for bit in range(8): + # We are cheating here, since the functions are in the globals. + exec(dedent(""" + + def tst_bit{0}(flag, self): + if flag == 0: + with self.assertRaises(AttributeError): + QtCore.QCborArray.fake_feature_{1:02x} + with self.assertRaises(KeyError): + QtCore.QCborArray.__dict__["fake_feature_{1:02x}"] + else: + QtCore.QCborArray.fake_feature_{1:02x} + QtCore.QCborArray.__dict__["fake_feature_{1:02x}"] + + """.format(bit, 1 << bit)), globals(), globals()) + feature_list = _really_all_feature_names + func_list = [tst_bit0, tst_bit1, tst_bit2, tst_bit3, + tst_bit4, tst_bit5, tst_bit6, tst_bit7] + + for idx in range(0x100): + __name__ = "feature_{:02x}".format(idx) + print() + print("--- Feature Test Module `{}` ---".format(__name__)) + print("Imports:") + for bit in range(8): + if idx & 1 << bit: + feature = feature_list[bit] + text = "from __feature__ import {}".format(feature) + print(text) + exec(text) + for bit in range(8): + value = idx & 1 << bit + func_list[bit](value, self=self) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/shiboken2/libshiboken/basewrapper.cpp b/sources/shiboken2/libshiboken/basewrapper.cpp index 4c15582e9..5c2dc7807 100644 --- a/sources/shiboken2/libshiboken/basewrapper.cpp +++ b/sources/shiboken2/libshiboken/basewrapper.cpp @@ -95,12 +95,31 @@ void Sbk_object_dealloc(PyObject *self) static void SbkObjectTypeDealloc(PyObject *pyObj); static PyObject *SbkObjectTypeTpNew(PyTypeObject *metatype, PyObject *args, PyObject *kwds); +static SelectableFeatureHook SelectFeatureSet = nullptr; + +void initSelectableFeature(SelectableFeatureHook func) +{ + SelectFeatureSet = func; +} + +// PYSIDE-1019: Switch type's tp_dict to the currently active namespace. +static PyObject *Sbk_TypeGet___dict__(PyTypeObject *type, void *context) +{ + auto dict = type->tp_dict; + if (dict == NULL) + Py_RETURN_NONE; + if (SelectFeatureSet != nullptr) + dict = SelectFeatureSet(type); + return PyDictProxy_New(dict); +} + // PYSIDE-908: The function PyType_Modified does not work in PySide, so we need to // explicitly pass __doc__. For __signature__ it _did_ actually work, because // it was not existing before. We add them both for clarity. static PyGetSetDef SbkObjectType_Type_getsetlist[] = { {const_cast("__signature__"), (getter)Sbk_TypeGet___signature__}, {const_cast("__doc__"), (getter)Sbk_TypeGet___doc__}, + {const_cast("__dict__"), (getter)Sbk_TypeGet___dict__}, {nullptr} // Sentinel }; @@ -121,8 +140,25 @@ static PyObject *SbkObjectType_repr(PyObject *type) #endif // PY_VERSION_HEX < 0x03000000 +// PYSIDE-1019: Switch type's tp_dict to the currently active namespace. +static PyObject *(*type_getattro)(PyObject *type, PyObject *name); + +static PyObject *mangled_type_getattro(PyTypeObject *type, PyObject *name) +{ + /* + * Note: This `type_getattro` version is only the default that comes + * from `PyType_Type.tp_getattro`. This does *not* interfere in any way + * with the complex `tp_getattro` of `QObject` and other instances. + * What we change here is the meta class of `QObject`. + */ + if (SelectFeatureSet != nullptr) + type->tp_dict = SelectFeatureSet(type); + return type_getattro(reinterpret_cast(type), name); +} + static PyType_Slot SbkObjectType_Type_slots[] = { {Py_tp_dealloc, reinterpret_cast(SbkObjectTypeDealloc)}, + {Py_tp_getattro, reinterpret_cast(mangled_type_getattro)}, {Py_tp_setattro, reinterpret_cast(PyObject_GenericSetAttr)}, {Py_tp_base, static_cast(&PyType_Type)}, {Py_tp_alloc, reinterpret_cast(PyType_GenericAlloc)}, @@ -235,6 +271,9 @@ PyTypeObject *SbkObjectType_TypeF(void) { static PyTypeObject *type = nullptr; if (!type) { + // PYSIDE-1019: Insert the default tp_getattro explicitly here + // so we can overwrite it a bit. + type_getattro = PyType_Type.tp_getattro; SbkObjectType_Type_spec.basicsize = PepHeapType_SIZE + sizeof(SbkObjectTypePrivate); type = reinterpret_cast(SbkType_FromSpec(&SbkObjectType_Type_spec)); diff --git a/sources/shiboken2/libshiboken/basewrapper.h b/sources/shiboken2/libshiboken/basewrapper.h index 7248103cc..a4a8629fb 100644 --- a/sources/shiboken2/libshiboken/basewrapper.h +++ b/sources/shiboken2/libshiboken/basewrapper.h @@ -93,6 +93,9 @@ typedef void (*ObjectDestructor)(void *); typedef void (*SubTypeInitHook)(SbkObjectType *, PyObject *, PyObject *); +typedef PyObject *(*SelectableFeatureHook)(PyTypeObject *); +LIBSHIBOKEN_API void initSelectableFeature(SelectableFeatureHook func); + extern LIBSHIBOKEN_API PyTypeObject *SbkObjectType_TypeF(void); extern LIBSHIBOKEN_API SbkObjectType *SbkObject_TypeF(void); diff --git a/sources/shiboken2/libshiboken/pep384impl.cpp b/sources/shiboken2/libshiboken/pep384impl.cpp index 5e0053e2e..4149bbcbf 100644 --- a/sources/shiboken2/libshiboken/pep384impl.cpp +++ b/sources/shiboken2/libshiboken/pep384impl.cpp @@ -78,14 +78,20 @@ static PyGetSetDef probe_getseters[] = { {nullptr} /* Sentinel */ }; +static PyMemberDef probe_members[] = { + {nullptr} /* Sentinel */ +}; + #define probe_tp_dealloc make_dummy(1) #define probe_tp_repr make_dummy(2) #define probe_tp_call make_dummy(3) +#define probe_tp_getattro make_dummy(16) #define probe_tp_str make_dummy(4) #define probe_tp_traverse make_dummy(5) #define probe_tp_clear make_dummy(6) #define probe_tp_iternext make_dummy(7) #define probe_tp_methods probe_methoddef +#define probe_tp_members probe_members #define probe_tp_getset probe_getseters #define probe_tp_descr_get make_dummy(10) #define probe_tp_init make_dummy(11) @@ -101,11 +107,13 @@ static PyType_Slot typeprobe_slots[] = { {Py_tp_dealloc, probe_tp_dealloc}, {Py_tp_repr, probe_tp_repr}, {Py_tp_call, probe_tp_call}, + {Py_tp_getattro, probe_tp_getattro}, {Py_tp_str, probe_tp_str}, {Py_tp_traverse, probe_tp_traverse}, {Py_tp_clear, probe_tp_clear}, {Py_tp_iternext, probe_tp_iternext}, {Py_tp_methods, probe_tp_methods}, + {Py_tp_members, probe_tp_members}, {Py_tp_getset, probe_tp_getset}, {Py_tp_descr_get, probe_tp_descr_get}, {Py_tp_init, probe_tp_init}, @@ -144,6 +152,7 @@ check_PyTypeObject_valid() || probe_tp_dealloc != check->tp_dealloc || probe_tp_repr != check->tp_repr || probe_tp_call != check->tp_call + || probe_tp_getattro != check->tp_getattro || probe_tp_str != check->tp_str || probe_tp_traverse != check->tp_traverse || probe_tp_clear != check->tp_clear @@ -427,27 +436,6 @@ PyRun_String(const char *str, int start, PyObject *globals, PyObject *locals) #endif // Py_LIMITED_API -// This is only a simple local helper that returns a computed variable. -// Used also in Python 2. -#if defined(Py_LIMITED_API) || defined(IS_PY2) -static PyObject * -PepRun_GetResult(const char *command) -{ - PyObject *d, *v, *res; - - d = PyDict_New(); - if (d == nullptr - || PyDict_SetItem(d, Shiboken::PyMagicName::builtins(), PyEval_GetBuiltins()) < 0) { - return nullptr; - } - v = PyRun_String(command, Py_file_input, d, d); - res = v ? PyDict_GetItem(d, Shiboken::PyName::result()) : nullptr; - Py_XDECREF(v); - Py_DECREF(d); - return res; -} -#endif // defined(Py_LIMITED_API) || defined(IS_PY2) - /***************************************************************************** * * Support for classobject.h @@ -669,6 +657,29 @@ PyImport_GetModule(PyObject *name) #endif // PY_VERSION_HEX < 0x03070000 || defined(Py_LIMITED_API) +// 2020-06-16: For simplicity of creating arbitrary things, this function +// is now made public. + +PyObject * +PepRun_GetResult(const char *command) +{ + /* + * Evaluate a string and return the variable `result` + */ + PyObject *d, *v, *res; + + d = PyDict_New(); + if (d == nullptr + || PyDict_SetItem(d, Shiboken::PyMagicName::builtins(), PyEval_GetBuiltins()) < 0) { + return nullptr; + } + v = PyRun_String(command, Py_file_input, d, d); + res = v ? PyDict_GetItem(d, Shiboken::PyName::result()) : nullptr; + Py_XDECREF(v); + Py_DECREF(d); + return res; +} + /***************************************************************************** * * Python 2 incompatibilities diff --git a/sources/shiboken2/libshiboken/pep384impl.h b/sources/shiboken2/libshiboken/pep384impl.h index 2bfe52254..2a14d6543 100644 --- a/sources/shiboken2/libshiboken/pep384impl.h +++ b/sources/shiboken2/libshiboken/pep384impl.h @@ -98,7 +98,7 @@ typedef struct _typeobject { void *X13; // hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; - void *X16; // getattrofunc tp_getattro; + getattrofunc tp_getattro; void *X17; // setattrofunc tp_setattro; void *X18; // PyBufferProcs *tp_as_buffer; unsigned long tp_flags; @@ -110,7 +110,7 @@ typedef struct _typeobject { void *X25; // getiterfunc tp_iter; iternextfunc tp_iternext; struct PyMethodDef *tp_methods; - void *X28; // struct PyMemberDef *tp_members; + struct PyMemberDef *tp_members; struct PyGetSetDef *tp_getset; struct _typeobject *tp_base; PyObject *tp_dict; @@ -531,6 +531,9 @@ extern LIBSHIBOKEN_API PyTypeObject *PepMethodDescr_TypePtr; LIBSHIBOKEN_API PyObject *PyImport_GetModule(PyObject *name); #endif // PY_VERSION_HEX < 0x03070000 || defined(Py_LIMITED_API) +// Evaluate a script and return the variable `result` +LIBSHIBOKEN_API PyObject *PepRun_GetResult(const char *command); + /***************************************************************************** * * Python 2 incompatibilities diff --git a/sources/shiboken2/libshiboken/sbkstaticstrings.cpp b/sources/shiboken2/libshiboken/sbkstaticstrings.cpp index 541d74918..9dd98eef3 100644 --- a/sources/shiboken2/libshiboken/sbkstaticstrings.cpp +++ b/sources/shiboken2/libshiboken/sbkstaticstrings.cpp @@ -52,9 +52,11 @@ namespace Shiboken { namespace PyName { // exported: +STATIC_STRING_IMPL(dict_ring, "dict_ring") STATIC_STRING_IMPL(dumps, "dumps") STATIC_STRING_IMPL(loads, "loads") STATIC_STRING_IMPL(result, "result") +STATIC_STRING_IMPL(select_id, "select_id") STATIC_STRING_IMPL(value, "value") STATIC_STRING_IMPL(values, "values") @@ -75,6 +77,7 @@ STATIC_STRING_IMPL(staticmethod, "staticmethod") namespace PyMagicName { // exported: STATIC_STRING_IMPL(class_, "__class__") +STATIC_STRING_IMPL(dict, "__dict__") STATIC_STRING_IMPL(ecf, "__ecf__") STATIC_STRING_IMPL(file, "__file__") STATIC_STRING_IMPL(members, "__members__") diff --git a/sources/shiboken2/libshiboken/sbkstaticstrings.h b/sources/shiboken2/libshiboken/sbkstaticstrings.h index 4078d163c..ebb64579c 100644 --- a/sources/shiboken2/libshiboken/sbkstaticstrings.h +++ b/sources/shiboken2/libshiboken/sbkstaticstrings.h @@ -49,11 +49,13 @@ namespace Shiboken namespace PyName { LIBSHIBOKEN_API PyObject *co_name(); +LIBSHIBOKEN_API PyObject *dict_ring(); LIBSHIBOKEN_API PyObject *dumps(); LIBSHIBOKEN_API PyObject *f_code(); LIBSHIBOKEN_API PyObject *f_lineno(); LIBSHIBOKEN_API PyObject *loads(); LIBSHIBOKEN_API PyObject *result(); +LIBSHIBOKEN_API PyObject *select_id(); LIBSHIBOKEN_API PyObject *value(); LIBSHIBOKEN_API PyObject *values(); } // namespace PyName @@ -61,6 +63,7 @@ LIBSHIBOKEN_API PyObject *values(); namespace PyMagicName { LIBSHIBOKEN_API PyObject *class_(); +LIBSHIBOKEN_API PyObject *dict(); LIBSHIBOKEN_API PyObject *ecf(); LIBSHIBOKEN_API PyObject *file(); LIBSHIBOKEN_API PyObject *members(); diff --git a/sources/shiboken2/libshiboken/shiboken.h b/sources/shiboken2/libshiboken/shiboken.h index 0d2d6b0a6..3e1df5235 100644 --- a/sources/shiboken2/libshiboken/shiboken.h +++ b/sources/shiboken2/libshiboken/shiboken.h @@ -55,6 +55,7 @@ #include "sbkstaticstrings.h" #include "shibokenmacros.h" #include "shibokenbuffer.h" +#include "signature.h" #endif // SHIBOKEN_H diff --git a/sources/shiboken2/libshiboken/signature.cpp b/sources/shiboken2/libshiboken/signature.cpp index b09b68d47..4614a131e 100644 --- a/sources/shiboken2/libshiboken/signature.cpp +++ b/sources/shiboken2/libshiboken/signature.cpp @@ -74,8 +74,8 @@ typedef struct safe_globals_struc { PyObject *helper_module; PyObject *arg_dict; PyObject *map_dict; - PyObject *value_dict; // for writing signatures - PyObject *feature_dict; // registry for PySide.__feature__ + PyObject *value_dict; // for writing signatures + PyObject *feature_dict; // registry for PySide.support.__feature__ // init part 2: run module PyObject *pyside_type_init_func; PyObject *create_signature_func; @@ -653,22 +653,17 @@ _fixup_getset(PyTypeObject *type, const char *name, PyGetSetDef *new_gsp) } } } - PyMemberDef *md = type->tp_members; - if (md != nullptr) - for (; md->name != nullptr; md++) - if (strcmp(md->name, name) == 0) - return 1; - // staticmethod has just a `__doc__` in the class - assert(strcmp(type->tp_name, "staticmethod") == 0 && strcmp(name, "__doc__") == 0); + // staticmethod has just a __doc__ in the class + assert(strcmp(type->tp_name, "staticmethod") == 0); return 0; } static int -add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **doc_descr) +add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **old_descr) { /* - * This function is used to assign a new `__signature__` attribute, - * and also to override a `__doc__` or `__name__` attribute. + * This function is used to assign a new __signature__ attribute, + * and also to override a __doc__ attribute. */ assert(PyType_Check(type)); PyType_Ready(type); @@ -676,11 +671,9 @@ add_more_getsets(PyTypeObject *type, PyGetSetDef *gsp, PyObject **doc_descr) for (; gsp->name != nullptr; gsp++) { PyObject *have_descr = PyDict_GetItemString(dict, gsp->name); if (have_descr != nullptr) { + assert(strcmp(gsp->name, "__doc__") == 0); Py_INCREF(have_descr); - if (strcmp(gsp->name, "__doc__") == 0) - *doc_descr = have_descr; - else - assert(false); + *old_descr = have_descr; if (!_fixup_getset(type, gsp->name, gsp)) continue; } @@ -831,7 +824,7 @@ static PyGetSetDef new_PyWrapperDescr_getsets[] = { // // Additionally to the interface via __signature__, we also provide // a general function, which allows for different signature layouts. -// The "modifier" argument is a string that is passed in from 'loader.py'. +// The "modifier" argument is a string that is passed in from loader.py . // Configuration what the modifiers mean is completely in Python. // @@ -916,25 +909,13 @@ PySide_PatchTypes(void) reinterpret_cast(&PyString_Type), "split")); Shiboken::AutoDecRef wrap_descr(PyObject_GetAttrString( reinterpret_cast(Py_TYPE(Py_True)), "__add__")); - // abbreviations for readability - auto md_gs = new_PyMethodDescr_getsets; - auto md_doc = &old_md_doc_descr; - auto cf_gs = new_PyCFunction_getsets; - auto cf_doc = &old_cf_doc_descr; - auto sm_gs = new_PyStaticMethod_getsets; - auto sm_doc = &old_sm_doc_descr; - auto tp_gs = new_PyType_getsets; - auto tp_doc = &old_tp_doc_descr; - auto wd_gs = new_PyWrapperDescr_getsets; - auto wd_doc = &old_wd_doc_descr; - if (meth_descr.isNull() || wrap_descr.isNull() || PyType_Ready(Py_TYPE(meth_descr)) < 0 - || add_more_getsets(PepMethodDescr_TypePtr, md_gs, md_doc) < 0 - || add_more_getsets(&PyCFunction_Type, cf_gs, cf_doc) < 0 - || add_more_getsets(PepStaticMethod_TypePtr, sm_gs, sm_doc) < 0 - || add_more_getsets(&PyType_Type, tp_gs, tp_doc) < 0 - || add_more_getsets(Py_TYPE(wrap_descr), wd_gs, wd_doc) < 0 + || add_more_getsets(PepMethodDescr_TypePtr, new_PyMethodDescr_getsets, &old_md_doc_descr) < 0 + || add_more_getsets(&PyCFunction_Type, new_PyCFunction_getsets, &old_cf_doc_descr) < 0 + || add_more_getsets(PepStaticMethod_TypePtr, new_PyStaticMethod_getsets, &old_sm_doc_descr) < 0 + || add_more_getsets(&PyType_Type, new_PyType_getsets, &old_tp_doc_descr) < 0 + || add_more_getsets(Py_TYPE(wrap_descr), new_PyWrapperDescr_getsets, &old_wd_doc_descr) < 0 ) return -1; #ifndef _WIN32 @@ -1233,8 +1214,8 @@ FinishSignatureInitialization(PyObject *module, const char *signatures[]) * Still, it is not possible to call init phase 2 from here, * because the import is still running. Do it from Python! */ - if ( PySide_PatchTypes() < 0 - || PySide_FinishSignatures(module, signatures) < 0) { + PySide_PatchTypes(); + if (PySide_FinishSignatures(module, signatures) < 0) { PyErr_Print(); PyErr_SetNone(PyExc_ImportError); } @@ -1285,4 +1266,10 @@ PyObject *Sbk_TypeGet___doc__(PyObject *ob) return pyside_tp_get___doc__(ob); } +PyObject *GetFeatureDict() +{ + init_module_1(); + return pyside_globals->feature_dict; +} + } //extern "C" diff --git a/sources/shiboken2/libshiboken/signature.h b/sources/shiboken2/libshiboken/signature.h index b22a78497..2a69df9cf 100644 --- a/sources/shiboken2/libshiboken/signature.h +++ b/sources/shiboken2/libshiboken/signature.h @@ -50,6 +50,7 @@ LIBSHIBOKEN_API void FinishSignatureInitialization(PyObject *, const char *[]); LIBSHIBOKEN_API void SetError_Argument(PyObject *, const char *); LIBSHIBOKEN_API PyObject *Sbk_TypeGet___signature__(PyObject *, PyObject *); LIBSHIBOKEN_API PyObject *Sbk_TypeGet___doc__(PyObject *); +LIBSHIBOKEN_API PyObject *GetFeatureDict(); } // extern "C" diff --git a/sources/shiboken2/shibokenmodule/CMakeLists.txt b/sources/shiboken2/shibokenmodule/CMakeLists.txt index e1eafa12f..b14de5c9e 100644 --- a/sources/shiboken2/shibokenmodule/CMakeLists.txt +++ b/sources/shiboken2/shibokenmodule/CMakeLists.txt @@ -42,6 +42,8 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_config.py" configure_file("${CMAKE_CURRENT_SOURCE_DIR}/__init__.py.in" "${CMAKE_CURRENT_BINARY_DIR}/__init__.py" @ONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/files.dir/shibokensupport/__feature__.py" + "${CMAKE_CURRENT_BINARY_DIR}/files.dir/shibokensupport/__feature__.py" COPYONLY) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/files.dir/shibokensupport/__init__.py" "${CMAKE_CURRENT_BINARY_DIR}/files.dir/shibokensupport/__init__.py" COPYONLY) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/files.dir/shibokensupport/signature/__init__.py" diff --git a/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py new file mode 100644 index 000000000..58c1ff925 --- /dev/null +++ b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py @@ -0,0 +1,111 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## 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 Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## 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-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +from __future__ import print_function, absolute_import + +""" +__feature__.py + +This is the feature file for the Qt for Python project. There is some +similarity to Python's `__future__` file, but also some distinction. +""" + +import sys + +all_feature_names = [ + "_dummy_feature_01", + "_dummy_feature_02", + "_dummy_feature_04", + "_dummy_feature_08", + "_dummy_feature_10", + "_dummy_feature_20", + "_dummy_feature_40", + "_dummy_feature_80", +] + +__all__ = ["all_feature_names"] + all_feature_names + +_dummy_feature_01 = 0x01 +_dummy_feature_02 = 0x02 +_dummy_feature_04 = 0x04 +_dummy_feature_08 = 0x08 +_dummy_feature_10 = 0x10 +_dummy_feature_20 = 0x20 +_dummy_feature_40 = 0x40 +_dummy_feature_80 = 0x80 + +# let's remove the dummies for the normal user +_really_all_feature_names = all_feature_names[:] +all_feature_names = list(_ for _ in all_feature_names if not _.startswith("_")) + +# Install an import hook that controls the `__feature__` import. +""" +Note: This are two imports. +>>> import dis +>>> def test(): +... from __feature__ import snake_case +... +>>> dis.dis(test) + 2 0 LOAD_CONST 1 (0) + 2 LOAD_CONST 2 (('snake_case',)) + 4 IMPORT_NAME 0 (__feature__) + 6 IMPORT_FROM 1 (snake_case) + 8 STORE_FAST 0 (snake_case) + 10 POP_TOP + 12 LOAD_CONST 0 (None) + 14 RETURN_VALUE +""" +# XXX build an improved C version +def _import(name, *args, **kwargs): + if name == "__feature__" and args[2]: + # This is an `import from` statement that corresponds to `IMPORT_NAME`. + # The following `IMPORT_FROM` will handle errors. (Confusing, ofc.) + flag = 0 + for feature in args[2]: + if feature in _really_all_feature_names: + flag |= globals()[feature] + else: + raise SyntaxError("PySide feature {} is not defined".format(feature)) + importing_module = sys._getframe(1).f_globals['__name__'] + existing = pyside_feature_dict.get(importing_module, 0) + if isinstance(existing, int): + flag |= existing & 255 + pyside_feature_dict[importing_module] = flag + return sys.modules["__feature__"] + return original_import(name, *args, **kwargs) diff --git a/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/signature/loader.py b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/signature/loader.py index 1efc6fde5..6cee54680 100644 --- a/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/signature/loader.py +++ b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/signature/loader.py @@ -114,8 +114,10 @@ def finish_import(module): import signature_bootstrap -from shibokensupport import signature +from shibokensupport import signature, __feature__ signature.get_signature = signature_bootstrap.get_signature +# PYSIDE-1019: Publish the __feature__ dictionary. +__feature__.pyside_feature_dict = signature_bootstrap.pyside_feature_dict del signature_bootstrap def _get_modname(mod): @@ -194,6 +196,7 @@ def move_into_pyside_package(): import PySide2.support except ModuleNotFoundError: PySide2.support = types.ModuleType("PySide2.support") + put_into_package(PySide2.support, __feature__) put_into_package(PySide2.support, signature) put_into_package(PySide2.support.signature, mapping) put_into_package(PySide2.support.signature, errorhandler) @@ -217,5 +220,16 @@ from shibokensupport.signature.lib import enum_sig if "PySide2" in sys.modules: # We publish everything under "PySide2.support.signature", again. move_into_pyside_package() + # PYSIDE-1019: Modify `__import__` to be `__feature__` aware. + # __feature__ is already in sys.modules, so this is actually no import + try: + import PySide2.support.__feature__ + sys.modules["__feature__"] = PySide2.support.__feature__ + PySide2.support.__feature__.original_import = __builtins__["__import__"] + __builtins__["__import__"] = PySide2.support.__feature__._import + # Maybe we should optimize that and change `__import__` from C, instead? + except ModuleNotFoundError: + print("__feature__ could not be imported. " + "This is an unsolved PyInstaller problem.", file=sys.stderr) # end of file -- cgit v1.2.3 From 3d98daaa90e9c7f7784d1a388d97039fcdeffdf1 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Mon, 6 Jul 2020 14:16:48 +0200 Subject: Rewrite the settings editor example - Port settings editor example from QRegExp to QRegularExpression - Use snake case names - Import classes - Make the type checking mechanism work for more types by reading out bool/int values from QSettings by type - Use QCheckBox, QSpinBox for bool/int Change-Id: Ib6b69536df3f26afa5c0e2babed7bad5de471d7f Reviewed-by: Christian Tismer Reviewed-by: Cristian Maureira-Fredes --- .../corelib/tools/settingseditor/settingseditor.py | 889 +++++++++++---------- 1 file changed, 469 insertions(+), 420 deletions(-) diff --git a/examples/corelib/tools/settingseditor/settingseditor.py b/examples/corelib/tools/settingseditor/settingseditor.py index 4cd262568..a3e50a7c9 100644 --- a/examples/corelib/tools/settingseditor/settingseditor.py +++ b/examples/corelib/tools/settingseditor/settingseditor.py @@ -1,8 +1,9 @@ +# -*- coding: utf-8 -*- ############################################################################# ## ## Copyright (C) 2013 Riverbank Computing Limited. -## Copyright (C) 2016 The Qt Company Ltd. +## Copyright (C) 2020 The Qt Company Ltd. ## Contact: http://www.qt.io/licensing/ ## ## This file is part of the Qt for Python examples of the Qt Toolkit. @@ -44,260 +45,379 @@ import sys -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2.QtCore import (QByteArray, QDate, QDateTime, QDir, QEvent, QPoint, + QRect, QRegularExpression, QSettings, QSize, QTime, QTimer, Qt) +from PySide2.QtGui import (QColor, QIcon, QIntValidator, QDoubleValidator, + QRegularExpressionValidator, QValidator) +from PySide2.QtWidgets import (QAbstractItemView, QAction, QApplication, + QCheckBox, QComboBox, QFileDialog, QDialog, QDialogButtonBox, QGridLayout, + QGroupBox, QHeaderView, QInputDialog, QItemDelegate, QLabel, QLineEdit, + QMainWindow, QMessageBox, QStyle, QSpinBox, QStyleOptionViewItem, + QTableWidget, QTableWidgetItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout) -class MainWindow(QtWidgets.QMainWindow): +class TypeChecker: + def __init__(self, parent=None): + self.bool_exp = QRegularExpression('^(true)|(false)$') + assert self.bool_exp.isValid() + self.bool_exp.setPatternOptions(QRegularExpression.CaseInsensitiveOption) + + self.byteArray_exp = QRegularExpression(r'^[\x00-\xff]*$') + assert self.byteArray_exp.isValid() + + self.char_exp = QRegularExpression('^.$') + assert self.char_exp.isValid() + + pattern = r'^[+-]?\d+$' + self.int_exp = QRegularExpression(pattern) + assert self.int_exp.isValid() + + pattern = r'^\(([0-9]*),([0-9]*),([0-9]*),([0-9]*)\)$' + self.color_exp = QRegularExpression(pattern) + assert self.color_exp.isValid() + + pattern = r'^\((-?[0-9]*),(-?[0-9]*)\)$' + self.point_exp = QRegularExpression(pattern) + assert self.point_exp.isValid() + + pattern = r'^\((-?[0-9]*),(-?[0-9]*),(-?[0-9]*),(-?[0-9]*)\)$' + self.rect_exp = QRegularExpression(pattern) + assert self.rect_exp.isValid() + + self.size_exp = QRegularExpression(self.point_exp) + + date_pattern = '([0-9]{,4})-([0-9]{,2})-([0-9]{,2})' + self.date_exp = QRegularExpression('^{}$'.format(date_pattern)) + assert self.date_exp.isValid() + + time_pattern = '([0-9]{,2}):([0-9]{,2}):([0-9]{,2})' + self.time_exp = QRegularExpression('^{}$'.format(time_pattern)) + assert self.time_exp.isValid() + + pattern = '^{}T{}$'.format(date_pattern, time_pattern) + self.dateTime_exp = QRegularExpression(pattern) + assert self.dateTime_exp.isValid() + + def type_from_text(self, text): + if self.bool_exp.match(text).hasMatch(): + return bool + if self.int_exp.match(text).hasMatch(): + return int + return None + + def create_validator(self, value, parent): + if isinstance(value, bool): + return QRegularExpressionValidator(self.bool_exp, parent) + if isinstance(value, float): + return QDoubleValidator(parent) + if isinstance(value, int): + return QIntValidator(parent) + if isinstance(value, QByteArray): + return QRegularExpressionValidator(self.byteArray_exp, parent) + if isinstance(value, QColor): + return QRegularExpressionValidator(self.color_exp, parent) + if isinstance(value, QDate): + return QRegularExpressionValidator(self.date_exp, parent) + if isinstance(value, QDateTime): + return QRegularExpressionValidator(self.dateTime_exp, parent) + if isinstance(value, QTime): + return QRegularExpressionValidator(self.time_exp, parent) + if isinstance(value, QPoint): + return QRegularExpressionValidator(self.point_exp, parent) + if isinstance(value, QRect): + return QRegularExpressionValidator(self.rect_exp, parent) + if isinstance(value, QSize): + return QRegularExpressionValidator(self.size_exp, parent) + return None + + def from_string(self, text, original_value): + if isinstance(original_value, QColor): + match = self.color_exp.match(text) + return QColor(min(int(match.captured(1)), 255), + min(int(match.captured(2)), 255), + min(int(match.captured(3)), 255), + min(int(match.captured(4)), 255)) + if isinstance(original_value, QDate): + value = QDate.fromString(text, Qt.ISODate) + return value if value.isValid() else None + if isinstance(original_value, QDateTime): + value = QDateTime.fromString(text, Qt.ISODate) + return value if value.isValid() else None + if isinstance(original_value, QTime): + value = QTime.fromString(text, Qt.ISODate) + return value if value.isValid() else None + if isinstance(original_value, QPoint): + match = self.point_exp.match(text) + return QPoint(int(match.captured(1)), + int(match.captured(2))) + if isinstance(original_value, QRect): + match = self.rect_exp.match(text) + return QRect(int(match.captured(1)), + int(match.captured(2)), + int(match.captured(3)), + int(match.captured(4))) + if isinstance(original_value, QSize): + match = self.size_exp.match(text) + return QSize(int(match.captured(1)), + int(match.captured(2))) + if isinstance(original_value, list): + return text.split(',') + return type(original_value)(text) + + +class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) - self.settingsTree = SettingsTree() - self.setCentralWidget(self.settingsTree) + self.settings_tree = SettingsTree() + self.setCentralWidget(self.settings_tree) - self.locationDialog = None + self.location_dialog = None - self.createActions() - self.createMenus() + self.create_actions() + self.create_menus() - self.autoRefreshAct.setChecked(True) - self.fallbacksAct.setChecked(True) + self.auto_refresh_action.setChecked(True) + self.fallbacks_action.setChecked(True) self.setWindowTitle("Settings Editor") self.resize(500, 600) - def openSettings(self): - if self.locationDialog is None: - self.locationDialog = LocationDialog(self) + def open_settings(self): + if self.location_dialog is None: + self.location_dialog = LocationDialog(self) - if self.locationDialog.exec_(): - settings = QtCore.QSettings(self.locationDialog.format(), - self.locationDialog.scope(), - self.locationDialog.organization(), - self.locationDialog.application()) - self.setSettingsObject(settings) - self.fallbacksAct.setEnabled(True) + if self.location_dialog.exec_(): + settings = QSettings(self.location_dialog.format(), + self.location_dialog.scope(), + self.location_dialog.organization(), + self.location_dialog.application()) + self.set_settings_object(settings) + self.fallbacks_action.setEnabled(True) - def openIniFile(self): - fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open INI File", + def open_inifile(self): + file_name, _ = QFileDialog.getOpenFileName(self, "Open INI File", '', "INI Files (*.ini *.conf)") - if fileName: - settings = QtCore.QSettings(fileName, QtCore.QSettings.IniFormat) - self.setSettingsObject(settings) - self.fallbacksAct.setEnabled(False) + if file_name: + self.load_ini_file(file_name) + + def load_ini_file(self, file_name): + settings = QSettings(file_name, QSettings.IniFormat) + if settings.status() != QSettings.NoError: + return + self.set_settings_object(settings) + self.fallbacks_action.setEnabled(False) - def openPropertyList(self): - fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, + def open_property_list(self): + file_name, _ = QFileDialog.getOpenFileName(self, "Open Property List", '', "Property List Files (*.plist)") - if fileName: - settings = QtCore.QSettings(fileName, QtCore.QSettings.NativeFormat) - self.setSettingsObject(settings) - self.fallbacksAct.setEnabled(False) + if file_name: + settings = QSettings(file_name, QSettings.NativeFormat) + self.set_settings_object(settings) + self.fallbacks_action.setEnabled(False) - def openRegistryPath(self): - path, ok = QtWidgets.QInputDialog.getText(self, "Open Registry Path", + def open_registry_path(self): + path, ok = QInputDialog.getText(self, "Open Registry Path", "Enter the path in the Windows registry:", - QtWidgets.QLineEdit.Normal, 'HKEY_CURRENT_USER\\') + QLineEdit.Normal, 'HKEY_CURRENT_USER\\') if ok and path != '': - settings = QtCore.QSettings(path, QtCore.QSettings.NativeFormat) - self.setSettingsObject(settings) - self.fallbacksAct.setEnabled(False) + settings = QSettings(path, QSettings.NativeFormat) + self.set_settings_object(settings) + self.fallbacks_action.setEnabled(False) def about(self): - QtWidgets.QMessageBox.about(self, "About Settings Editor", + QMessageBox.about(self, "About Settings Editor", "The Settings Editor example shows how to access " "application settings using Qt.") - def createActions(self): - self.openSettingsAct = QtWidgets.QAction("&Open Application Settings...", - self, shortcut="Ctrl+O", triggered=self.openSettings) + def create_actions(self): + self.open_settings_action = QAction("&Open Application Settings...", + self, shortcut="Ctrl+O", triggered=self.open_settings) - self.openIniFileAct = QtWidgets.QAction("Open I&NI File...", self, - shortcut="Ctrl+N", triggered=self.openIniFile) + self.open_ini_file_action = QAction("Open I&NI File...", self, + shortcut="Ctrl+N", triggered=self.open_inifile) - self.openPropertyListAct = QtWidgets.QAction("Open macOS &Property List...", - self, shortcut="Ctrl+P", triggered=self.openPropertyList) + self.open_property_list_action = QAction("Open macOS &Property List...", + self, shortcut="Ctrl+P", triggered=self.open_property_list) if sys.platform != 'darwin': - self.openPropertyListAct.setEnabled(False) + self.open_property_list_action.setEnabled(False) - self.openRegistryPathAct = QtWidgets.QAction( + self.open_registry_path_action = QAction( "Open Windows &Registry Path...", self, shortcut="Ctrl+G", - triggered=self.openRegistryPath) + triggered=self.open_registry_path) if sys.platform != 'win32': - self.openRegistryPathAct.setEnabled(False) + self.open_registry_path_action.setEnabled(False) - self.refreshAct = QtWidgets.QAction("&Refresh", self, shortcut="Ctrl+R", - enabled=False, triggered=self.settingsTree.refresh) + self.refresh_action = QAction("&Refresh", self, shortcut="Ctrl+R", + enabled=False, triggered=self.settings_tree.refresh) - self.exitAct = QtWidgets.QAction("E&xit", self, shortcut="Ctrl+Q", + self.exit_action = QAction("E&xit", self, shortcut="Ctrl+Q", triggered=self.close) - self.autoRefreshAct = QtWidgets.QAction("&Auto-Refresh", self, + self.auto_refresh_action = QAction("&Auto-Refresh", self, shortcut="Ctrl+A", checkable=True, enabled=False) - self.autoRefreshAct.triggered[bool].connect(self.settingsTree.setAutoRefresh) - self.autoRefreshAct.triggered[bool].connect(self.refreshAct.setDisabled) + self.auto_refresh_action.triggered[bool].connect(self.settings_tree.set_auto_refresh) + self.auto_refresh_action.triggered[bool].connect(self.refresh_action.setDisabled) - self.fallbacksAct = QtWidgets.QAction("&Fallbacks", self, + self.fallbacks_action = QAction("&Fallbacks", self, shortcut="Ctrl+F", checkable=True, enabled=False) - self.fallbacksAct.triggered[bool].connect(self.settingsTree.setFallbacksEnabled) + self.fallbacks_action.triggered[bool].connect(self.settings_tree.set_fallbacks_enabled) - self.aboutAct = QtWidgets.QAction("&About", self, triggered=self.about) + self.about_action = QAction("&About", self, triggered=self.about) - self.aboutQtAct = QtWidgets.QAction("About &Qt", self, - triggered=qApp.aboutQt) + self.about_Qt_action = QAction("About &Qt", self, + triggered=qApp.aboutQt) - def createMenus(self): - self.fileMenu = self.menuBar().addMenu("&File") - self.fileMenu.addAction(self.openSettingsAct) - self.fileMenu.addAction(self.openIniFileAct) - self.fileMenu.addAction(self.openPropertyListAct) - self.fileMenu.addAction(self.openRegistryPathAct) - self.fileMenu.addSeparator() - self.fileMenu.addAction(self.refreshAct) - self.fileMenu.addSeparator() - self.fileMenu.addAction(self.exitAct) + def create_menus(self): + self.file_menu = self.menuBar().addMenu("&File") + self.file_menu.addAction(self.open_settings_action) + self.file_menu.addAction(self.open_ini_file_action) + self.file_menu.addAction(self.open_property_list_action) + self.file_menu.addAction(self.open_registry_path_action) + self.file_menu.addSeparator() + self.file_menu.addAction(self.refresh_action) + self.file_menu.addSeparator() + self.file_menu.addAction(self.exit_action) - self.optionsMenu = self.menuBar().addMenu("&Options") - self.optionsMenu.addAction(self.autoRefreshAct) - self.optionsMenu.addAction(self.fallbacksAct) + self.options_menu = self.menuBar().addMenu("&Options") + self.options_menu.addAction(self.auto_refresh_action) + self.options_menu.addAction(self.fallbacks_action) self.menuBar().addSeparator() - self.helpMenu = self.menuBar().addMenu("&Help") - self.helpMenu.addAction(self.aboutAct) - self.helpMenu.addAction(self.aboutQtAct) + self.help_menu = self.menuBar().addMenu("&Help") + self.help_menu.addAction(self.about_action) + self.help_menu.addAction(self.about_Qt_action) - def setSettingsObject(self, settings): - settings.setFallbacksEnabled(self.fallbacksAct.isChecked()) - self.settingsTree.setSettingsObject(settings) + def set_settings_object(self, settings): + settings.setFallbacksEnabled(self.fallbacks_action.isChecked()) + self.settings_tree.set_settings_object(settings) - self.refreshAct.setEnabled(True) - self.autoRefreshAct.setEnabled(True) + self.refresh_action.setEnabled(True) + self.auto_refresh_action.setEnabled(True) - niceName = settings.fileName() - niceName.replace('\\', '/') - niceName = niceName.split('/')[-1] + nice_name = QDir.fromNativeSeparators(settings.fileName()) + nice_name = nice_name.split('/')[-1] if not settings.isWritable(): - niceName += " (read only)" + nice_name += " (read only)" - self.setWindowTitle("%s - Settings Editor" % niceName) + self.setWindowTitle("{} - Settings Editor".format(nice_name)) -class LocationDialog(QtWidgets.QDialog): +class LocationDialog(QDialog): def __init__(self, parent=None): super(LocationDialog, self).__init__(parent) - self.formatComboBox = QtWidgets.QComboBox() - self.formatComboBox.addItem("Native") - self.formatComboBox.addItem("INI") - - self.scopeComboBox = QtWidgets.QComboBox() - self.scopeComboBox.addItem("User") - self.scopeComboBox.addItem("System") - - self.organizationComboBox = QtWidgets.QComboBox() - self.organizationComboBox.addItem("Trolltech") - self.organizationComboBox.setEditable(True) - - self.applicationComboBox = QtWidgets.QComboBox() - self.applicationComboBox.addItem("Any") - self.applicationComboBox.addItem("Application Example") - self.applicationComboBox.addItem("Assistant") - self.applicationComboBox.addItem("Designer") - self.applicationComboBox.addItem("Linguist") - self.applicationComboBox.setEditable(True) - self.applicationComboBox.setCurrentIndex(3) - - formatLabel = QtWidgets.QLabel("&Format:") - formatLabel.setBuddy(self.formatComboBox) - - scopeLabel = QtWidgets.QLabel("&Scope:") - scopeLabel.setBuddy(self.scopeComboBox) - - organizationLabel = QtWidgets.QLabel("&Organization:") - organizationLabel.setBuddy(self.organizationComboBox) - - applicationLabel = QtWidgets.QLabel("&Application:") - applicationLabel.setBuddy(self.applicationComboBox) - - self.locationsGroupBox = QtWidgets.QGroupBox("Setting Locations") - - self.locationsTable = QtWidgets.QTableWidget() - self.locationsTable.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.locationsTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.locationsTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self.locationsTable.setColumnCount(2) - self.locationsTable.setHorizontalHeaderLabels(("Location", "Access")) - self.locationsTable.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - self.locationsTable.horizontalHeader().resizeSection(1, 180) - - self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) - - self.formatComboBox.activated.connect(self.updateLocationsTable) - self.scopeComboBox.activated.connect(self.updateLocationsTable) - self.organizationComboBox.lineEdit().editingFinished.connect(self.updateLocationsTable) - self.applicationComboBox.lineEdit().editingFinished.connect(self.updateLocationsTable) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - locationsLayout = QtWidgets.QVBoxLayout() - locationsLayout.addWidget(self.locationsTable) - self.locationsGroupBox.setLayout(locationsLayout) - - mainLayout = QtWidgets.QGridLayout() - mainLayout.addWidget(formatLabel, 0, 0) - mainLayout.addWidget(self.formatComboBox, 0, 1) - mainLayout.addWidget(scopeLabel, 1, 0) - mainLayout.addWidget(self.scopeComboBox, 1, 1) - mainLayout.addWidget(organizationLabel, 2, 0) - mainLayout.addWidget(self.organizationComboBox, 2, 1) - mainLayout.addWidget(applicationLabel, 3, 0) - mainLayout.addWidget(self.applicationComboBox, 3, 1) - mainLayout.addWidget(self.locationsGroupBox, 4, 0, 1, 2) - mainLayout.addWidget(self.buttonBox, 5, 0, 1, 2) - self.setLayout(mainLayout) - - self.updateLocationsTable() + self.format_combo = QComboBox() + self.format_combo.addItem("Native") + self.format_combo.addItem("INI") + + self.scope_cCombo = QComboBox() + self.scope_cCombo.addItem("User") + self.scope_cCombo.addItem("System") + + self.organization_combo = QComboBox() + self.organization_combo.addItem("Trolltech") + self.organization_combo.setEditable(True) + + self.application_combo = QComboBox() + self.application_combo.addItem("Any") + self.application_combo.addItem("Application Example") + self.application_combo.addItem("Assistant") + self.application_combo.addItem("Designer") + self.application_combo.addItem("Linguist") + self.application_combo.setEditable(True) + self.application_combo.setCurrentIndex(3) + + format_label = QLabel("&Format:") + format_label.setBuddy(self.format_combo) + + scope_label = QLabel("&Scope:") + scope_label.setBuddy(self.scope_cCombo) + + organization_label = QLabel("&Organization:") + organization_label.setBuddy(self.organization_combo) + + application_label = QLabel("&Application:") + application_label.setBuddy(self.application_combo) + + self.locations_groupbox = QGroupBox("Setting Locations") + + self.locations_table = QTableWidget() + self.locations_table.setSelectionMode(QAbstractItemView.SingleSelection) + self.locations_table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.locations_table.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.locations_table.setColumnCount(2) + self.locations_table.setHorizontalHeaderLabels(("Location", "Access")) + self.locations_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) + self.locations_table.horizontalHeader().resizeSection(1, 180) + + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.format_combo.activated.connect(self.update_locations) + self.scope_cCombo.activated.connect(self.update_locations) + self.organization_combo.lineEdit().editingFinished.connect(self.update_locations) + self.application_combo.lineEdit().editingFinished.connect(self.update_locations) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + locations_layout = QVBoxLayout(self.locations_groupbox) + locations_layout.addWidget(self.locations_table) + + mainLayout = QGridLayout(self) + mainLayout.addWidget(format_label, 0, 0) + mainLayout.addWidget(self.format_combo, 0, 1) + mainLayout.addWidget(scope_label, 1, 0) + mainLayout.addWidget(self.scope_cCombo, 1, 1) + mainLayout.addWidget(organization_label, 2, 0) + mainLayout.addWidget(self.organization_combo, 2, 1) + mainLayout.addWidget(application_label, 3, 0) + mainLayout.addWidget(self.application_combo, 3, 1) + mainLayout.addWidget(self.locations_groupbox, 4, 0, 1, 2) + mainLayout.addWidget(self.button_box, 5, 0, 1, 2) + + self.update_locations() self.setWindowTitle("Open Application Settings") self.resize(650, 400) def format(self): - if self.formatComboBox.currentIndex() == 0: - return QtCore.QSettings.NativeFormat + if self.format_combo.currentIndex() == 0: + return QSettings.NativeFormat else: - return QtCore.QSettings.IniFormat + return QSettings.IniFormat def scope(self): - if self.scopeComboBox.currentIndex() == 0: - return QtCore.QSettings.UserScope + if self.scope_cCombo.currentIndex() == 0: + return QSettings.UserScope else: - return QtCore.QSettings.SystemScope + return QSettings.SystemScope def organization(self): - return self.organizationComboBox.currentText() + return self.organization_combo.currentText() def application(self): - if self.applicationComboBox.currentText() == "Any": + if self.application_combo.currentText() == "Any": return '' - return self.applicationComboBox.currentText() + return self.application_combo.currentText() - def updateLocationsTable(self): - self.locationsTable.setUpdatesEnabled(False) - self.locationsTable.setRowCount(0) + def update_locations(self): + self.locations_table.setUpdatesEnabled(False) + self.locations_table.setRowCount(0) for i in range(2): if i == 0: - if self.scope() == QtCore.QSettings.SystemScope: + if self.scope() == QSettings.SystemScope: continue - actualScope = QtCore.QSettings.UserScope + actualScope = QSettings.UserScope else: - actualScope = QtCore.QSettings.SystemScope + actualScope = QSettings.SystemScope for j in range(2): if j == 0: @@ -308,16 +428,16 @@ class LocationDialog(QtWidgets.QDialog): else: actualApplication = '' - settings = QtCore.QSettings(self.format(), actualScope, - self.organization(), actualApplication) + settings = QSettings(self.format(), actualScope, + self.organization(), actualApplication) - row = self.locationsTable.rowCount() - self.locationsTable.setRowCount(row + 1) + row = self.locations_table.rowCount() + self.locations_table.setRowCount(row + 1) - item0 = QtWidgets.QTableWidgetItem() + item0 = QTableWidgetItem() item0.setText(settings.fileName()) - item1 = QtWidgets.QTableWidgetItem() + item1 = QTableWidgetItem() disable = not (settings.childKeys() or settings.childGroups()) if row == 0: @@ -326,77 +446,79 @@ class LocationDialog(QtWidgets.QDialog): disable = False else: item1.setText("Read-only") - self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setDisabled(disable) + self.button_box.button(QDialogButtonBox.Ok).setDisabled(disable) else: item1.setText("Read-only fallback") if disable: - item0.setFlags(item0.flags() & ~QtCore.Qt.ItemIsEnabled) - item1.setFlags(item1.flags() & ~QtCore.Qt.ItemIsEnabled) + item0.setFlags(item0.flags() & ~Qt.ItemIsEnabled) + item1.setFlags(item1.flags() & ~Qt.ItemIsEnabled) - self.locationsTable.setItem(row, 0, item0) - self.locationsTable.setItem(row, 1, item1) + self.locations_table.setItem(row, 0, item0) + self.locations_table.setItem(row, 1, item1) - self.locationsTable.setUpdatesEnabled(True) + self.locations_table.setUpdatesEnabled(True) -class SettingsTree(QtWidgets.QTreeWidget): +class SettingsTree(QTreeWidget): def __init__(self, parent=None): super(SettingsTree, self).__init__(parent) - self.setItemDelegate(VariantDelegate(self)) + self._type_checker = TypeChecker() + self.setItemDelegate(VariantDelegate(self._type_checker, self)) self.setHeaderLabels(("Setting", "Type", "Value")) - self.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - self.header().setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) + self.header().setSectionResizeMode(0, QHeaderView.Stretch) + self.header().setSectionResizeMode(2, QHeaderView.Stretch) self.settings = None - self.refreshTimer = QtCore.QTimer() - self.refreshTimer.setInterval(2000) - self.autoRefresh = False - - self.groupIcon = QtGui.QIcon() - self.groupIcon.addPixmap(self.style().standardPixmap(QtWidgets.QStyle.SP_DirClosedIcon), - QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.groupIcon.addPixmap(self.style().standardPixmap(QtWidgets.QStyle.SP_DirOpenIcon), - QtGui.QIcon.Normal, QtGui.QIcon.On) - self.keyIcon = QtGui.QIcon() - self.keyIcon.addPixmap(self.style().standardPixmap(QtWidgets.QStyle.SP_FileIcon)) - - self.refreshTimer.timeout.connect(self.maybeRefresh) - - def setSettingsObject(self, settings): + self.refresh_timer = QTimer() + self.refresh_timer.setInterval(2000) + self.auto_refresh = False + + self.group_icon = QIcon() + style = self.style() + self.group_icon.addPixmap(style.standardPixmap(QStyle.SP_DirClosedIcon), + QIcon.Normal, QIcon.Off) + self.group_icon.addPixmap(style.standardPixmap(QStyle.SP_DirOpenIcon), + QIcon.Normal, QIcon.On) + self.key_icon = QIcon() + self.key_icon.addPixmap(style.standardPixmap(QStyle.SP_FileIcon)) + + self.refresh_timer.timeout.connect(self.maybe_refresh) + + def set_settings_object(self, settings): self.settings = settings self.clear() if self.settings is not None: self.settings.setParent(self) self.refresh() - if self.autoRefresh: - self.refreshTimer.start() + if self.auto_refresh: + self.refresh_timer.start() else: - self.refreshTimer.stop() + self.refresh_timer.stop() def sizeHint(self): - return QtCore.QSize(800, 600) + return QSize(800, 600) - def setAutoRefresh(self, autoRefresh): - self.autoRefresh = autoRefresh + def set_auto_refresh(self, autoRefresh): + self.auto_refresh = autoRefresh if self.settings is not None: - if self.autoRefresh: - self.maybeRefresh() - self.refreshTimer.start() + if self.auto_refresh: + self.maybe_refresh() + self.refresh_timer.start() else: - self.refreshTimer.stop() + self.refresh_timer.stop() - def setFallbacksEnabled(self, enabled): + def set_fallbacks_enabled(self, enabled): if self.settings is not None: self.settings.setFallbacksEnabled(enabled) self.refresh() - def maybeRefresh(self): - if self.state() != QtWidgets.QAbstractItemView.EditingState: + def maybe_refresh(self): + if self.state() != QAbstractItemView.EditingState: self.refresh() def refresh(self): @@ -405,23 +527,23 @@ class SettingsTree(QtWidgets.QTreeWidget): # The signal might not be connected. try: - self.itemChanged.disconnect(self.updateSetting) + self.itemChanged.disconnect(self.update_setting) except: pass self.settings.sync() - self.updateChildItems(None) + self.update_child_items(None) - self.itemChanged.connect(self.updateSetting) + self.itemChanged.connect(self.update_setting) def event(self, event): - if event.type() == QtCore.QEvent.WindowActivate: - if self.isActiveWindow() and self.autoRefresh: - self.maybeRefresh() + if event.type() == QEvent.WindowActivate: + if self.isActiveWindow() and self.auto_refresh: + self.maybe_refresh() return super(SettingsTree, self).event(event) - def updateSetting(self, item): + def update_setting(self, item): key = item.text(0) ancestor = item.parent() @@ -429,154 +551,121 @@ class SettingsTree(QtWidgets.QTreeWidget): key = ancestor.text(0) + '/' + key ancestor = ancestor.parent() - d = item.data(2, QtCore.Qt.UserRole) - self.settings.setValue(key, item.data(2, QtCore.Qt.UserRole)) + d = item.data(2, Qt.UserRole) + self.settings.setValue(key, item.data(2, Qt.UserRole)) - if self.autoRefresh: + if self.auto_refresh: self.refresh() - def updateChildItems(self, parent): - dividerIndex = 0 + def update_child_items(self, parent): + divider_index = 0 for group in self.settings.childGroups(): - childIndex = self.findChild(parent, group, dividerIndex) - if childIndex != -1: - child = self.childAt(parent, childIndex) + child_index = self.find_child(parent, group, divider_index) + if child_index != -1: + child = self.child_at(parent, child_index) child.setText(1, '') child.setText(2, '') - child.setData(2, QtCore.Qt.UserRole, None) - self.moveItemForward(parent, childIndex, dividerIndex) + child.setData(2, Qt.UserRole, None) + self.move_item_forward(parent, child_index, divider_index) else: - child = self.createItem(group, parent, dividerIndex) + child = self.create_item(group, parent, divider_index) - child.setIcon(0, self.groupIcon) - dividerIndex += 1 + child.setIcon(0, self.group_icon) + divider_index += 1 self.settings.beginGroup(group) - self.updateChildItems(child) + self.update_child_items(child) self.settings.endGroup() for key in self.settings.childKeys(): - childIndex = self.findChild(parent, key, 0) - if childIndex == -1 or childIndex >= dividerIndex: - if childIndex != -1: - child = self.childAt(parent, childIndex) + child_index = self.find_child(parent, key, 0) + if child_index == -1 or child_index >= divider_index: + if child_index != -1: + child = self.child_at(parent, child_index) for i in range(child.childCount()): - self.deleteItem(child, i) - self.moveItemForward(parent, childIndex, dividerIndex) + self.delete_item(child, i) + self.move_item_forward(parent, child_index, divider_index) else: - child = self.createItem(key, parent, dividerIndex) - child.setIcon(0, self.keyIcon) - dividerIndex += 1 + child = self.create_item(key, parent, divider_index) + child.setIcon(0, self.key_icon) + divider_index += 1 else: - child = self.childAt(parent, childIndex) + child = self.child_at(parent, child_index) value = self.settings.value(key) if value is None: child.setText(1, 'Invalid') else: + # Try to convert to type unless a QByteArray is received + if isinstance(value, str): + value_type = self._type_checker.type_from_text(value) + if value_type: + value = self.settings.value(key, type=value_type) child.setText(1, value.__class__.__name__) child.setText(2, VariantDelegate.displayText(value)) - child.setData(2, QtCore.Qt.UserRole, value) + child.setData(2, Qt.UserRole, value) - while dividerIndex < self.childCount(parent): - self.deleteItem(parent, dividerIndex) + while divider_index < self.child_count(parent): + self.delete_item(parent, divider_index) - def createItem(self, text, parent, index): + def create_item(self, text, parent, index): after = None if index != 0: - after = self.childAt(parent, index - 1) + after = self.child_at(parent, index - 1) if parent is not None: - item = QtWidgets.QTreeWidgetItem(parent, after) + item = QTreeWidgetItem(parent, after) else: - item = QtWidgets.QTreeWidgetItem(self, after) + item = QTreeWidgetItem(self, after) item.setText(0, text) - item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) + item.setFlags(item.flags() | Qt.ItemIsEditable) return item - def deleteItem(self, parent, index): + def delete_item(self, parent, index): if parent is not None: item = parent.takeChild(index) else: item = self.takeTopLevelItem(index) del item - def childAt(self, parent, index): + def child_at(self, parent, index): if parent is not None: return parent.child(index) else: return self.topLevelItem(index) - def childCount(self, parent): + def child_count(self, parent): if parent is not None: return parent.childCount() else: return self.topLevelItemCount() - def findChild(self, parent, text, startIndex): - for i in range(self.childCount(parent)): - if self.childAt(parent, i).text(0) == text: + def find_child(self, parent, text, startIndex): + for i in range(self.child_count(parent)): + if self.child_at(parent, i).text(0) == text: return i return -1 - def moveItemForward(self, parent, oldIndex, newIndex): + def move_item_forward(self, parent, oldIndex, newIndex): for int in range(oldIndex - newIndex): - self.deleteItem(parent, newIndex) + self.delete_item(parent, newIndex) -class VariantDelegate(QtWidgets.QItemDelegate): - def __init__(self, parent=None): +class VariantDelegate(QItemDelegate): + def __init__(self, type_checker, parent=None): super(VariantDelegate, self).__init__(parent) - - self.boolExp = QtCore.QRegExp() - self.boolExp.setPattern('true|false') - self.boolExp.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - - self.byteArrayExp = QtCore.QRegExp() - self.byteArrayExp.setPattern('[\\x00-\\xff]*') - - self.charExp = QtCore.QRegExp() - self.charExp.setPattern('.') - - self.colorExp = QtCore.QRegExp() - self.colorExp.setPattern('\\(([0-9]*),([0-9]*),([0-9]*),([0-9]*)\\)') - - self.doubleExp = QtCore.QRegExp() - self.doubleExp.setPattern('') - - self.pointExp = QtCore.QRegExp() - self.pointExp.setPattern('\\((-?[0-9]*),(-?[0-9]*)\\)') - - self.rectExp = QtCore.QRegExp() - self.rectExp.setPattern('\\((-?[0-9]*),(-?[0-9]*),(-?[0-9]*),(-?[0-9]*)\\)') - - self.signedIntegerExp = QtCore.QRegExp() - self.signedIntegerExp.setPattern('-?[0-9]*') - - self.sizeExp = QtCore.QRegExp(self.pointExp) - - self.unsignedIntegerExp = QtCore.QRegExp() - self.unsignedIntegerExp.setPattern('[0-9]*') - - self.dateExp = QtCore.QRegExp() - self.dateExp.setPattern('([0-9]{,4})-([0-9]{,2})-([0-9]{,2})') - - self.timeExp = QtCore.QRegExp() - self.timeExp.setPattern('([0-9]{,2}):([0-9]{,2}):([0-9]{,2})') - - self.dateTimeExp = QtCore.QRegExp() - self.dateTimeExp.setPattern(self.dateExp.pattern() + 'T' + self.timeExp.pattern()) + self._type_checker = type_checker def paint(self, painter, option, index): if index.column() == 2: - value = index.model().data(index, QtCore.Qt.UserRole) - if not self.isSupportedType(value): - myOption = QtWidgets.QStyleOptionViewItem(option) - myOption.state &= ~QtWidgets.QStyle.State_Enabled - super(VariantDelegate, self).paint(painter, myOption, index) + value = index.model().data(index, Qt.UserRole) + if not self.is_supported_type(value): + my_option = QStyleOptionViewItem(option) + my_option.state &= ~QStyle.State_Enabled + super(VariantDelegate, self).paint(painter, my_option, index) return super(VariantDelegate, self).paint(painter, option, index) @@ -585,137 +674,97 @@ class VariantDelegate(QtWidgets.QItemDelegate): if index.column() != 2: return None - originalValue = index.model().data(index, QtCore.Qt.UserRole) - if not self.isSupportedType(originalValue): + original_value = index.model().data(index, Qt.UserRole) + if not self.is_supported_type(original_value): return None - lineEdit = QtWidgets.QLineEdit(parent) - lineEdit.setFrame(False) - - if isinstance(originalValue, bool): - regExp = self.boolExp - elif isinstance(originalValue, float): - regExp = self.doubleExp - elif isinstance(originalValue, int): - regExp = self.signedIntegerExp - elif isinstance(originalValue, QtCore.QByteArray): - regExp = self.byteArrayExp - elif isinstance(originalValue, QtGui.QColor): - regExp = self.colorExp - elif isinstance(originalValue, QtCore.QDate): - regExp = self.dateExp - elif isinstance(originalValue, QtCore.QDateTime): - regExp = self.dateTimeExp - elif isinstance(originalValue, QtCore.QTime): - regExp = self.timeExp - elif isinstance(originalValue, QtCore.QPoint): - regExp = self.pointExp - elif isinstance(originalValue, QtCore.QRect): - regExp = self.rectExp - elif isinstance(originalValue, QtCore.QSize): - regExp = self.sizeExp + editor = None + if isinstance(original_value, bool): + editor = QCheckBox(parent) + if isinstance(original_value, int): + editor = QSpinBox(parent) + editor.setRange(-32767, 32767) else: - regExp = QtCore.QRegExp() - - if not regExp.isEmpty(): - validator = QtGui.QRegExpValidator(regExp, lineEdit) - lineEdit.setValidator(validator) - - return lineEdit + editor = QLineEdit(parent) + editor.setFrame(False) + validator = self._type_checker.create_validator(original_value, editor) + if validator: + editor.setValidator(validator) + return editor def setEditorData(self, editor, index): - value = index.model().data(index, QtCore.Qt.UserRole) - if editor is not None: - editor.setText(self.displayText(value)) - - def setModelData(self, editor, model, index): - if not editor.isModified(): + if not editor: return + value = index.model().data(index, Qt.UserRole) + if isinstance(editor, QCheckBox): + editor.setCheckState(Qt.Checked if value else Qt.Unchecked) + elif isinstance(editor, QSpinBox): + editor.setValue(value) + else: + editor.setText(self.displayText(value)) - text = editor.text() - validator = editor.validator() + def value_from_lineedit(self, lineedit, model, index): + if not lineedit.isModified(): + return None + text = lineedit.text() + validator = lineedit.validator() if validator is not None: state, text, _ = validator.validate(text, 0) - if state != QtGui.QValidator.Acceptable: - return + if state != QValidator.Acceptable: + return None + original_value = index.model().data(index, Qt.UserRole) + return self._type_checker.from_string(text, original_value) - originalValue = index.model().data(index, QtCore.Qt.UserRole) - - if isinstance(originalValue, QtGui.QColor): - self.colorExp.exactMatch(text) - value = QtGui.QColor(min(int(self.colorExp.cap(1)), 255), - min(int(self.colorExp.cap(2)), 255), - min(int(self.colorExp.cap(3)), 255), - min(int(self.colorExp.cap(4)), 255)) - elif isinstance(originalValue, QtCore.QDate): - value = QtCore.QDate.fromString(text, QtCore.Qt.ISODate) - if not value.isValid(): - return - elif isinstance(originalValue, QtCore.QDateTime): - value = QtCore.QDateTime.fromString(text, QtCore.Qt.ISODate) - if not value.isValid(): - return - elif isinstance(originalValue, QtCore.QTime): - value = QtCore.QTime.fromString(text, QtCore.Qt.ISODate) - if not value.isValid(): - return - elif isinstance(originalValue, QtCore.QPoint): - self.pointExp.exactMatch(text) - value = QtCore.QPoint(int(self.pointExp.cap(1)), - int(self.pointExp.cap(2))) - elif isinstance(originalValue, QtCore.QRect): - self.rectExp.exactMatch(text) - value = QtCore.QRect(int(self.rectExp.cap(1)), - int(self.rectExp.cap(2)), - int(self.rectExp.cap(3)), - int(self.rectExp.cap(4))) - elif isinstance(originalValue, QtCore.QSize): - self.sizeExp.exactMatch(text) - value = QtCore.QSize(int(self.sizeExp.cap(1)), - int(self.sizeExp.cap(2))) - elif isinstance(originalValue, list): - value = text.split(',') + def setModelData(self, editor, model, index): + value = None + if isinstance(editor, QCheckBox): + value = editor.checkState() == Qt.Checked + elif isinstance(editor, QSpinBox): + value = editor.value() else: - value = type(originalValue)(text) - - model.setData(index, self.displayText(value), QtCore.Qt.DisplayRole) - model.setData(index, value, QtCore.Qt.UserRole) + value = self.value_from_lineedit(editor, model, index) + if not value is None: + model.setData(index, value, Qt.UserRole) + model.setData(index, self.displayText(value), Qt.DisplayRole) @staticmethod - def isSupportedType(value): - return isinstance(value, (bool, float, int, QtCore.QByteArray, - str, QtGui.QColor, QtCore.QDate, QtCore.QDateTime, - QtCore.QTime, QtCore.QPoint, QtCore.QRect, QtCore.QSize, - list)) + def is_supported_type(value): + return isinstance(value, (bool, float, int, QByteArray, str, QColor, + QDate, QDateTime, QTime, QPoint, QRect, + QSize, list)) @staticmethod def displayText(value): - if isinstance(value, (bool, int, QtCore.QByteArray)): - return str(value) if isinstance(value, str): return value - elif isinstance(value, float): - return '%g' % value - elif isinstance(value, QtGui.QColor): - return '(%u,%u,%u,%u)' % (value.red(), value.green(), value.blue(), value.alpha()) - elif isinstance(value, (QtCore.QDate, QtCore.QDateTime, QtCore.QTime)): - return value.toString(QtCore.Qt.ISODate) - elif isinstance(value, QtCore.QPoint): - return '(%d,%d)' % (value.x(), value.y()) - elif isinstance(value, QtCore.QRect): - return '(%d,%d,%d,%d)' % (value.x(), value.y(), value.width(), value.height()) - elif isinstance(value, QtCore.QSize): - return '(%d,%d)' % (value.width(), value.height()) - elif isinstance(value, list): + if isinstance(value, bool): + return '✓' if value else '☐' + if isinstance(value, (int, float, QByteArray)): + return str(value) + if isinstance(value, QColor): + return '({},{},{},{})'.format(value.red(), value.green(), + value.blue(), value.alpha()) + if isinstance(value, (QDate, QDateTime, QTime)): + return value.toString(Qt.ISODate) + if isinstance(value, QPoint): + return '({},{})'.format(value.x(), value.y()) + if isinstance(value, QRect): + return '({},{},{},{})'.format(value.x(), value.y(), value.width(), + value.height()) + if isinstance(value, QSize): + return '({},{})'.format(value.width(), value.height()) + if isinstance(value, list): return ','.join(value) - elif value is None: + if value is None: return '' - return '<%s>' % value + return '<{}>'.format(value) if __name__ == '__main__': - app = QtWidgets.QApplication(sys.argv) - mainWin = MainWindow() - mainWin.show() + app = QApplication(sys.argv) + main_win = MainWindow() + if len(sys.argv) > 1: + main_win.load_ini_file(sys.argv[1]) + main_win.show() sys.exit(app.exec_()) -- cgit v1.2.3 From 7211180820d76814c7060d39d7ce0e9902865e14 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Thu, 9 Jul 2020 14:04:54 +0200 Subject: shiboken2: Re-add support for parsing Q_PROPERTY Following how qdoc does it, define Q_PROPERTY as a static assert with the stringified macro content in a ','-operator and parse it with clang. Task-number: PYSIDE-1019 Change-Id: Idcf53f1cd1c1cb29f4320444f446e9abad33d251 Reviewed-by: Qt CI Bot Reviewed-by: Christian Tismer --- sources/pyside2/PySide2/global.h.in | 7 +++++ .../shiboken2/ApiExtractor/abstractmetalang.cpp | 34 ++++++++++++++++++++++ sources/shiboken2/ApiExtractor/abstractmetalang.h | 8 +++++ .../ApiExtractor/clangparser/clangbuilder.cpp | 13 +++++++++ .../shiboken2/ApiExtractor/parser/codemodel.cpp | 2 ++ 5 files changed, 64 insertions(+) diff --git a/sources/pyside2/PySide2/global.h.in b/sources/pyside2/PySide2/global.h.in index ae1b103f5..a23b0f332 100644 --- a/sources/pyside2/PySide2/global.h.in +++ b/sources/pyside2/PySide2/global.h.in @@ -40,6 +40,13 @@ // Make "signals:", "slots:" visible as access specifiers #define QT_ANNOTATE_ACCESS_SPECIFIER(a) __attribute__((annotate(#a))) +// Q_PROPERTY is defined as class annotation which does not work since a +// sequence of properties will to expand to a sequence of annotations +// annotating nothing, causing clang to complain. Instead, define it away in a +// static assert with the stringified argument in a ','-operator (cf qdoc). + +#define QT_ANNOTATE_CLASS(type,...) static_assert(sizeof(#__VA_ARGS__),#type); + #include #if @ENABLE_X11@ diff --git a/sources/shiboken2/ApiExtractor/abstractmetalang.cpp b/sources/shiboken2/ApiExtractor/abstractmetalang.cpp index e9a2c2b57..a202c42d5 100644 --- a/sources/shiboken2/ApiExtractor/abstractmetalang.cpp +++ b/sources/shiboken2/ApiExtractor/abstractmetalang.cpp @@ -2619,6 +2619,15 @@ void AbstractMetaClass::format(QDebug &d) const d << (i ? ',' : '<') << instantiatedTypes.at(i)->name(); d << ">\""; } + if (const int count = m_propertySpecs.size()) { + d << ", properties (" << count << "): ["; + for (int i = 0; i < count; ++i) { + if (i) + d << ", "; + m_propertySpecs.at(i)->formatDebug(d); + } + d << ']'; + } } void AbstractMetaClass::formatMembers(QDebug &d) const @@ -2725,3 +2734,28 @@ QString AbstractMetaEnum::package() const { return m_typeEntry->targetLangPackage(); } + +#ifndef QT_NO_DEBUG_STREAM +void QPropertySpec::formatDebug(QDebug &d) const +{ + d << '#' << m_index << " \"" << m_name << "\" (" << m_type->qualifiedCppName() + << "), read=" << m_read; + if (!m_write.isEmpty()) + d << ", write=" << m_write; + if (!m_reset.isEmpty()) + d << ", reset=" << m_reset; + if (!m_designable.isEmpty()) + d << ", esignable=" << m_designable; +} + +QDebug operator<<(QDebug d, const QPropertySpec &p) +{ + QDebugStateSaver s(d); + d.noquote(); + d.nospace(); + d << "QPropertySpec("; + p.formatDebug(d); + d << ')'; + return d; +} +#endif // QT_NO_DEBUG_STREAM diff --git a/sources/shiboken2/ApiExtractor/abstractmetalang.h b/sources/shiboken2/ApiExtractor/abstractmetalang.h index 830631e68..8169c4a30 100644 --- a/sources/shiboken2/ApiExtractor/abstractmetalang.h +++ b/sources/shiboken2/ApiExtractor/abstractmetalang.h @@ -1807,6 +1807,10 @@ public: m_index = index; } +#ifndef QT_NO_DEBUG_STREAM + void formatDebug(QDebug &d) const; +#endif + private: QString m_name; QString m_read; @@ -1817,4 +1821,8 @@ private: int m_index = -1; }; +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QPropertySpec &p); +#endif + #endif // ABSTRACTMETALANG_H diff --git a/sources/shiboken2/ApiExtractor/clangparser/clangbuilder.cpp b/sources/shiboken2/ApiExtractor/clangparser/clangbuilder.cpp index 263c0a0bb..d08720934 100644 --- a/sources/shiboken2/ApiExtractor/clangparser/clangbuilder.cpp +++ b/sources/shiboken2/ApiExtractor/clangparser/clangbuilder.cpp @@ -1117,6 +1117,19 @@ BaseVisitor::StartTokenResult Builder::startToken(const CXCursor &cursor) if (!d->m_currentFunction.isNull()) d->m_currentFunction->setOverride(true); break; + case CXCursor_StaticAssert: + // Check for Q_PROPERTY() (see PySide2/global.h.in for an explanation + // how it is defined, and qdoc). + if (clang_isDeclaration(cursor.kind) && !d->m_currentClass.isNull()) { + auto snippet = getCodeSnippet(cursor); + const auto length = snippet.second - snippet.first; + if (length > 12 && *(snippet.second - 1) == ')' + && std::strncmp(snippet.first, "Q_PROPERTY(", 11) == 0) { + const QString qProperty = QString::fromUtf8(snippet.first + 11, length - 12); + d->m_currentClass->addPropertyDeclaration(qProperty); + } + } + break; default: break; } diff --git a/sources/shiboken2/ApiExtractor/parser/codemodel.cpp b/sources/shiboken2/ApiExtractor/parser/codemodel.cpp index e5a6e074c..3b9521e82 100644 --- a/sources/shiboken2/ApiExtractor/parser/codemodel.cpp +++ b/sources/shiboken2/ApiExtractor/parser/codemodel.cpp @@ -789,6 +789,8 @@ void _ClassModelItem::formatDebug(QDebug &d) const } formatModelItemList(d, ", templateParameters=", m_templateParameters); formatScopeItemsDebug(d); + if (!m_propertyDeclarations.isEmpty()) + d << ", Properties=" << m_propertyDeclarations; } #endif // !QT_NO_DEBUG_STREAM -- cgit v1.2.3