From 2d44c85faa01c7e805ff27bac4e3e1574ab0f5d3 Mon Sep 17 00:00:00 2001 From: Christian Tismer Date: Wed, 15 Jul 2020 15:39:48 +0200 Subject: feature-select: allow snake_case instead of camelCase for methods This is the implementation of the first of a series of dynamically selectable features. The decision depends of the following setting at the beginning of a module after PySide2 import: from __feature__ import snake_case For more info, see the Jira issue, section The Principle Of Selectable Features In PySide The crucial problems that are now solved were: - it is not sufficient to patch a type dict, instead the whole `tp_mro` must be walked to rename everything. - tp_getattro must be changed for every existing type. This is done either in shiboken by a changed PyObject_GenericGetAttr or PyObject_SenericGetAttr, or in the generated tp_(get|set)attro functions. An example is included in sources/pyside2/doc/tutorial/expenses. Task-number: PYSIDE-1019 Change-Id: I5f103190be2c884b0b4ad806187f3fef8e6598c9 Reviewed-by: Christian Tismer --- .../doc/tutorials/expenses/main_snake_case.py | 209 +++++++++++++++++++++ sources/pyside2/libpyside/feature_select.cpp | 133 +++++++++++-- sources/pyside2/libpyside/feature_select.h | 6 +- sources/pyside2/libpyside/pyside.cpp | 2 +- sources/pyside2/tests/QtCore/CMakeLists.txt | 3 +- sources/pyside2/tests/QtCore/feature_test.py | 105 ----------- .../pyside2/tests/QtCore/multiple_feature_test.py | 127 +++++++++++++ .../tests/QtCore/snake_case_feature_test.py | 86 +++++++++ .../shiboken2/generator/shiboken2/cppgenerator.cpp | 16 +- sources/shiboken2/libshiboken/basewrapper.cpp | 104 +++++++--- sources/shiboken2/libshiboken/basewrapper.h | 5 + sources/shiboken2/libshiboken/bindingmanager.cpp | 28 ++- sources/shiboken2/libshiboken/bindingmanager.h | 2 +- sources/shiboken2/libshiboken/pep384impl.cpp | 3 + sources/shiboken2/libshiboken/pep384impl.h | 2 +- sources/shiboken2/libshiboken/sbkstring.cpp | 44 +++++ sources/shiboken2/libshiboken/sbkstring.h | 1 + .../files.dir/shibokensupport/__feature__.py | 7 +- 18 files changed, 718 insertions(+), 165 deletions(-) create mode 100644 sources/pyside2/doc/tutorials/expenses/main_snake_case.py delete mode 100644 sources/pyside2/tests/QtCore/feature_test.py create mode 100644 sources/pyside2/tests/QtCore/multiple_feature_test.py create mode 100644 sources/pyside2/tests/QtCore/snake_case_feature_test.py diff --git a/sources/pyside2/doc/tutorials/expenses/main_snake_case.py b/sources/pyside2/doc/tutorials/expenses/main_snake_case.py new file mode 100644 index 000000000..154396b41 --- /dev/null +++ b/sources/pyside2/doc/tutorials/expenses/main_snake_case.py @@ -0,0 +1,209 @@ +############################################################################# +## +## 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. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import sys +from PySide2.QtCore import Qt, Slot +from PySide2.QtGui import QPainter +from PySide2.QtWidgets import (QAction, QApplication, QHeaderView, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QPushButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) +from PySide2.QtCharts import QtCharts + +from __feature__ import snake_case + + +class Widget(QWidget): + def __init__(self): + QWidget.__init__(self) + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.set_column_count(2) + self.table.set_horizontal_header_labels(["Description", "Price"]) + self.table.horizontal_header().set_section_resize_mode(QHeaderView.Stretch) + + # Chart + self.chart_view = QtCharts.QChartView() + self.chart_view.set_render_hint(QPainter.Antialiasing) + + # Right + self.description = QLineEdit() + self.price = QLineEdit() + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + self.quit = QPushButton("Quit") + self.plot = QPushButton("Plot") + + # Disabling 'Add' button + self.add.setEnabled(False) + + self.right = QVBoxLayout() + self.right.set_margin(10) + self.right.add_widget(QLabel("Description")) + self.right.add_widget(self.description) + self.right.add_widget(QLabel("Price")) + self.right.add_widget(self.price) + self.right.add_widget(self.add) + self.right.add_widget(self.plot) + self.right.add_widget(self.chart_view) + self.right.add_widget(self.clear) + self.right.add_widget(self.quit) + + # QWidget Layout + self.layout = QHBoxLayout() + + #self.table_view.setSizePolicy(size) + self.layout.add_widget(self.table) + self.layout.add_layout(self.right) + + # Set the layout to the QWidget + self.set_layout(self.layout) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.quit.clicked.connect(self.quit_application) + self.plot.clicked.connect(self.plot_data) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged[str].connect(self.check_disable) + self.price.textChanged[str].connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = self.price.text() + + self.table.insert_row(self.items) + description_item = QTableWidgetItem(des) + price_item = QTableWidgetItem("{:.2f}".format(float(price))) + price_item.set_text_alignment(Qt.AlignRight) + + self.table.set_item(self.items, 0, description_item) + self.table.set_item(self.items, 1, price_item) + + self.description.set_text("") + self.price.set_text("") + + self.items += 1 + + @Slot() + def check_disable(self, s): + if not self.description.text() or not self.price.text(): + self.add.set_enabled(False) + else: + self.add.set_enabled(True) + + @Slot() + def plot_data(self): + # Get table information + series = QtCharts.QPieSeries() + for i in range(self.table.row_count()): + text = self.table.item(i, 0).text() + number = float(self.table.item(i, 1).text()) + series.append(text, number) + + chart = QtCharts.QChart() + chart.add_series(series) + chart.legend().set_alignment(Qt.AlignLeft) + self.chart_view.set_chart(chart) + + @Slot() + def quit_application(self): + QApplication.quit() + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + description_item = QTableWidgetItem(desc) + price_item = QTableWidgetItem("{:.2f}".format(price)) + price_item.set_text_alignment(Qt.AlignRight) + self.table.insert_row(self.items) + self.table.set_item(self.items, 0, description_item) + self.table.set_item(self.items, 1, price_item) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.set_row_count(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + QMainWindow.__init__(self) + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menu_bar() + self.file_menu = self.menu.add_menu("File") + + # Exit QAction + exit_action = QAction("Exit", self) + exit_action.set_shortcut("Ctrl+Q") + exit_action.triggered.connect(self.exit_app) + + self.file_menu.add_action(exit_action) + self.set_central_widget(widget) + + @Slot() + def exit_app(self, checked): + QApplication.quit() + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec_()) diff --git a/sources/pyside2/libpyside/feature_select.cpp b/sources/pyside2/libpyside/feature_select.cpp index 6e5670d6d..7b98b03f0 100644 --- a/sources/pyside2/libpyside/feature_select.cpp +++ b/sources/pyside2/libpyside/feature_select.cpp @@ -111,12 +111,16 @@ 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. +Furthermore, we need to overwrite every `tp_getattro` and `tp_setattro` +with a version that switches dicts before looking up methods. +The dict changing must follow the `tp_mro` in order to change all names. + This is everything that the following code does. *****************************************************************************/ -namespace PySide { namespace FeatureSelector { +namespace PySide { namespace Feature { using namespace Shiboken; @@ -155,7 +159,6 @@ createDerivedDictType() return reinterpret_cast(ChameleonDict); } -static PyTypeObject *old_dict_type = Py_TYPE(PyType_Type.tp_dict); static PyTypeObject *new_dict_type = nullptr; static void ensureNewDictType() @@ -285,7 +288,7 @@ static bool createNewFeatureSet(PyTypeObject *type, PyObject *select_id) Py_INCREF(prev_dict); if (!addNewDict(type, select_id)) return false; - int id = PyInt_AsSsize_t(select_id); + auto id = PyInt_AsSsize_t(select_id); if (id == -1) return false; FeatureProc *proc = featurePointer; @@ -307,11 +310,30 @@ static bool createNewFeatureSet(PyTypeObject *type, PyObject *select_id) return true; } +static bool SelectFeatureSetSubtype(PyTypeObject *type, PyObject *select_id) +{ + if (Py_TYPE(type->tp_dict) == Py_TYPE(PyType_Type.tp_dict)) { + // PYSIDE-1019: On first touch, we initialize the dynamic naming. + // The dict type will be replaced after the first call. + if (!replaceClassDict(type)) { + Py_FatalError("failed to replace class dict!"); + return false; + } + } + if (!moveToFeatureSet(type, select_id)) { + if (!createNewFeatureSet(type, select_id)) { + Py_FatalError("failed to create a new feature set!"); + 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 + * Generated functions call this directly. * Shiboken will assign it via a public hook of `basewrapper.cpp`. */ if (Py_TYPE(type->tp_dict) == Py_TYPE(PyType_Type.tp_dict)) { @@ -323,16 +345,27 @@ static PyObject *SelectFeatureSet(PyTypeObject *type) 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; - } + PyObject *mro = type->tp_mro; + Py_ssize_t idx, n = PyTuple_GET_SIZE(mro); + // We leave 'Shiboken.Object' and 'object' alone, therefore "n - 2". + for (idx = 0; idx < n - 2; idx++) { + auto *sub_type = reinterpret_cast(PyTuple_GET_ITEM(mro, idx)); + // When any subtype is already resolved (false), we can stop. + if (!SelectFeatureSetSubtype(sub_type, select_id)) + break; + } } return type->tp_dict; } -static bool feature_01_addDummyNames(PyTypeObject *type, PyObject *prev_dict); +// For cppgenerator: +void Select(PyObject *obj) +{ + auto type = Py_TYPE(obj); + type->tp_dict = SelectFeatureSet(type); +} + +static bool feature_01_addLowerNames(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); @@ -342,7 +375,7 @@ 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_01_addLowerNames, feature_02_addDummyNames, feature_04_addDummyNames, feature_08_addDummyNames, @@ -363,7 +396,80 @@ void init() // // PYSIDE-1019: Support switchable extensions // -// Feature 0x01..0x80: A fake switchable option for testing +// Feature 0x01: Allow snake_case instead of camelCase +// +// 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. +// + +static PyObject *methodWithLowerName(PyTypeObject *type, + PyMethodDef *meth, + const char *new_name) +{ + /* + * Create a method with a lower case name. + */ + auto obtype = reinterpret_cast(type); + int len = strlen(new_name); + auto name = new char[len + 1]; + strcpy(name, new_name); + auto new_meth = new PyMethodDef; + new_meth->ml_name = name; + new_meth->ml_meth = meth->ml_meth; + new_meth->ml_flags = meth->ml_flags; + new_meth->ml_doc = meth->ml_doc; + PyObject *descr = nullptr; + if (new_meth->ml_flags & METH_STATIC) { + AutoDecRef cfunc(PyCFunction_NewEx(new_meth, obtype, nullptr)); + if (cfunc.isNull()) + return nullptr; + descr = PyStaticMethod_New(cfunc); + } + else { + descr = PyDescr_NewMethod(type, new_meth); + } + return descr; +} + +static bool feature_01_addLowerNames(PyTypeObject *type, PyObject *prev_dict) +{ + /* + * Add objects with lower names to `type->tp_dict` from 'prev_dict`. + */ + PyObject *lower_dict = type->tp_dict; + PyObject *key, *value; + Py_ssize_t pos = 0; + + // We first copy the things over which will not be changed: + while (PyDict_Next(prev_dict, &pos, &key, &value)) { + if ( Py_TYPE(value) != PepMethodDescr_TypePtr + && Py_TYPE(value) != PepStaticMethod_TypePtr) { + if (PyDict_SetItem(lower_dict, key, value)) + return false; + continue; + } + } + // Then we walk over the tp_methods to get all methods and insert + // them with changed names. + PyMethodDef *meth = type->tp_methods; + for (; meth != nullptr && meth->ml_name != nullptr; ++meth) { + const char *name = String::toCString(String::getSnakeCaseName(meth->ml_name, true)); + AutoDecRef new_method(methodWithLowerName(type, meth, name)); + if (new_method.isNull()) + return false; + if (PyDict_SetItemString(lower_dict, name, new_method) < 0) + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// +// PYSIDE-1019: Support switchable extensions +// +// Feature 0x02..0x80: A fake switchable option for testing // #define SIMILAR_FEATURE(xx) \ @@ -378,7 +484,6 @@ static bool feature_##xx##_addDummyNames(PyTypeObject *type, PyObject *prev_dict return true; \ } -SIMILAR_FEATURE(01) SIMILAR_FEATURE(02) SIMILAR_FEATURE(04) SIMILAR_FEATURE(08) @@ -388,4 +493,4 @@ SIMILAR_FEATURE(40) SIMILAR_FEATURE(80) } // namespace PySide -} // namespace FeatureSelector +} // namespace Feature diff --git a/sources/pyside2/libpyside/feature_select.h b/sources/pyside2/libpyside/feature_select.h index 68e29292d..32abffac6 100644 --- a/sources/pyside2/libpyside/feature_select.h +++ b/sources/pyside2/libpyside/feature_select.h @@ -41,13 +41,15 @@ #define FEATURE_SELECT_H #include "pysidemacros.h" +#include namespace PySide { -namespace FeatureSelector { +namespace Feature { PYSIDE_API void init(); +PYSIDE_API void Select(PyObject *obj); +} // namespace Feature } // namespace PySide -} // namespace FeatureSelector #endif // FEATURE_SELECT_H diff --git a/sources/pyside2/libpyside/pyside.cpp b/sources/pyside2/libpyside/pyside.cpp index 074fa764b..c1ddfc278 100644 --- a/sources/pyside2/libpyside/pyside.cpp +++ b/sources/pyside2/libpyside/pyside.cpp @@ -94,7 +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(); + Feature::init(); initQApp(); } diff --git a/sources/pyside2/tests/QtCore/CMakeLists.txt b/sources/pyside2/tests/QtCore/CMakeLists.txt index 771e1aeef..0c89f0d03 100644 --- a/sources/pyside2/tests/QtCore/CMakeLists.txt +++ b/sources/pyside2/tests/QtCore/CMakeLists.txt @@ -37,12 +37,12 @@ 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) PYSIDE_TEST(missing_symbols_test.py) PYSIDE_TEST(mockclass_test.py) +PYSIDE_TEST(multiple_feature_test.py) PYSIDE_TEST(python_conversion.py) PYSIDE_TEST(qabs_test.py) PYSIDE_TEST(qabstractitemmodel_test.py) @@ -128,6 +128,7 @@ PYSIDE_TEST(quuid_test.py) PYSIDE_TEST(qversionnumber_test.py) PYSIDE_TEST(repr_test.py) PYSIDE_TEST(setprop_on_ctor_test.py) +PYSIDE_TEST(snake_case_feature_test.py) PYSIDE_TEST(staticMetaObject_test.py) PYSIDE_TEST(static_method_test.py) PYSIDE_TEST(thread_signals_test.py) diff --git a/sources/pyside2/tests/QtCore/feature_test.py b/sources/pyside2/tests/QtCore/feature_test.py deleted file mode 100644 index cf1e8c3f2..000000000 --- a/sources/pyside2/tests/QtCore/feature_test.py +++ /dev/null @@ -1,105 +0,0 @@ -############################################################################# -## -## 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/pyside2/tests/QtCore/multiple_feature_test.py b/sources/pyside2/tests/QtCore/multiple_feature_test.py new file mode 100644 index 000000000..26488326c --- /dev/null +++ b/sources/pyside2/tests/QtCore/multiple_feature_test.py @@ -0,0 +1,127 @@ +############################################################################# +## +## 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 + +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 + +""" +multiple_feature_test.py +------------------------ + +This tests the selectable features in PySide. + +The first feature is `snake_case` instead of `camelCase`. +There is much more to come. +""" + +class FeaturesTest(unittest.TestCase): + + def testAllFeatureCombinations(self): + """ + Test for all 256 possible combinations of `__feature__` imports. + """ + global __name__ + + def tst_bit0(flag, self): + if flag == 0: + QtCore.QCborArray.isEmpty + QtCore.QCborArray.__dict__["isEmpty"] + with self.assertRaises(AttributeError): + QtCore.QCborArray.is_empty + with self.assertRaises(KeyError): + QtCore.QCborArray.__dict__["is_empty"] + else: + QtCore.QCborArray.is_empty + QtCore.QCborArray.__dict__["is_empty"] + with self.assertRaises(AttributeError): + QtCore.QCborArray.isEmpty + with self.assertRaises(KeyError): + QtCore.QCborArray.__dict__["isEmpty"] + + edict = {} + for bit in range(1, 8): + # We are cheating here, since the functions are in the globals. + + eval(compile(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), "", "exec"), globals(), edict) + globals().update(edict) + 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) + eval(compile(text, "", "exec"), globals(), edict) + for bit in range(8): + value = idx & 1 << bit + func_list[bit](value, self=self) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside2/tests/QtCore/snake_case_feature_test.py b/sources/pyside2/tests/QtCore/snake_case_feature_test.py new file mode 100644 index 000000000..b7f23396e --- /dev/null +++ b/sources/pyside2/tests/QtCore/snake_case_feature_test.py @@ -0,0 +1,86 @@ +############################################################################# +## +## 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 QtWidgets + +""" +snake_case_feature_test.py +-------------------------- + +Test the snake_case feature. + +This works now. More tests needed! +""" + +class RenamingTest(unittest.TestCase): + def setUp(self): + qApp or QtWidgets.QApplication() + + def tearDown(self): + qApp.shutdown() + + def testRenamedFunctions(self): + + class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + + window = Window() + window.setWindowTitle('camelCase') + + # and now the same with snake_case enabled + from __feature__ import snake_case + + class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + + window = Window() + window.set_window_title('snake_case') + +if __name__ == '__main__': + unittest.main() diff --git a/sources/shiboken2/generator/shiboken2/cppgenerator.cpp b/sources/shiboken2/generator/shiboken2/cppgenerator.cpp index 069431e59..b10074f79 100644 --- a/sources/shiboken2/generator/shiboken2/cppgenerator.cpp +++ b/sources/shiboken2/generator/shiboken2/cppgenerator.cpp @@ -313,6 +313,7 @@ void CppGenerator::generateClass(QTextStream &s, const GeneratorContext &classCo << "#include \n" << "#include \n" << "#include \n" + << "#include \n" << "#include \n\n" << "QT_WARNING_DISABLE_DEPRECATED\n\n"; } @@ -949,10 +950,10 @@ void CppGenerator::writeVirtualMethodNative(QTextStream &s, s << INDENT << returnStatement << '\n'; } - s << INDENT << "static PyObject *pyFuncName = Shiboken::String::createStaticString(\"" - << funcName << "\");\n"; + s << INDENT << "static PyObject *nameCache[2] = {};\n"; + s << INDENT << "static const char *funcName = \"" << funcName << "\";\n"; s << INDENT << "Shiboken::AutoDecRef " << PYTHON_OVERRIDE_VAR - << "(Shiboken::BindingManager::instance().getOverride(this, pyFuncName));\n"; + << "(Shiboken::BindingManager::instance().getOverride(this, nameCache, funcName));\n"; s << INDENT << "if (" << PYTHON_OVERRIDE_VAR << ".isNull()) {\n"; { Indentation indentation(INDENT); @@ -5371,6 +5372,11 @@ void CppGenerator::writeSetattroFunction(QTextStream &s, AttroCheck attroCheck, Q_ASSERT(!context.forSmartPointer()); const AbstractMetaClass *metaClass = context.metaClass(); writeSetattroDefinition(s, metaClass); + + // PYSIDE-1019: Switch tp_dict before doing tp_setattro. + if (usePySideExtensions()) + s << INDENT << "PySide::Feature::Select(self);\n"; + // PYSIDE-803: Detect duck-punching; clear cache if a method is set. if (attroCheck.testFlag(AttroCheckFlag::SetattroMethodOverride) && context.useWrapper()) { @@ -5458,6 +5464,10 @@ void CppGenerator::writeGetattroFunction(QTextStream &s, AttroCheck attroCheck, const AbstractMetaClass *metaClass = context.metaClass(); writeGetattroDefinition(s, metaClass); + // PYSIDE-1019: Switch tp_dict before doing tp_getattro. + if (usePySideExtensions()) + s << INDENT << "PySide::Feature::Select(self);\n"; + const QString getattrFunc = usePySideExtensions() && metaClass->isQObject() ? qObjectGetAttroFunction() : QLatin1String("PyObject_GenericGetAttr(self, name)"); diff --git a/sources/shiboken2/libshiboken/basewrapper.cpp b/sources/shiboken2/libshiboken/basewrapper.cpp index 5c2dc7807..a602688ec 100644 --- a/sources/shiboken2/libshiboken/basewrapper.cpp +++ b/sources/shiboken2/libshiboken/basewrapper.cpp @@ -97,21 +97,7 @@ static PyObject *SbkObjectTypeTpNew(PyTypeObject *metatype, PyObject *args, PyOb 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); -} +static PyObject *Sbk_TypeGet___dict__(PyTypeObject *type, void *context); // forward // 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 @@ -140,26 +126,12 @@ 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 PyObject *(*type_getattro)(PyObject *type, PyObject *name); // forward +static PyObject *mangled_type_getattro(PyTypeObject *type, PyObject *name); // forward 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)}, {Py_tp_new, reinterpret_cast(SbkObjectTypeTpNew)}, @@ -340,7 +312,12 @@ static int SbkObject_clear(PyObject *self) return 0; } +static PyObject *SbkObject_GenericGetAttr(PyObject *obj, PyObject *name); +static int SbkObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value); + static PyType_Slot SbkObject_Type_slots[] = { + {Py_tp_getattro, reinterpret_cast(SbkObject_GenericGetAttr)}, + {Py_tp_setattro, reinterpret_cast(SbkObject_GenericSetAttr)}, {Py_tp_dealloc, reinterpret_cast(SbkDeallocWrapperWithPrivateDtor)}, {Py_tp_traverse, reinterpret_cast(SbkObject_traverse)}, {Py_tp_clear, reinterpret_cast(SbkObject_clear)}, @@ -533,6 +510,71 @@ void SbkObjectTypeDealloc(PyObject *pyObj) } } +////////////////////////////////////////////////////////////////////////////// +// +// PYSIDE-1019: Support switchable extensions +// +// We simply exchange the complete class dicts. +// This is done in +// - mangled_type_getattro which replaces +// - Sbk_TypeGet___dict__ +// - SbkObjectType_replace_getattro +// - SbkObjectType_replace_setattro +// + +void initSelectableFeature(SelectableFeatureHook func) +{ + SelectFeatureSet = func; +} + +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 PyObject *Sbk_TypeGet___dict__(PyTypeObject *type, void *context) +{ + /* + * This is the override for getting a dict. + */ + auto dict = type->tp_dict; + if (dict == NULL) + Py_RETURN_NONE; + if (SelectFeatureSet != nullptr) + dict = SelectFeatureSet(type); + return PyDictProxy_New(dict); +} + +// These functions replace the standard PyObject_Generic(Get|Set)Attr functions. +// They provide the default that "object" inherits. +// Everything else is directly handled by an insertion PyObject_GenericGetAttr +static PyObject *SbkObject_GenericGetAttr(PyObject *obj, PyObject *name) +{ + auto type = Py_TYPE(obj); + if (SelectFeatureSet != nullptr) + type->tp_dict = SelectFeatureSet(type); + return PyObject_GenericGetAttr(obj, name); +} + +static int SbkObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value) +{ + auto type = Py_TYPE(obj); + if (SelectFeatureSet != nullptr) + type->tp_dict = SelectFeatureSet(type); + return PyObject_GenericSetAttr(obj, name, value); +} + +// +////////////////////////////////////////////////////////////////////////////// + static PyObject *SbkObjectTypeTpNew(PyTypeObject *metatype, PyObject *args, PyObject *kwds) { // Check if all bases are new style before calling type.tp_new diff --git a/sources/shiboken2/libshiboken/basewrapper.h b/sources/shiboken2/libshiboken/basewrapper.h index a4a8629fb..47ea89577 100644 --- a/sources/shiboken2/libshiboken/basewrapper.h +++ b/sources/shiboken2/libshiboken/basewrapper.h @@ -93,9 +93,14 @@ typedef void (*ObjectDestructor)(void *); typedef void (*SubTypeInitHook)(SbkObjectType *, PyObject *, PyObject *); +// PYSIDE-1019: Set the function to select the current feature. typedef PyObject *(*SelectableFeatureHook)(PyTypeObject *); LIBSHIBOKEN_API void initSelectableFeature(SelectableFeatureHook func); +// PYSIDE-1019: Publish the start of setattro. +LIBSHIBOKEN_API void SbkObject_NotifySetAttr(PyObject *obj, PyObject *name, PyObject *value); + + extern LIBSHIBOKEN_API PyTypeObject *SbkObjectType_TypeF(void); extern LIBSHIBOKEN_API SbkObjectType *SbkObject_TypeF(void); diff --git a/sources/shiboken2/libshiboken/bindingmanager.cpp b/sources/shiboken2/libshiboken/bindingmanager.cpp index 1c38da81c..b35a02ef4 100644 --- a/sources/shiboken2/libshiboken/bindingmanager.cpp +++ b/sources/shiboken2/libshiboken/bindingmanager.cpp @@ -44,6 +44,7 @@ #include "sbkdbg.h" #include "gilstate.h" #include "sbkstring.h" +#include "sbkstaticstrings.h" #include "debugfreehook.h" #include @@ -273,7 +274,19 @@ SbkObject *BindingManager::retrieveWrapper(const void *cptr) return iter->second; } -PyObject *BindingManager::getOverride(const void *cptr, PyObject *methodName) +static bool mangleNameFlag(PyTypeObject *type) +{ + // PYSIDE-1019: See if a dict is set with a snake_case bit. + static PyTypeObject *old_dict_type = Py_TYPE(PyType_Type.tp_dict); + auto dict = type->tp_dict; + if (Py_TYPE(dict) == old_dict_type) + return false; + Shiboken::AutoDecRef select_id(PyObject_GetAttr(dict, Shiboken::PyName::select_id())); + auto id = PyInt_AsSsize_t(select_id); + return (id & 1) != 0; +} + +PyObject *BindingManager::getOverride(const void *cptr, PyObject *methodNameCache[2], const char *methodName) { SbkObject *wrapper = retrieveWrapper(cptr); // The refcount can be 0 if the object is dieing and someone called @@ -281,15 +294,22 @@ PyObject *BindingManager::getOverride(const void *cptr, PyObject *methodName) if (!wrapper || reinterpret_cast(wrapper)->ob_refcnt == 0) return nullptr; + bool flag = mangleNameFlag(Py_TYPE(wrapper)); + PyObject *pyMethodName = methodNameCache[flag]; // borrowed + if (pyMethodName == nullptr) { + pyMethodName = Shiboken::String::getSnakeCaseName(methodName, flag); + methodNameCache[flag] = pyMethodName; + } + if (wrapper->ob_dict) { - PyObject *method = PyDict_GetItem(wrapper->ob_dict, methodName); + PyObject *method = PyDict_GetItem(wrapper->ob_dict, pyMethodName); if (method) { Py_INCREF(reinterpret_cast(method)); return method; } } - PyObject *method = PyObject_GetAttr(reinterpret_cast(wrapper), methodName); + PyObject *method = PyObject_GetAttr(reinterpret_cast(wrapper), pyMethodName); if (method && PyMethod_Check(method) && PyMethod_GET_SELF(method) == reinterpret_cast(wrapper)) { @@ -301,7 +321,7 @@ PyObject *BindingManager::getOverride(const void *cptr, PyObject *methodName) for (int i = 1; i < PyTuple_GET_SIZE(mro) - 1; i++) { auto *parent = reinterpret_cast(PyTuple_GET_ITEM(mro, i)); if (parent->tp_dict) { - defaultMethod = PyDict_GetItem(parent->tp_dict, methodName); + defaultMethod = PyDict_GetItem(parent->tp_dict, pyMethodName); if (defaultMethod && PyMethod_GET_FUNCTION(method) != defaultMethod) return method; } diff --git a/sources/shiboken2/libshiboken/bindingmanager.h b/sources/shiboken2/libshiboken/bindingmanager.h index 0bcde196f..8882f402e 100644 --- a/sources/shiboken2/libshiboken/bindingmanager.h +++ b/sources/shiboken2/libshiboken/bindingmanager.h @@ -73,7 +73,7 @@ public: void addToDeletionInMainThread(const DestructorEntry &); SbkObject *retrieveWrapper(const void *cptr); - PyObject *getOverride(const void *cptr, PyObject *methodName); + PyObject *getOverride(const void *cptr, PyObject *methodNameCache[2], const char *methodName); void addClassInheritance(SbkObjectType *parent, SbkObjectType *child); /** diff --git a/sources/shiboken2/libshiboken/pep384impl.cpp b/sources/shiboken2/libshiboken/pep384impl.cpp index 4149bbcbf..57d3de261 100644 --- a/sources/shiboken2/libshiboken/pep384impl.cpp +++ b/sources/shiboken2/libshiboken/pep384impl.cpp @@ -86,6 +86,7 @@ static PyMemberDef probe_members[] = { #define probe_tp_repr make_dummy(2) #define probe_tp_call make_dummy(3) #define probe_tp_getattro make_dummy(16) +#define probe_tp_setattro make_dummy(17) #define probe_tp_str make_dummy(4) #define probe_tp_traverse make_dummy(5) #define probe_tp_clear make_dummy(6) @@ -108,6 +109,7 @@ static PyType_Slot typeprobe_slots[] = { {Py_tp_repr, probe_tp_repr}, {Py_tp_call, probe_tp_call}, {Py_tp_getattro, probe_tp_getattro}, + {Py_tp_setattro, probe_tp_setattro}, {Py_tp_str, probe_tp_str}, {Py_tp_traverse, probe_tp_traverse}, {Py_tp_clear, probe_tp_clear}, @@ -153,6 +155,7 @@ check_PyTypeObject_valid() || probe_tp_repr != check->tp_repr || probe_tp_call != check->tp_call || probe_tp_getattro != check->tp_getattro + || probe_tp_setattro != check->tp_setattro || probe_tp_str != check->tp_str || probe_tp_traverse != check->tp_traverse || probe_tp_clear != check->tp_clear diff --git a/sources/shiboken2/libshiboken/pep384impl.h b/sources/shiboken2/libshiboken/pep384impl.h index 2a14d6543..973cf06ce 100644 --- a/sources/shiboken2/libshiboken/pep384impl.h +++ b/sources/shiboken2/libshiboken/pep384impl.h @@ -99,7 +99,7 @@ typedef struct _typeobject { ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; - void *X17; // setattrofunc tp_setattro; + setattrofunc tp_setattro; void *X18; // PyBufferProcs *tp_as_buffer; unsigned long tp_flags; void *X20; // const char *tp_doc; diff --git a/sources/shiboken2/libshiboken/sbkstring.cpp b/sources/shiboken2/libshiboken/sbkstring.cpp index 092745d3d..ed8b61fc8 100644 --- a/sources/shiboken2/libshiboken/sbkstring.cpp +++ b/sources/shiboken2/libshiboken/sbkstring.cpp @@ -271,5 +271,49 @@ PyObject *createStaticString(const char *str) return result; } +/////////////////////////////////////////////////////////////////////// +// +// PYSIDE-1019: Helper function for snake_case vs. camelCase names +// --------------------------------------------------------------- +// +// When renaming dict entries, `BindingManager::getOverride` must +// use adapted names. +// +// This might become more complex when we need to register +// exceptions from this rule. +// + +PyObject *getSnakeCaseName(const char *name, bool lower) +{ + /* + * Convert `camelCase` to `snake_case`. + * Gives up when there are two consecutive upper chars. + * + * Also functions beginning with `gl` followed by upper case stay + * unchanged since that are the special OpenGL functions. + */ + if (!lower + || strlen(name) < 3 + || (name[0] == 'g' && name[1] == 'l' && isupper(name[2]))) + return createStaticString(name); + + char new_name[200 + 1] = {}; + const char *p = name; + char *q = new_name; + for (; *p && q - new_name < 200; ++p, ++q) { + if (isupper(*p)) { + if (p != name && isupper(*(p - 1))) + return createStaticString(name); + *q = '_'; + ++q; + *q = tolower(*p); + } + else { + *q = *p; + } + } + return createStaticString(new_name); +} + } // namespace String } // namespace Shiboken diff --git a/sources/shiboken2/libshiboken/sbkstring.h b/sources/shiboken2/libshiboken/sbkstring.h index 84d7768c5..3475d3acd 100644 --- a/sources/shiboken2/libshiboken/sbkstring.h +++ b/sources/shiboken2/libshiboken/sbkstring.h @@ -61,6 +61,7 @@ namespace String LIBSHIBOKEN_API int compare(PyObject *val1, const char *val2); LIBSHIBOKEN_API Py_ssize_t len(PyObject *str); LIBSHIBOKEN_API PyObject *createStaticString(const char *str); + LIBSHIBOKEN_API PyObject *getSnakeCaseName(const char *name, bool lower); } // namespace String } // namespace Shiboken diff --git a/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py index 58c1ff925..d088ea41b 100644 --- a/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py +++ b/sources/shiboken2/shibokenmodule/files.dir/shibokensupport/__feature__.py @@ -49,7 +49,7 @@ similarity to Python's `__future__` file, but also some distinction. import sys all_feature_names = [ - "_dummy_feature_01", + "snake_case", "_dummy_feature_02", "_dummy_feature_04", "_dummy_feature_08", @@ -61,7 +61,7 @@ all_feature_names = [ __all__ = ["all_feature_names"] + all_feature_names -_dummy_feature_01 = 0x01 +snake_case = 1 _dummy_feature_02 = 0x02 _dummy_feature_04 = 0x04 _dummy_feature_08 = 0x08 @@ -107,5 +107,8 @@ def _import(name, *args, **kwargs): if isinstance(existing, int): flag |= existing & 255 pyside_feature_dict[importing_module] = flag + if importing_module == "__main__": + # We need to add all modules here which should see __feature__. + pyside_feature_dict["rlcompleter"] = flag return sys.modules["__feature__"] return original_import(name, *args, **kwargs) -- cgit v1.2.3