Skip to content

Commit

Permalink
uic: No longer generate star imports in Python
Browse files Browse the repository at this point in the history
Use class WriteIncludesBase and store classes encountered in a
per-module hash (Qt/custom widgets). Write out only the required
classes.

Add --star-import as a fallback should the change cause issues.

Task-number: PYSIDE-1404
Change-Id: Ic50e26758ddd0f2f8aebbce470d32a36fb09a2c4
Reviewed-by: Qt CI Bot <[email protected]>
Reviewed-by: Cristian Maureira-Fredes <[email protected]>
  • Loading branch information
FriedemannKleint committed Jun 1, 2021
1 parent b42e2d7 commit de15836
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 63 deletions.
6 changes: 6 additions & 0 deletions src/tools/uic/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ int runUic(int argc, char *argv[])
fromImportsOption.setDescription(QStringLiteral("Python: generate imports relative to '.'"));
parser.addOption(fromImportsOption);

// FIXME Qt 7: Remove?
QCommandLineOption useStarImportsOption(QStringLiteral("star-imports"));
useStarImportsOption.setDescription(QStringLiteral("Python: Use * imports"));
parser.addOption(useStarImportsOption);

parser.addPositionalArgument(QStringLiteral("[uifile]"), QStringLiteral("Input file (*.ui), otherwise stdin."));

parser.process(app);
Expand All @@ -123,6 +128,7 @@ int runUic(int argc, char *argv[])
driver.option().implicitIncludes = !parser.isSet(noImplicitIncludesOption);
driver.option().idBased = parser.isSet(idBasedOption);
driver.option().fromImports = parser.isSet(fromImportsOption);
driver.option().useStarImports = parser.isSet(useStarImportsOption);
driver.option().postfix = parser.value(postfixOption);
driver.option().translateFunction = parser.value(translateOption);
driver.option().includeFile = parser.value(includeOption);
Expand Down
2 changes: 2 additions & 0 deletions src/tools/uic/option.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct Option
unsigned int fromImports: 1;
unsigned int forceMemberFnPtrConnectionSyntax: 1;
unsigned int forceStringConnectionSyntax: 1;
unsigned int useStarImports: 1;

QString inputFile;
QString outputFile;
Expand All @@ -71,6 +72,7 @@ struct Option
fromImports(0),
forceMemberFnPtrConnectionSyntax(0),
forceStringConnectionSyntax(0),
useStarImports(0),
prefix(QLatin1String("Ui_"))
{ indent.fill(QLatin1Char(' '), 4); }

Expand Down
206 changes: 154 additions & 52 deletions src/tools/uic/python/pythonwriteimports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,49 @@
#include <customwidgetsinfo.h>
#include <option.h>
#include <uic.h>
#include <driver.h>

#include <ui4.h>

#include <QtCore/qtextstream.h>

#include <algorithm>

QT_BEGIN_NAMESPACE

static QString standardImports()
// Generate imports for Python. Note some things differ from C++:
// - qItemView->header()->setFoo() does not require QHeaderView to be imported
// - qLabel->setFrameShape(QFrame::Box) however requires QFrame to be imported
// (see acceptProperty())

namespace Python {

// Classes required for properties
static WriteImports::ClassesPerModule defaultClasses()
{
return QString::fromLatin1(R"I(from PySide%1.QtCore import * # type: ignore
from PySide%1.QtGui import * # type: ignore
from PySide%1.QtWidgets import * # type: ignore
)I").arg(QT_VERSION_MAJOR);
return {
{QStringLiteral("QtCore"),
{QStringLiteral("QCoreApplication"), QStringLiteral("QDate"),
QStringLiteral("QDateTime"), QStringLiteral("QLocale"),
QStringLiteral("QMetaObject"), QStringLiteral("QObject"),
QStringLiteral("QPoint"), QStringLiteral("QRect"),
QStringLiteral("QSize"), QStringLiteral("QTime"),
QStringLiteral("QUrl"), QStringLiteral("Qt")},
},
{QStringLiteral("QtGui"),
{QStringLiteral("QBrush"), QStringLiteral("QColor"),
QStringLiteral("QConicalGradient"), QStringLiteral("QCursor"),
QStringLiteral("QGradient"), QStringLiteral("QFont"),
QStringLiteral("QFontDatabase"), QStringLiteral("QIcon"),
QStringLiteral("QImage"), QStringLiteral("QKeySequence"),
QStringLiteral("QLinearGradient"), QStringLiteral("QPalette"),
QStringLiteral("QPainter"), QStringLiteral("QPixmap"),
QStringLiteral("QTransform"), QStringLiteral("QRadialGradient")}
},
{QStringLiteral("QtWidgets"),
{QStringLiteral("QSizePolicy")}
}
};
}

// Change the name of a qrc file "dir/foo.qrc" file to the Python
Expand All @@ -60,19 +90,72 @@ static QString pythonResource(QString resource)
return resource;
}

