diff options
author | Christian Tismer <tismer@stackless.com> | 2020-07-15 15:39:48 +0200 |
---|---|---|
committer | Christian Tismer <tismer@stackless.com> | 2020-07-24 01:19:21 +0200 |
commit | 2d44c85faa01c7e805ff27bac4e3e1574ab0f5d3 (patch) | |
tree | c0a5359f6a6d001aac596af5b3e520802beef114 /sources/pyside2 | |
parent | b429d2a06bf5acb4b44cd0c7bd599c6d4cc7ebae (diff) |
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 <tismer@stackless.com>
Diffstat (limited to 'sources/pyside2')
-rw-r--r-- | sources/pyside2/doc/tutorials/expenses/main_snake_case.py | 209 | ||||
-rw-r--r-- | sources/pyside2/libpyside/feature_select.cpp | 133 | ||||
-rw-r--r-- | sources/pyside2/libpyside/feature_select.h | 6 | ||||
-rw-r--r-- | sources/pyside2/libpyside/pyside.cpp | 2 | ||||
-rw-r--r-- | sources/pyside2/tests/QtCore/CMakeLists.txt | 3 | ||||
-rw-r--r-- | sources/pyside2/tests/QtCore/multiple_feature_test.py (renamed from sources/pyside2/tests/QtCore/feature_test.py) | 36 | ||||
-rw-r--r-- | sources/pyside2/tests/QtCore/snake_case_feature_test.py | 86 |
7 files changed, 450 insertions, 25 deletions
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<PyTypeObject *>(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<PyTypeObject *>(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<PyObject *>(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 <sbkpython.h> 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/multiple_feature_test.py index cf1e8c3f2..26488326c 100644 --- a/sources/pyside2/tests/QtCore/feature_test.py +++ b/sources/pyside2/tests/QtCore/multiple_feature_test.py @@ -37,6 +37,8 @@ ## ############################################################################# +from __future__ import print_function, absolute_import + import os import sys import unittest @@ -50,12 +52,13 @@ from PySide2.support.__feature__ import _really_all_feature_names from textwrap import dedent """ -feature_test.py --------------- +multiple_feature_test.py +------------------------ This tests the selectable features in PySide. -There are no real features implemented. They will be added, later. +The first feature is `snake_case` instead of `camelCase`. +There is much more to come. """ class FeaturesTest(unittest.TestCase): @@ -66,9 +69,27 @@ class FeaturesTest(unittest.TestCase): """ global __name__ - for bit in range(8): + 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. - exec(dedent(""" + + eval(compile(dedent(""" def tst_bit{0}(flag, self): if flag == 0: @@ -80,7 +101,8 @@ class FeaturesTest(unittest.TestCase): QtCore.QCborArray.fake_feature_{1:02x} QtCore.QCborArray.__dict__["fake_feature_{1:02x}"] - """.format(bit, 1 << bit)), globals(), globals()) + """).format(bit, 1 << bit), "<string>", "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] @@ -95,7 +117,7 @@ class FeaturesTest(unittest.TestCase): feature = feature_list[bit] text = "from __feature__ import {}".format(feature) print(text) - exec(text) + eval(compile(text, "<string>", "exec"), globals(), edict) for bit in range(8): value = idx & 1 << bit func_list[bit](value, self=self) 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() |