namespace Python {
// Helpers for WriteImports::ClassesPerModule maps
static void insertClass(const QString &module, const QString &className,
WriteImports::ClassesPerModule *c)
{
auto usedIt = c->find(module);
if (usedIt == c->end())
c->insert(module, {className});
else if (!usedIt.value().contains(className))
usedIt.value().append(className);
}

// Format a class list: "from A import (B, C)"
static void formatImportClasses(QTextStream &str, QStringList classList)
{
std::sort(classList.begin(), classList.end());

const qsizetype size = classList.size();
if (size > 1)
str << '(';
for (qsizetype i = 0; i < size; ++i) {
if (i > 0)
str << (i % 4 == 0 ? ",\n " : ", ");
str << classList.at(i);
}
if (size > 1)
str << ')';
}

static void formatClasses(QTextStream &str, const WriteImports::ClassesPerModule &c,
bool useStarImports = false,
const QByteArray &modulePrefix = {})
{
for (auto it = c.cbegin(), end = c.cend(); it != end; ++it) {
str << "from " << modulePrefix << it.key() << " import ";
if (useStarImports)
str << "* # type: ignore";
else
formatImportClasses(str, it.value());
str << '\n';
}
}

WriteImports::WriteImports(Uic *uic) : m_uic(uic)
WriteImports::WriteImports(Uic *uic) : WriteIncludesBase(uic),
m_qtClasses(defaultClasses())
{
for (const auto &e : classInfoEntries())
m_classToModule.insert(QLatin1String(e.klass), QLatin1String(e.module));
}

void WriteImports::acceptUI(DomUI *node)
{
auto &output = m_uic->output();
output << standardImports() << '\n';
if (auto customWidgets = node->elementCustomWidgets()) {
TreeWalker::acceptCustomWidgets(customWidgets);
WriteIncludesBase::acceptUI(node);

auto &output = uic()->output();
const bool useStarImports = uic()->driver()->option().useStarImports;

const QByteArray qtPrefix = QByteArrayLiteral("PySide")
+ QByteArray::number(QT_VERSION_MAJOR) + '.';

formatClasses(output, m_qtClasses, useStarImports, qtPrefix);

if (!m_customWidgets.isEmpty() || !m_plainCustomWidgets.isEmpty()) {
output << '\n';
formatClasses(output, m_customWidgets, useStarImports);
for (const auto &w : m_plainCustomWidgets)
output << "import " << w << '\n';
}

if (auto resources = node->elementResources()) {
Expand All @@ -87,57 +170,76 @@ void WriteImports::acceptUI(DomUI *node)

void WriteImports::writeImport(const QString &module)
{

if (m_uic->option().fromImports)
m_uic->output() << "from . ";
m_uic->output() << "import " << module << '\n';
if (uic()->option().fromImports)
uic()->output() << "from . ";
uic()->output() << "import " << module << '\n';
}

QString WriteImports::qtModuleOf(const DomCustomWidget *node) const
void WriteImports::doAdd(const QString &className, const DomCustomWidget *dcw)
{
if (m_uic->customWidgetsInfo()->extends(node->elementClass(), QLatin1String("QAxWidget")))
return QStringLiteral("QtAxContainer");
if (const auto headerElement = node->elementHeader()) {
const auto &header = headerElement->text();
if (header.startsWith(QLatin1String("Qt"))) {
const int slash = header.indexOf(QLatin1Char('/'));
if (slash != -1)
return header.left(slash);
}
const CustomWidgetsInfo *cwi = uic()->customWidgetsInfo();
if (cwi->extends(className, QLatin1String("QListWidget")))
add(QStringLiteral("QListWidgetItem"));
else if (cwi->extends(className, QLatin1String("QTreeWidget")))
add(QStringLiteral("QTreeWidgetItem"));
else if (cwi->extends(className, QLatin1String("QTableWidget")))
add(QStringLiteral("QTableWidgetItem"));

if (dcw != nullptr) {
addPythonCustomWidget(className, dcw);
return;
}
return QString();

if (!addQtClass(className))
qWarning("WriteImports::add(): Unknown Qt class %s", qPrintable(className));
}

bool WriteImports::addQtClass(const QString &className)
{
// QVariant is not exposed in PySide
if (className == u"QVariant" || className == u"Qt")
return true;

const auto moduleIt = m_classToModule.constFind(className);
const bool result = moduleIt != m_classToModule.cend();
if (result)
insertClass(moduleIt.value(), className, &m_qtClasses);
return result;
}

void WriteImports::acceptCustomWidget(DomCustomWidget *node)
void WriteImports::addPythonCustomWidget(const QString &className, const DomCustomWidget *node)
{
const auto &className = node->elementClass();
if (className.contains(QLatin1String("::")))
return; // Exclude namespaced names (just to make tests pass).
const QString &importModule = qtModuleOf(node);
auto &output = m_uic->output();
// For starting importing Qt for Python modules
if (!importModule.isEmpty()) {
output << "from ";
if (importModule.startsWith(QLatin1String("Qt")))
output << "PySide" << QT_VERSION_MAJOR << '.';
output << importModule;
if (!className.isEmpty())
output << " import " << className << "\n\n";
} else {
// When the elementHeader is not set, we know it's the continuation
// of a Qt for Python import or a normal import of another module.
if (!node->elementHeader() || node->elementHeader()->text().isEmpty()) {
output << "import " << className << '\n';
} else { // When we do have elementHeader, we know it's a relative import.
QString modulePath = node->elementHeader()->text();
// Replace the '/' by '.'
modulePath.replace(QLatin1Char('/'), QLatin1Char('.'));
// '.h' is added by default on headers for <customwidget>
if (modulePath.endsWith(QLatin1String(".h")))
modulePath.chop(2);
output << "from " << modulePath << " import " << className << '\n';
}

if (addQtClass(className)) // Qt custom widgets like QQuickWidget, QAxWidget, etc
return;

// When the elementHeader is not set, we know it's the continuation
// of a Qt for Python import or a normal import of another module.
if (!node->elementHeader() || node->elementHeader()->text().isEmpty()) {
m_plainCustomWidgets.append(className);
} else { // When we do have elementHeader, we know it's a relative import.
QString modulePath = node->elementHeader()->text();
// Replace the '/' by '.'
modulePath.replace(QLatin1Char('/'), QLatin1Char('.'));
// '.h' is added by default on headers for <customwidget>
if (modulePath.endsWith(QLatin1String(".h")))
modulePath.chop(2);
insertClass(modulePath, className, &m_customWidgets);
}
}

void WriteImports::acceptProperty(DomProperty *node)
{
if (node->kind() == DomProperty::Enum) {
// Add base classes like QFrame for QLabel::frameShape()
const QString &enumV = node->elementEnum();
const auto colonPos = enumV.indexOf(u"::");
if (colonPos > 0)
addQtClass(enumV.left(colonPos));
}
WriteIncludesBase::acceptProperty(node);
}

} // namespace Python
Expand Down
27 changes: 20 additions & 7 deletions src/tools/uic/python/pythonwriteimports.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,40 @@
#ifndef PYTHONWRITEIMPORTS_H
#define PYTHONWRITEIMPORTS_H

#include <treewalker.h>
#include <writeincludesbase.h>

QT_BEGIN_NAMESPACE
#include <QtCore/qhash.h>
#include <QtCore/qmap.h>
#include <QtCore/qstringlist.h>

class Uic;
QT_BEGIN_NAMESPACE

namespace Python {

struct WriteImports : public TreeWalker
class WriteImports : public WriteIncludesBase
{
public:
using ClassesPerModule = QMap<QString, QStringList>;

explicit WriteImports(Uic *uic);

void acceptUI(DomUI *node) override;
void acceptCustomWidget(DomCustomWidget *node) override;
void acceptProperty(DomProperty *node) override;

protected:
void doAdd(const QString &className, const DomCustomWidget *dcw = nullptr) override;

private:
void addPythonCustomWidget(const QString &className, const DomCustomWidget *dcw);
bool addQtClass(const QString &className);
void writeImport(const QString &module);
QString qtModuleOf(const DomCustomWidget *node) const;

Uic *const m_uic;
QHash<QString, QString> m_classToModule;
// Module->class (modules sorted)

ClassesPerModule m_qtClasses;
ClassesPerModule m_customWidgets;
QStringList m_plainCustomWidgets; // Custom widgets without any module
};

} // namespace Python
Expand Down
15 changes: 11 additions & 4 deletions tests/auto/tools/uic/baseline/config.ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,20 @@
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import * # type: ignore
from PySide6.QtGui import * # type: ignore
from PySide6.QtWidgets import * # type: ignore
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDialog,
QGridLayout, QGroupBox, QHBoxLayout, QLabel,
QPushButton, QRadioButton, QSizePolicy, QSlider,
QSpacerItem, QSpinBox, QVBoxLayout)

from gammaview import GammaView


class Ui_Config(object):
def setupUi(self, Config):
if not Config.objectName():
Expand Down

0 comments on commit de15836

Please sign in to comment.