diff --git a/.gitignore b/.gitignore index 486ec80..e7c50d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,40 +1,75 @@ *~ +.pycache __pycache__ .directory -# -.cache/ + +du.log +nogit/ +__TRASH__/ +review.json + CodeReview.egg-info/ -CodeReview/PatienceDiff/_patiencediff_c.cpython-36m-x86_64-linux-gnu.so MANIFEST build/ dist/ + doc/sphinx/build/ doc/sphinx/source/api/ -# -bzr-backup/ -trash/ -# -CodeReview/TextDistance/buggy.c -DELETED-SPHINX-SOURCE -du.log -gui-prototypes/ -liblevenshtein_distance.so -notes/ -profile-pyqgit.sh -setup.py-backup0 -setup_data.py-backup0 -share/icons/copy-icons.sh -test-failure.txt -test-repository/ -test/Model.py -test/github3-test.py -test/init-stage-test-repo.sh -test/stage-test -test/swalign-fabrice/ -test/swalign-mbreese/ -test/test-diff.py -test/test-levenshtein-distance.py -test/test-repo/ -test/token.txt -tools/rsync-vps.sh -tools/upload-www + +src/CodeReview/GitHub/code/ +src/CodeReview/QmlApplication/rcc/CodeReviewRessource.py +src/CodeReview/QmlApplication/rcc/code-review.rcc +src/CodeReview/TextDistance/buggy.c +src/CodeReview/ovh.txt +src/CodeReview/share/icons/copy-icons.sh + +src/CodeReview/PatienceDiff/_patiencediff_c.cpython-314-x86_64-linux-gnu.so +src/CodeReview/TextDistance/levenshtein_distance_c.cpython-314-x86_64-linux-gnu.so +src/code_review.egg-info/ + +doc/IDEA.md +doc/git-gui.md +doc/gitkraken/ + +old/RELEASE-TEST.rst +old/__BUGS__/ +old/gitkraken-id.txt +old/gui-prototypes/qlog.ui +old/gui-prototypes/side-view/side-view_ui.py +old/notes/bzr-qlog1.png +old/notes/codereview.png +old/notes/diffview-link.txt +old/notes/git-man-pages/ +old/notes/github1.png +old/notes/github2.png +old/notes/github3.png +old/notes/github4.png +old/notes/github5.png +old/notes/github6.png +old/notes/qbzr-qdiff.png +old/notes/qbzr-qlog.png +old/notes/qgit1.png +old/notes/qgit2.png +old/notes/qgit3.png +old/notes/qgit4.png +old/notes/qgit5.png +old/notes/qgit6.png +old/profile-pyqgit.sh +old/test-failure.txt +old/test-repository/bzr-repo/ +old/test-repository/git-repo/repo-branches/ +old/test-repository/git-repo/repo-twisted-branches/ +old/test-repository/git-repo/repo-two-branches/ +old/test-repository/git-repo/repo1/ +old/test/Model.py +old/test/github3-test.py +old/test/init-stage-test-repo.sh +old/test/stage-test +old/test/swalign-fabrice/ +old/test/swalign-mbreese/ +old/test/test-diff.py +old/test/test-levenshtein-distance.py +old/test/test-repo/ +old/test/token.txt +old/tools/rsync-vps.sh +old/tools/upload-www diff --git a/CodeReview/Config/ConfigInstall.py b/CodeReview/Config/ConfigInstall.py deleted file mode 100644 index 1cbd931..0000000 --- a/CodeReview/Config/ConfigInstall.py +++ /dev/null @@ -1,61 +0,0 @@ -#################################################################################################### - -import os -import sys - -#################################################################################################### - -import CodeReview.Tools.Path as PathTools # due to Path class - -#################################################################################################### - -_this_file = PathTools.to_absolute_path(__file__) - -class Path: - - CodeReview_module_directory = PathTools.parent_directory_of(_this_file, step=2) - config_directory = os.path.dirname(_this_file) - - ############################################## - - @staticmethod - def share_directory(): - - path = os.path.dirname(Path.CodeReview_module_directory) - if path.startswith(sys.exec_prefix): - return os.path.join(sys.exec_prefix, 'share', 'CodeReview') - else: - return os.path.join(path, 'share') - -#################################################################################################### - -class Logging: - - default_config_file = 'logging.yml' - directories = (Path.config_directory,) - - ############################################## - - @staticmethod - def find(config_file): - - return PathTools.find(config_file, Logging.directories) - -#################################################################################################### - -class Icon: - - icon_directory = os.path.join(Path.share_directory(), 'icons') - - ############################################## - - @staticmethod - def find(file_name, icon_size): - - if icon_size == 'svg': - size_directory = icon_size - else: - size_directory = '{0}x{0}'.format(icon_size) - - icon_directory = os.path.join(Icon.icon_directory, size_directory) - return PathTools.find(file_name, (icon_directory,)) diff --git a/CodeReview/GUI/LogBrowser/__init__.py b/CodeReview/GUI/LogBrowser/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/GUI/LogBrowser/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/GUI/Widgets/__init__.py b/CodeReview/GUI/Widgets/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/GUI/Widgets/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/GUI/__init__.py b/CodeReview/GUI/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/GUI/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Logging/__init__.py b/CodeReview/Logging/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/Logging/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Math/__init__.py b/CodeReview/Math/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/Math/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Tools/__init__.py b/CodeReview/Tools/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/Tools/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/__init__.py b/CodeReview/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/MANIFEST.in b/MANIFEST.in index 3e5c4ba..3550c33 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,32 +1,23 @@ -include .bzrignore include .gitignore include .pyflymakerc include .travis.yml -include CHANGES.txt include GPL-V3.0.txt -include LICENSE.txt include MANIFEST.in include README.html include README.rst include README.txt -include make-release.sh -include pylintrc.ini include requirements.txt -include setenv.sh start.sh include setup_data.py -include test-repository/git-repo/*.sh -recursive-include CodeReview *.py *.yml *.c *.h *.ui Makefile.pyqt .in -recursive-include bin +recursive-include CodeReview *.py *.yml *.c *.h *.ui Makefile.pyqt *.png *.svg recursive-include doc * recursive-include doc/sphinx/source/_static * recursive-include gh-pages * -recursive-include share *.png *.svg recursive-include spec *.spec *.desktop -recursive-include tools * recursive-include unit-test *.py recursive-include unit-test/data *.txt -prune *~ *.pyc +prune *~ +prune *.pyc prune build prune doc/sphinx/build -prune test-repository/bzr-repo/ -prune test-repository/git-repo/ +prune tasks +prune QtShim diff --git a/README.html b/README.html index a43266f..222b9f1 100644 --- a/README.html +++ b/README.html @@ -3,8 +3,8 @@ - - + +README.rst \ No newline at end of file diff --git a/share/icons/32x32/go-next.svg b/src/CodeReview/share/icons/32x32/go-next.svg similarity index 100% rename from share/icons/32x32/go-next.svg rename to src/CodeReview/share/icons/32x32/go-next.svg diff --git a/share/icons/32x32/go-previous.svg b/src/CodeReview/share/icons/32x32/go-previous.svg similarity index 100% rename from share/icons/32x32/go-previous.svg rename to src/CodeReview/share/icons/32x32/go-previous.svg diff --git a/src/CodeReview/share/icons/32x32/media-playlist-repeat.svg b/src/CodeReview/share/icons/32x32/media-playlist-repeat.svg new file mode 100644 index 0000000..0e3071b --- /dev/null +++ b/src/CodeReview/share/icons/32x32/media-playlist-repeat.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/share/icons/32x32/view-refresh.svg b/src/CodeReview/share/icons/32x32/view-refresh.svg similarity index 100% rename from share/icons/32x32/view-refresh.svg rename to src/CodeReview/share/icons/32x32/view-refresh.svg diff --git a/share/icons/48x48/dialog-warning.png b/src/CodeReview/share/icons/48x48/dialog-warning.png similarity index 100% rename from share/icons/48x48/dialog-warning.png rename to src/CodeReview/share/icons/48x48/dialog-warning.png diff --git a/share/icons/code-review-250.png b/src/CodeReview/share/icons/code-review-250.png similarity index 100% rename from share/icons/code-review-250.png rename to src/CodeReview/share/icons/code-review-250.png diff --git a/share/icons/svg/align-mode.svg b/src/CodeReview/share/icons/svg/align-mode.svg similarity index 100% rename from share/icons/svg/align-mode.svg rename to src/CodeReview/share/icons/svg/align-mode.svg diff --git a/share/icons/svg/code-review.svg b/src/CodeReview/share/icons/svg/code-review.svg similarity index 100% rename from share/icons/svg/code-review.svg rename to src/CodeReview/share/icons/svg/code-review.svg diff --git a/share/icons/svg/complete-mode.svg b/src/CodeReview/share/icons/svg/complete-mode.svg similarity index 100% rename from share/icons/svg/complete-mode.svg rename to src/CodeReview/share/icons/svg/complete-mode.svg diff --git a/src/CodeReview/share/icons/svg/edit-delete.svg b/src/CodeReview/share/icons/svg/edit-delete.svg new file mode 120000 index 0000000..39d3d8a --- /dev/null +++ b/src/CodeReview/share/icons/svg/edit-delete.svg @@ -0,0 +1 @@ +../32x32/edit-delete.svg \ No newline at end of file diff --git a/share/icons/svg/go-next.svg b/src/CodeReview/share/icons/svg/go-next.svg similarity index 100% rename from share/icons/svg/go-next.svg rename to src/CodeReview/share/icons/svg/go-next.svg diff --git a/share/icons/svg/go-previous.svg b/src/CodeReview/share/icons/svg/go-previous.svg similarity index 100% rename from share/icons/svg/go-previous.svg rename to src/CodeReview/share/icons/svg/go-previous.svg diff --git a/share/icons/svg/highlight.svg b/src/CodeReview/share/icons/svg/highlight.svg similarity index 100% rename from share/icons/svg/highlight.svg rename to src/CodeReview/share/icons/svg/highlight.svg diff --git a/share/icons/svg/line-number-mode.svg b/src/CodeReview/share/icons/svg/line-number-mode.svg similarity index 100% rename from share/icons/svg/line-number-mode.svg rename to src/CodeReview/share/icons/svg/line-number-mode.svg diff --git a/src/CodeReview/share/icons/svg/media-playlist-repeat.svg b/src/CodeReview/share/icons/svg/media-playlist-repeat.svg new file mode 120000 index 0000000..47a4244 --- /dev/null +++ b/src/CodeReview/share/icons/svg/media-playlist-repeat.svg @@ -0,0 +1 @@ +../32x32/media-playlist-repeat.svg \ No newline at end of file diff --git a/share/icons/svg/view-refresh.svg b/src/CodeReview/share/icons/svg/view-refresh.svg similarity index 100% rename from share/icons/svg/view-refresh.svg rename to src/CodeReview/share/icons/svg/view-refresh.svg diff --git a/src/QtShim/QtConfig.py b/src/QtShim/QtConfig.py new file mode 100644 index 0000000..e1b09db --- /dev/null +++ b/src/QtShim/QtConfig.py @@ -0,0 +1,745 @@ +#################################################################################################### + +"""Common members of all bindings + +This is where each member of Qt.py is explicitly defined. +It is based on a 'lowest common denominator' of all bindings; +including members found in each of the 4 bindings. + +The '_common_members' dictionary is generated using the +build_membership.sh script. + +""" + +_common_members = { + 'QtCore': [ + 'QAbstractAnimation', + 'QAbstractEventDispatcher', + 'QAbstractItemModel', + 'QAbstractListModel', + 'QAbstractState', + 'QAbstractTableModel', + 'QAbstractTransition', + 'QAnimationGroup', + 'QBasicTimer', + 'QBitArray', + 'QBuffer', + 'QByteArray', + 'QByteArrayMatcher', + 'QChildEvent', + 'QCoreApplication', + 'QCryptographicHash', + 'QDataStream', + 'QDate', + 'QDateTime', + 'QDir', + 'QDirIterator', + 'QDynamicPropertyChangeEvent', + 'QEasingCurve', + 'QElapsedTimer', + 'QEvent', + 'QEventLoop', + 'QEventTransition', + 'QFile', + 'QFileInfo', + 'QFileSystemWatcher', + 'QFinalState', + 'QGenericArgument', + 'QGenericReturnArgument', + 'QHistoryState', + 'QItemSelectionRange', + 'QIODevice', + 'QLibraryInfo', + 'QLine', + 'QLineF', + 'QLocale', + 'QMargins', + 'QMetaClassInfo', + 'QMetaEnum', + 'QMetaMethod', + 'QMetaObject', + 'QMetaProperty', + 'QMimeData', + 'QModelIndex', + 'QMutex', + 'QMutexLocker', + 'QObject', + 'QParallelAnimationGroup', + 'QPauseAnimation', + 'QPersistentModelIndex', + 'QPluginLoader', + 'QPoint', + 'QPointF', + 'QProcess', + 'QProcessEnvironment', + 'QPropertyAnimation', + 'QReadLocker', + 'QReadWriteLock', + 'QRect', + 'QRectF', + 'QRegExp', + 'QResource', + 'QRunnable', + 'QSemaphore', + 'QSequentialAnimationGroup', + 'QSettings', + 'QSignalMapper', + 'QSignalTransition', + 'QSize', + 'QSizeF', + 'QSocketNotifier', + 'QState', + 'QStateMachine', + 'QSysInfo', + 'QSystemSemaphore', + 'QT_TRANSLATE_NOOP', + 'QT_TR_NOOP', + 'QT_TR_NOOP_UTF8', + 'QTemporaryFile', + 'QTextBoundaryFinder', + 'QTextCodec', + 'QTextDecoder', + 'QTextEncoder', + 'QTextStream', + 'QTextStreamManipulator', + 'QThread', + 'QThreadPool', + 'QTime', + 'QTimeLine', + 'QTimer', + 'QTimerEvent', + 'QTranslator', + 'QUrl', + 'QVariantAnimation', + 'QWaitCondition', + 'QWriteLocker', + 'QXmlStreamAttribute', + 'QXmlStreamAttributes', + 'QXmlStreamEntityDeclaration', + 'QXmlStreamEntityResolver', + 'QXmlStreamNamespaceDeclaration', + 'QXmlStreamNotationDeclaration', + 'QXmlStreamReader', + 'QXmlStreamWriter', + 'Qt', + 'QtCriticalMsg', + 'QtDebugMsg', + 'QtFatalMsg', + 'QtMsgType', + 'QtSystemMsg', + 'QtWarningMsg', + 'qAbs', + 'qAddPostRoutine', + 'qChecksum', + 'qCritical', + 'qDebug', + 'qFatal', + 'qFuzzyCompare', + 'qIsFinite', + 'qIsInf', + 'qIsNaN', + 'qIsNull', + 'qRegisterResourceData', + 'qUnregisterResourceData', + 'qVersion', + 'qWarning', + 'qrand', + 'qsrand' + ], + 'QtGui': [ + 'QAbstractTextDocumentLayout', + 'QActionEvent', + 'QBitmap', + 'QBrush', + 'QClipboard', + 'QCloseEvent', + 'QColor', + 'QConicalGradient', + 'QContextMenuEvent', + 'QCursor', + 'QDesktopServices', + 'QDoubleValidator', + 'QDrag', + 'QDragEnterEvent', + 'QDragLeaveEvent', + 'QDragMoveEvent', + 'QDropEvent', + 'QFileOpenEvent', + 'QFocusEvent', + 'QFont', + 'QFontDatabase', + 'QFontInfo', + 'QFontMetrics', + 'QFontMetricsF', + 'QGradient', + 'QGuiApplication', + 'QHelpEvent', + 'QHideEvent', + 'QHoverEvent', + 'QIcon', + 'QIconDragEvent', + 'QIconEngine', + 'QImage', + 'QImageIOHandler', + 'QImageReader', + 'QImageWriter', + 'QInputEvent', + 'QInputMethodEvent', + 'QIntValidator', + 'QKeyEvent', + 'QKeySequence', + 'QLinearGradient', + 'QMatrix2x2', + 'QMatrix2x3', + 'QMatrix2x4', + 'QMatrix3x2', + 'QMatrix3x3', + 'QMatrix3x4', + 'QMatrix4x2', + 'QMatrix4x3', + 'QMatrix4x4', + 'QMouseEvent', + 'QMoveEvent', + 'QMovie', + 'QPaintDevice', + 'QPaintEngine', + 'QPaintEngineState', + 'QPaintEvent', + 'QPainter', + 'QPainterPath', + 'QPainterPathStroker', + 'QPalette', + 'QPen', + 'QPicture', + 'QPictureIO', + 'QPixmap', + 'QPixmapCache', + 'QPolygon', + 'QPolygonF', + 'QQuaternion', + 'QRadialGradient', + 'QRegExpValidator', + 'QRegion', + 'QResizeEvent', + 'QSessionManager', + 'QShortcutEvent', + 'QShowEvent', + 'QStandardItem', + 'QStandardItemModel', + 'QStatusTipEvent', + 'QSyntaxHighlighter', + 'QTabletEvent', + 'QTextBlock', + 'QTextBlockFormat', + 'QTextBlockGroup', + 'QTextBlockUserData', + 'QTextCharFormat', + 'QTextCursor', + 'QTextDocument', + 'QTextDocumentFragment', + 'QTextFormat', + 'QTextFragment', + 'QTextFrame', + 'QTextFrameFormat', + 'QTextImageFormat', + 'QTextInlineObject', + 'QTextItem', + 'QTextLayout', + 'QTextLength', + 'QTextLine', + 'QTextList', + 'QTextListFormat', + 'QTextObject', + 'QTextObjectInterface', + 'QTextOption', + 'QTextTable', + 'QTextTableCell', + 'QTextTableCellFormat', + 'QTextTableFormat', + 'QTouchEvent', + 'QTransform', + 'QValidator', + 'QVector2D', + 'QVector3D', + 'QVector4D', + 'QWhatsThisClickedEvent', + 'QWheelEvent', + 'QWindowStateChangeEvent', + 'qAlpha', + 'qBlue', + 'qGray', + 'qGreen', + 'qIsGray', + 'qRed', + 'qRgb', + 'qRgba' + ], + # 'QtHelp': [ + # 'QHelpContentItem', + # 'QHelpContentModel', + # 'QHelpContentWidget', + # 'QHelpEngine', + # 'QHelpEngineCore', + # 'QHelpIndexModel', + # 'QHelpIndexWidget', + # 'QHelpSearchEngine', + # 'QHelpSearchQuery', + # 'QHelpSearchQueryWidget', + # 'QHelpSearchResultWidget' + # ], + # 'QtMultimedia': [ + # 'QAbstractVideoBuffer', + # 'QAbstractVideoSurface', + # 'QAudio', + # 'QAudioDeviceInfo', + # 'QAudioFormat', + # 'QAudioInput', + # 'QAudioOutput', + # 'QVideoFrame', + # 'QVideoSurfaceFormat' + # ], + # 'QtNetwork': [ + # 'QAbstractNetworkCache', + # 'QAbstractSocket', + # 'QAuthenticator', + # 'QHostAddress', + # 'QHostInfo', + # 'QLocalServer', + # 'QLocalSocket', + # 'QNetworkAccessManager', + # 'QNetworkAddressEntry', + # 'QNetworkCacheMetaData', + # 'QNetworkConfiguration', + # 'QNetworkConfigurationManager', + # 'QNetworkCookie', + # 'QNetworkCookieJar', + # 'QNetworkDiskCache', + # 'QNetworkInterface', + # 'QNetworkProxy', + # 'QNetworkProxyFactory', + # 'QNetworkProxyQuery', + # 'QNetworkReply', + # 'QNetworkRequest', + # 'QNetworkSession', + # 'QSsl', + # 'QTcpServer', + # 'QTcpSocket', + # 'QUdpSocket' + # ], + # 'QtOpenGL': [ + # 'QGL', + # 'QGLContext', + # 'QGLFormat', + # 'QGLWidget' + # ], + # 'QtPrintSupport': [ + # 'QAbstractPrintDialog', + # 'QPageSetupDialog', + # 'QPrintDialog', + # 'QPrintEngine', + # 'QPrintPreviewDialog', + # 'QPrintPreviewWidget', + # 'QPrinter', + # 'QPrinterInfo' + # ], + # 'QtSql': [ + # 'QSql', + # 'QSqlDatabase', + # 'QSqlDriver', + # 'QSqlDriverCreatorBase', + # 'QSqlError', + # 'QSqlField', + # 'QSqlIndex', + # 'QSqlQuery', + # 'QSqlQueryModel', + # 'QSqlRecord', + # 'QSqlRelation', + # 'QSqlRelationalDelegate', + # 'QSqlRelationalTableModel', + # 'QSqlResult', + # 'QSqlTableModel' + # ], + 'QtSvg': [ + 'QGraphicsSvgItem', + 'QSvgGenerator', + 'QSvgRenderer', + 'QSvgWidget' + ], + # 'QtTest': [ + # 'QTest' + # ], + 'QtWidgets': [ + 'QAbstractButton', + 'QAbstractGraphicsShapeItem', + 'QAbstractItemDelegate', + 'QAbstractItemView', + 'QAbstractScrollArea', + 'QAbstractSlider', + 'QAbstractSpinBox', + 'QAction', + 'QActionGroup', + 'QApplication', + 'QBoxLayout', + 'QButtonGroup', + 'QCalendarWidget', + 'QCheckBox', + 'QColorDialog', + 'QColumnView', + 'QComboBox', + 'QCommandLinkButton', + 'QCommonStyle', + 'QCompleter', + 'QDataWidgetMapper', + 'QDateEdit', + 'QDateTimeEdit', + 'QDesktopWidget', + 'QDial', + 'QDialog', + 'QDialogButtonBox', + 'QDirModel', + 'QDockWidget', + 'QDoubleSpinBox', + 'QErrorMessage', + 'QFileDialog', + 'QFileIconProvider', + 'QFileSystemModel', + 'QFocusFrame', + 'QFontComboBox', + 'QFontDialog', + 'QFormLayout', + 'QFrame', + 'QGesture', + 'QGestureEvent', + 'QGestureRecognizer', + 'QGraphicsAnchor', + 'QGraphicsAnchorLayout', + 'QGraphicsBlurEffect', + 'QGraphicsColorizeEffect', + 'QGraphicsDropShadowEffect', + 'QGraphicsEffect', + 'QGraphicsEllipseItem', + 'QGraphicsGridLayout', + 'QGraphicsItem', + 'QGraphicsItemGroup', + 'QGraphicsLayout', + 'QGraphicsLayoutItem', + 'QGraphicsLineItem', + 'QGraphicsLinearLayout', + 'QGraphicsObject', + 'QGraphicsOpacityEffect', + 'QGraphicsPathItem', + 'QGraphicsPixmapItem', + 'QGraphicsPolygonItem', + 'QGraphicsProxyWidget', + 'QGraphicsRectItem', + 'QGraphicsRotation', + 'QGraphicsScale', + 'QGraphicsScene', + 'QGraphicsSceneContextMenuEvent', + 'QGraphicsSceneDragDropEvent', + 'QGraphicsSceneEvent', + 'QGraphicsSceneHelpEvent', + 'QGraphicsSceneHoverEvent', + 'QGraphicsSceneMouseEvent', + 'QGraphicsSceneMoveEvent', + 'QGraphicsSceneResizeEvent', + 'QGraphicsSceneWheelEvent', + 'QGraphicsSimpleTextItem', + 'QGraphicsTextItem', + 'QGraphicsTransform', + 'QGraphicsView', + 'QGraphicsWidget', + 'QGridLayout', + 'QGroupBox', + 'QHBoxLayout', + 'QHeaderView', + 'QInputDialog', + 'QItemDelegate', + 'QItemEditorCreatorBase', + 'QItemEditorFactory', + 'QKeyEventTransition', + 'QLCDNumber', + 'QLabel', + 'QLayout', + 'QLayoutItem', + 'QLineEdit', + 'QListView', + 'QListWidget', + 'QListWidgetItem', + 'QMainWindow', + 'QMdiArea', + 'QMdiSubWindow', + 'QMenu', + 'QMenuBar', + 'QMessageBox', + 'QMouseEventTransition', + 'QPanGesture', + 'QPinchGesture', + 'QPlainTextDocumentLayout', + 'QPlainTextEdit', + 'QProgressBar', + 'QProgressDialog', + 'QPushButton', + 'QRadioButton', + 'QRubberBand', + 'QScrollArea', + 'QScrollBar', + 'QShortcut', + 'QSizeGrip', + 'QSizePolicy', + 'QSlider', + 'QSpacerItem', + 'QSpinBox', + 'QSplashScreen', + 'QSplitter', + 'QSplitterHandle', + 'QStackedLayout', + 'QStackedWidget', + 'QStatusBar', + 'QStyle', + 'QStyleFactory', + 'QStyleHintReturn', + 'QStyleHintReturnMask', + 'QStyleHintReturnVariant', + 'QStyleOption', + 'QStyleOptionButton', + 'QStyleOptionComboBox', + 'QStyleOptionComplex', + 'QStyleOptionDockWidget', + 'QStyleOptionFocusRect', + 'QStyleOptionFrame', + 'QStyleOptionGraphicsItem', + 'QStyleOptionGroupBox', + 'QStyleOptionHeader', + 'QStyleOptionMenuItem', + 'QStyleOptionProgressBar', + 'QStyleOptionRubberBand', + 'QStyleOptionSizeGrip', + 'QStyleOptionSlider', + 'QStyleOptionSpinBox', + 'QStyleOptionTab', + 'QStyleOptionTabBarBase', + 'QStyleOptionTabWidgetFrame', + 'QStyleOptionTitleBar', + 'QStyleOptionToolBar', + 'QStyleOptionToolBox', + 'QStyleOptionToolButton', + 'QStyleOptionViewItem', + 'QStylePainter', + 'QStyledItemDelegate', + 'QSwipeGesture', + 'QSystemTrayIcon', + 'QTabBar', + 'QTabWidget', + 'QTableView', + 'QTableWidget', + 'QTableWidgetItem', + 'QTableWidgetSelectionRange', + 'QTapAndHoldGesture', + 'QTapGesture', + 'QTextBrowser', + 'QTextEdit', + 'QTimeEdit', + 'QToolBar', + 'QToolBox', + 'QToolButton', + 'QToolTip', + 'QTreeView', + 'QTreeWidget', + 'QTreeWidgetItem', + 'QTreeWidgetItemIterator', + 'QUndoCommand', + 'QUndoGroup', + 'QUndoStack', + 'QUndoView', + 'QVBoxLayout', + 'QWhatsThis', + 'QWidget', + 'QWidgetAction', + 'QWidgetItem', + 'QWizard', + 'QWizardPage' + ], + # 'QtX11Extras': [ + # 'QX11Info' + # ], + # 'QtXml': [ + # 'QDomAttr', + # 'QDomCDATASection', + # 'QDomCharacterData', + # 'QDomComment', + # 'QDomDocument', + # 'QDomDocumentFragment', + # 'QDomDocumentType', + # 'QDomElement', + # 'QDomEntity', + # 'QDomEntityReference', + # 'QDomImplementation', + # 'QDomNamedNodeMap', + # 'QDomNode', + # 'QDomNodeList', + # 'QDomNotation', + # 'QDomProcessingInstruction', + # 'QDomText', + # 'QXmlAttributes', + # 'QXmlContentHandler', + # 'QXmlDTDHandler', + # 'QXmlDeclHandler', + # 'QXmlDefaultHandler', + # 'QXmlEntityResolver', + # 'QXmlErrorHandler', + # 'QXmlInputSource', + # 'QXmlLexicalHandler', + # 'QXmlLocator', + # 'QXmlNamespaceSupport', + # 'QXmlParseException', + # 'QXmlReader', + # 'QXmlSimpleReader' + # ], + # 'QtXmlPatterns': [ + # 'QAbstractMessageHandler', + # 'QAbstractUriResolver', + # 'QAbstractXmlNodeModel', + # 'QAbstractXmlReceiver', + # 'QSourceLocation', + # 'QXmlFormatter', + # 'QXmlItem', + # 'QXmlName', + # 'QXmlNamePool', + # 'QXmlNodeModelIndex', + # 'QXmlQuery', + # 'QXmlResultItems', + # 'QXmlSchema', + # 'QXmlSchemaValidator', + # 'QXmlSerializer' + # ] + 'QtQml': [ + 'qmlRegisterType', + 'qmlRegisterUncreatableType', + 'QQmlApplicationEngine', + ], + 'QtQuick': [ + 'QQuickPaintedItem', + 'QQuickView', + ], +} + +#################################################################################################### + +"""Misplaced members + +These members from the original submodule are misplaced relative PySide2 + +""" + +_misplaced_members = { + 'PySide2': { + 'QtCore.QStringListModel': 'QtCore.QStringListModel', + 'QtGui.QStringListModel': 'QtCore.QStringListModel', + 'QtCore.Property': 'QtCore.Property', + 'QtCore.Signal': 'QtCore.Signal', + 'QtCore.Slot': 'QtCore.Slot', + 'QtCore.QAbstractProxyModel': 'QtCore.QAbstractProxyModel', + 'QtCore.QSortFilterProxyModel': 'QtCore.QSortFilterProxyModel', + 'QtCore.QItemSelection': 'QtCore.QItemSelection', + 'QtCore.QItemSelectionModel': 'QtCore.QItemSelectionModel', + 'QtCore.QItemSelectionRange': 'QtCore.QItemSelectionRange', + # 'QtUiTools.QUiLoader': ['QtCompat.loadUi', _loadUi], + # 'shiboken2.wrapInstance': ['QtCompat.wrapInstance', _wrapinstance], + # 'shiboken2.getCppPointer': ['QtCompat.getCppPointer', _getcpppointer], + 'QtWidgets.qApp': 'QtWidgets.QApplication.instance()', + # 'QtCore.QCoreApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtWidgets.QApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtCore.qInstallMessageHandler': [ + # 'QtCompat.qInstallMessageHandler', _qInstallMessageHandler + # ], + }, + 'PyQt5': { + 'QtCore.pyqtProperty': 'QtCore.Property', + 'QtCore.pyqtSignal': 'QtCore.Signal', + 'QtCore.pyqtSlot': 'QtCore.Slot', + 'QtCore.QAbstractProxyModel': 'QtCore.QAbstractProxyModel', + 'QtCore.QSortFilterProxyModel': 'QtCore.QSortFilterProxyModel', + 'QtCore.QStringListModel': 'QtCore.QStringListModel', + 'QtCore.QItemSelection': 'QtCore.QItemSelection', + 'QtCore.QItemSelectionModel': 'QtCore.QItemSelectionModel', + 'QtCore.QItemSelectionRange': 'QtCore.QItemSelectionRange', + # 'uic.loadUi': ['QtCompat.loadUi', _loadUi], + # 'sip.wrapinstance': ['QtCompat.wrapInstance', _wrapinstance], + # 'sip.unwrapinstance': ['QtCompat.getCppPointer', _getcpppointer], + 'QtWidgets.qApp': 'QtWidgets.QApplication.instance()', + # 'QtCore.QCoreApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtWidgets.QApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtCore.qInstallMessageHandler': [ + # 'QtCompat.qInstallMessageHandler', _qInstallMessageHandler + # ], + }, +} + +#################################################################################################### + +"""Compatibility Members + +This dictionary is used to build Qt.QtCompat objects that provide a consistent +interface for obsolete members, and differences in binding return values. + +{ + 'binding': { + 'classname': { + 'targetname': 'binding_namespace', + } + } +} + +""" + +_compatibility_members = { + 'PySide2': { + # 'QWidget': { + # 'grab': 'QtWidgets.QWidget.grab', + # }, + # 'QHeaderView': { + # 'sectionsClickable': 'QtWidgets.QHeaderView.sectionsClickable', + # 'setSectionsClickable': + # 'QtWidgets.QHeaderView.setSectionsClickable', + # 'sectionResizeMode': 'QtWidgets.QHeaderView.sectionResizeMode', + # 'setSectionResizeMode': + # 'QtWidgets.QHeaderView.setSectionResizeMode', + # 'sectionsMovable': 'QtWidgets.QHeaderView.sectionsMovable', + # 'setSectionsMovable': 'QtWidgets.QHeaderView.setSectionsMovable', + # }, + # 'QFileDialog': { + # 'getOpenFileName': 'QtWidgets.QFileDialog.getOpenFileName', + # 'getOpenFileNames': 'QtWidgets.QFileDialog.getOpenFileNames', + # 'getSaveFileName': 'QtWidgets.QFileDialog.getSaveFileName', + # }, + }, + 'PyQt5': { + # 'QWidget': { + # 'grab': 'QtWidgets.QWidget.grab', + # }, + # 'QHeaderView': { + # 'sectionsClickable': 'QtWidgets.QHeaderView.sectionsClickable', + # 'setSectionsClickable': + # 'QtWidgets.QHeaderView.setSectionsClickable', + # 'sectionResizeMode': 'QtWidgets.QHeaderView.sectionResizeMode', + # 'setSectionResizeMode': + # 'QtWidgets.QHeaderView.setSectionResizeMode', + # 'sectionsMovable': 'QtWidgets.QHeaderView.sectionsMovable', + # 'setSectionsMovable': 'QtWidgets.QHeaderView.setSectionsMovable', + # }, + # 'QFileDialog': { + # 'getOpenFileName': 'QtWidgets.QFileDialog.getOpenFileName', + # 'getOpenFileNames': 'QtWidgets.QFileDialog.getOpenFileNames', + # 'getSaveFileName': 'QtWidgets.QFileDialog.getSaveFileName', + # }, + }, +} diff --git a/src/QtShim/Wrapper.py b/src/QtShim/Wrapper.py new file mode 100644 index 0000000..a398f00 --- /dev/null +++ b/src/QtShim/Wrapper.py @@ -0,0 +1,34 @@ +#################################################################################################### + +def _qInstallMessageHandler(handler): + '''Install a message handler that works in all bindings + + Args: + handler: A function that takes 3 arguments, or None + ''' + + def messageOutputHandler(*args): + # In Qt5 bindings, message handlers are passed 3 arguments + # The first argument is a QtMsgType + # The last argument is the message to be printed + # The Middle argument (if passed) is a QMessageLogContext + if len(args) == 3: + msgType, logContext, msg = args + elif len(args) == 2: + msgType, msg = args + logContext = None + else: + raise TypeError( + 'handler expected 2 or 3 arguments, got {0}'.format(len(args))) + + if isinstance(msg, bytes): + # In python 3, some bindings pass a bytestring, which cannot be + # used elsewhere. Decoding a python 2 or 3 bytestring object will + # consistently return a unicode object. + msg = msg.decode() + + handler(msgType, logContext, msg) + + if not handler: + handler = messageOutputHandler + return Qt._QtCore.qInstallMessageHandler(handler) diff --git a/src/QtShim/__init__.py b/src/QtShim/__init__.py new file mode 100644 index 0000000..0438cc0 --- /dev/null +++ b/src/QtShim/__init__.py @@ -0,0 +1,389 @@ +"""Minimal Python 3 shim around PyQt5 and Pyside2 Qt bindings for QML applications. + +Forked from https://github.com/mottosso/Qt.py under MIT License. +Copyright (c) 2016 Marcus Ottosson + +Changes + +* Dropped Python2 and Qt4 support +* Focus on last Python 3 release +* Focus on last Qt API : QML + +Requirements + +* make use of lazy loading to speed up startup time ! +""" + +# Fixme: ressource file CodeReview/QtApplication/rcc/CodeReviewRessource.py + +#################################################################################################### + +# Enable support for `from Qt import *` +__all__ = [] + +#################################################################################################### + +import importlib +import logging +import os +import sys +import types + +from .QtConfig import _common_members, _misplaced_members, _compatibility_members + +#################################################################################################### + +# _module_logger = logging.getLogger(__name__) + +#################################################################################################### + +# Flags from environment variables +QT_VERBOSE = bool(os.getenv('QT_VERBOSE')) + +QT_PREFERRED_BINDING = os.getenv('QT_PREFERRED_BINDING', '') +if QT_PREFERRED_BINDING: + QT_PREFERRED_BINDING = list(x for x in QT_PREFERRED_BINDING.split(',') if x) +else: + # on dec 2018, PySide2 is still not fully operational + QT_PREFERRED_BINDING = ('PyQt5', 'PySide2') + +#################################################################################################### + +def _new_module(name): + return types.ModuleType(__name__ + '.' + name) + +#################################################################################################### + +# Reference to Qt.py +Qt = sys.modules[__name__] +Qt.QtCompat = _new_module('QtCompat') + +#################################################################################################### + +def _log(text): + if QT_VERBOSE: + # _logger print + sys.stdout.write(text + '\n') + +#################################################################################################### + +def _import_sub_module(module, name): + """import a submodule""" + _log('_import_sub_module {} {}'.format(module, name)) + module_name = module.__name__ + '.' + name # e.g. PyQt5.QtCore + module = importlib.import_module(module_name) + return module + +#################################################################################################### + +def _setup(module, extras): + """Install common submodules""" + + Qt.__binding__ = module.__name__ + + for name in list(_common_members) + extras: + try: + submodule = _import_sub_module(module, name) + except ImportError: + try: + # For extra modules like sip and shiboken that may not be + # children of the binding. + submodule = __import__(name) + except ImportError: + continue + + setattr(Qt, '_' + name, submodule) + + if name not in extras: + # Store reference to original binding + setattr(Qt, name, _new_module(name)) # Qt.QtCore = module(so module) + +#################################################################################################### + +def _reassign_misplaced_members(binding): + """Apply misplaced members from `binding` to Qt.py + + Arguments: + binding (dict): Misplaced members + + """ + + for src, dst in _misplaced_members[binding].items(): + # print() + dst_value = None + + # Fixme: to func + src_parts = src.split('.') + src_module = src_parts[0] + if len(src_parts): + src_member = src_parts[1:] + else: + src_member = None + + if isinstance(dst, (list, tuple)): + dst, dst_value = dst + # print(src, '->', dst, dst_value) + # print(src_module, src_member) + + dst_parts = dst.split('.') + dst_module = dst_parts[0] + if len(dst_parts): + dst_member = dst_parts[1] + else: + dst_member = None + # print(dst_module, dst_member) + + # Get the member we want to store in the namesapce. + if not dst_value: + try: + _part = getattr(Qt, '_' + src_module) + while src_member: + member = src_member.pop(0) + _part = getattr(_part, member) + dst_value = _part + except AttributeError: + # If the member we want to store in the namespace does not + # exist, there is no need to continue. This can happen if a + # request was made to rename a member that didn't exist, for + # example if QtWidgets isn't available on the target platform. + _log('Misplaced member has no source: {0}'.format(src)) + continue + # print(dst_value) + + try: + # Fixme: src_object ??? + src_object = getattr(Qt, dst_module) + except AttributeError: + # print('Failed to get src_object') + if dst_module not in _common_members: + # Only create the Qt parent module if its listed in + # _common_members. Without this check, if you remove QtCore + # from _common_members, the default _misplaced_members will add + # Qt.QtCore so it can add Signal, Slot, etc. + msg = "Not creating missing member module '{m}' for '{c}'" + _log(msg.format(m=dst_module, c=dst_member)) + continue + # If the dst is valid but the Qt parent module does not exist + # then go ahead and create a new module to contain the member. + setattr(Qt, dst_module, _new_module(dst_module)) + src_object = getattr(Qt, dst_module) + # Enable direct import of the new module + sys.modules[__name__ + '.' + dst_module] = src_object + + if not dst_value: + dst_value = getattr(Qt, '_' + src_module) + if src_member: + dst_value = getattr(dst_value, src_member) + + setattr( + src_object, + dst_member or dst_module, + dst_value + ) + +#################################################################################################### + +def _build_compatibility_members(binding, decorators=None): + """Apply `binding` to QtCompat + + Arguments: + binding (str): Top level binding in _compatibility_members. + decorators (dict, optional): Provides the ability to decorate the + original Qt methods when needed by a binding. This can be used + to change the returned value to a standard value. The key should + be the classname, the value is a dict where the keys are the + target method names, and the values are the decorator functions. + + """ + + decorators = decorators or dict() + + # Allow optional site-level customization of the compatibility members. + # This method does not need to be implemented in QtSiteConfig. + try: + import QtSiteConfig + except ImportError: + pass + else: + if hasattr(QtSiteConfig, 'update_compatibility_decorators'): + QtSiteConfig.update_compatibility_decorators(binding, decorators) + + _QtCompat = type('QtCompat', (object,), {}) + + for classname, bindings in _compatibility_members[binding].items(): + attrs = {} + for target, binding in bindings.items(): + namespaces = binding.split('.') + try: + src_object = getattr(Qt, '_' + namespaces[0]) + except AttributeError as e: + _log('QtCompat: AttributeError: %s' % e) + # Skip reassignment of non-existing members. + # This can happen if a request was made to + # rename a member that didn't exist, for example + # if QtWidgets isn't available on the target platform. + continue + + # Walk down any remaining namespace getting the object assuming + # that if the first namespace exists the rest will exist. + for namespace in namespaces[1:]: + src_object = getattr(src_object, namespace) + + # decorate the Qt method if a decorator was provided. + if target in decorators.get(classname, []): + # staticmethod must be called on the decorated method to + # prevent a TypeError being raised when the decorated method + # is called. + src_object = staticmethod( + decorators[classname][target](src_object)) + + attrs[target] = src_object + + # Create the QtCompat class and install it into the namespace + compat_class = type(classname, (_QtCompat,), attrs) + setattr(Qt.QtCompat, classname, compat_class) + +#################################################################################################### + +def _pyside2(): + """Initialise PySide2 + + These functions serve to test the existence of a binding + along with set it up in such a way that it aligns with + the final step; adding members from the original binding + to Qt.py + + """ + + import PySide2 as module + extras = [] + # try: + # from PySide2 import shiboken2 + # extras.append('shiboken2') + # except ImportError: + # pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + # if hasattr(Qt, '_shiboken2'): + # Qt.QtCompat.wrapInstance = _wrapinstance + # Qt.QtCompat.getCppPointer = _getcpppointer + # Qt.QtCompat.delete = shiboken2.delete + + if hasattr(Qt, '_QtCore'): + Qt.__qt_version__ = Qt._QtCore.qVersion() + + # if hasattr(Qt, '_QtWidgets'): + # Qt.QtCompat.setSectionResizeMode = \ + # Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members('PySide2') + # _build_compatibility_members('PySide2') + +#################################################################################################### + +def _pyqt5(): + """Initialise PyQt5""" + + import PyQt5 as module + extras = [] + # try: + # import sip + # extras.append(sip.__name__) + # except ImportError: + # sip = None + + _setup(module, extras) + # if hasattr(Qt, '_sip'): + # Qt.QtCompat.wrapInstance = _wrapinstance + # Qt.QtCompat.getCppPointer = _getcpppointer + # Qt.QtCompat.delete = sip.delete + + if hasattr(Qt, '_QtCore'): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + + # if hasattr(Qt, '_QtWidgets'): + # Qt.QtCompat.setSectionResizeMode = \ + # Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members('PyQt5') + # _build_compatibility_members('PyQt5') + +#################################################################################################### + +def _install(): + + # Default order (customise order and content via QT_PREFERRED_BINDING) + order = QT_PREFERRED_BINDING + + available = { + 'PySide2': _pyside2, + 'PyQt5': _pyqt5, + } + + _log("Order: {}".format(' '.join(order))) + + found_binding = False + for name in order: + _log('Trying %s' % name) + + try: + available[name]() + found_binding = True + break + + except ImportError as e: + _log('ImportError: %s' % e) + + except KeyError: + _log("ImportError: Preferred binding '%s' not found." % name) + + if not found_binding: + # If not binding were found, throw this error + raise ImportError('No Qt binding were found.') + + # Install individual members + for name, members in _common_members.items(): + try: + their_submodule = getattr(Qt, '_' + name) + except AttributeError: + continue + + our_submodule = getattr(Qt, name) + + # Enable import * + __all__.append(name) + + # Enable direct import of submodule, + # e.g. import Qt.QtCore + sys.modules[__name__ + '.' + name] = our_submodule + + for member in members: + # Accept that a submodule may miss certain members. + try: + their_member = getattr(their_submodule, member) + except AttributeError: + _log("'%s.%s' was missing." % (name, member)) + continue + setattr(our_submodule, member, their_member) + + # Enable direct import of QtCompat + sys.modules['Qt.QtCompat'] = Qt.QtCompat + +#################################################################################################### + +_install() + +#################################################################################################### + +# Fixme: Python 3.7 +# def __getattr__(name): +# print('__getattr__', name) + +#################################################################################################### + +# Setup Binding Enum states +Qt.IsPySide2 = Qt.__binding__ == 'PySide2' +Qt.IsPyQt5 = not Qt.IsPySide2 diff --git a/src/QtShim/not-implemented.py b/src/QtShim/not-implemented.py new file mode 100644 index 0000000..be022e1 --- /dev/null +++ b/src/QtShim/not-implemented.py @@ -0,0 +1,116 @@ +#################################################################################################### + +def _getcpppointer(object): + if hasattr(Qt, '_shiboken2'): + return getattr(Qt, '_shiboken2').getCppPointer(object)[0] + elif hasattr(Qt, '_sip'): + return getattr(Qt, '_sip').unwrapinstance(object) + raise AttributeError("'module' has no attribute 'getCppPointer'") + +#################################################################################################### + +def _wrapinstance(ptr, base=None): + '''Enable implicit cast of pointer to most suitable class + + This behaviour is available in sip per default. + + Based on http://nathanhorne.com/pyqtpyside-wrap-instance + + Usage: + This mechanism kicks in under these circumstances. + 1. Qt.py is using PySide 1 or 2. + 2. A `base` argument is not provided. + + See :func:`QtCompat.wrapInstance()` + + Arguments: + ptr (int): Pointer to QObject in memory + base (QObject, optional): Base class to wrap with. Defaults to QObject, + which should handle anything. + + ''' + + assert isinstance(ptr, int), "Argument 'ptr' must be of type " + assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( + "Argument 'base' must be of type ") + + if Qt.IsPyQt4 or Qt.IsPyQt5: + func = getattr(Qt, '_sip').wrapinstance + elif Qt.IsPySide2: + func = getattr(Qt, '_shiboken2').wrapInstance + elif Qt.IsPySide: + func = getattr(Qt, '_shiboken').wrapInstance + else: + raise AttributeError("'module' has no attribute 'wrapInstance'") + + if base is None: + q_object = func(int(ptr), Qt.QtCore.QObject) + meta_object = q_object.metaObject() + class_name = meta_object.className() + super_class_name = meta_object.superClass().className() + + if hasattr(Qt.QtWidgets, class_name): + base = getattr(Qt.QtWidgets, class_name) + + elif hasattr(Qt.QtWidgets, super_class_name): + base = getattr(Qt.QtWidgets, super_class_name) + + else: + base = Qt.QtCore.QObject + + return func(int(ptr), base) + +#################################################################################################### + +def _translate(context, sourceText, *args): + # In Qt4 bindings, translate can be passed 2 or 3 arguments + # In Qt5 bindings, translate can be passed 2 arguments + # The first argument is disambiguation[str] + # The last argument is n[int] + # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] + if len(args) == 3: + disambiguation, encoding, n = args + elif len(args) == 2: + disambiguation, n = args + encoding = None + else: + raise TypeError( + 'Expected 4 or 5 arguments, got {0}.'.format(len(args) + 2)) + + if hasattr(Qt.QtCore, 'QCoreApplication'): + app = getattr(Qt.QtCore, 'QCoreApplication') + else: + raise NotImplementedError( + 'Missing QCoreApplication implementation for {binding}'.format( + binding=Qt.__binding__, + ) + ) + if Qt.__binding__ in ('PySide2', 'PyQt5'): + sanitized_args = [context, sourceText, disambiguation, n] + else: + sanitized_args = [ + context, + sourceText, + disambiguation, + encoding or app.CodecForTr, + n + ] + return app.translate(*sanitized_args) + +#################################################################################################### +#################################################################################################### + +def _none(): + '''Internal option (used in installer)''' + + Mock = type('Mock', (), {'__getattr__': lambda Qt, attr: None}) + + Qt.__binding__ = 'None' + Qt.__qt_version__ = '0.0.0' + Qt.__binding_version__ = '0.0.0' + Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None + Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None + + for submodule in _common_members.keys(): + setattr(Qt, submodule, Mock()) + setattr(Qt, '_' + submodule, Mock()) diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 0000000..ce165f0 --- /dev/null +++ b/tasks/__init__.py @@ -0,0 +1,37 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +# http://www.pyinvoke.org + +#################################################################################################### + +from invoke import task, Collection + # import sys + +#################################################################################################### + +from . import clean +from . import doc +from . import release + +ns = Collection() +ns.add_collection(Collection.from_module(clean)) +ns.add_collection(Collection.from_module(release)) +ns.add_collection(Collection.from_module(doc)) diff --git a/tasks/clean.py b/tasks/clean.py new file mode 100644 index 0000000..2ef1b71 --- /dev/null +++ b/tasks/clean.py @@ -0,0 +1,38 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from invoke import task + +#################################################################################################### + +@task +def clean_flycheck(ctx): + with ctx.cd(ctx.Package): + ctx.run('find . -name "flycheck*.py" -exec rm {} \;') + +@task +def clean_emacs_backup(ctx): + ctx.run('find . -name "*~" -type f -exec rm -f {} \;') + +@task(clean_flycheck, clean_emacs_backup) +def clean(ctx): + pass diff --git a/tasks/data/license-template.py b/tasks/data/license-template.py new file mode 100644 index 0000000..1de9fa0 --- /dev/null +++ b/tasks/data/license-template.py @@ -0,0 +1,19 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### diff --git a/tasks/data/license-template.qml b/tasks/data/license-template.qml new file mode 100644 index 0000000..63f63df --- /dev/null +++ b/tasks/data/license-template.qml @@ -0,0 +1,19 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ diff --git a/tasks/doc.py b/tasks/doc.py new file mode 100644 index 0000000..7e6af7d --- /dev/null +++ b/tasks/doc.py @@ -0,0 +1,90 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path + +from invoke import task + +#################################################################################################### + +from .clean import clean_flycheck as clean_flycheck + +#################################################################################################### + +CODEREVIEW_SOURCE_PATH = Path(__file__).resolve().parents[1] + +SPHINX_PATH = CODEREVIEW_SOURCE_PATH.joinpath('doc', 'sphinx') +BUILD_PATH = SPHINX_PATH.joinpath('build') +RST_SOURCE_PATH = SPHINX_PATH.joinpath('source') +RST_API_PATH = RST_SOURCE_PATH.joinpath('api') +RST_EXAMPLES_PATH = RST_SOURCE_PATH.joinpath('examples') + +#################################################################################################### + +@task +def clean_api(ctx): + ctx.run('rm -rf doc/sphinx/source/api') + +#################################################################################################### + +@task(clean_flycheck, clean_api) +def make_api(ctx): + print('\nGenerate RST API files') + ctx.run('pyterate-rst-api {0.Package}'.format(ctx)) + print('\nRun Sphinx') + with ctx.cd('doc/sphinx/'): + ctx.run('./make-html') #--clean + +#################################################################################################### + +@task +def run_sphinx(ctx): + print() + print('Run Sphinx') + working_path = SPHINX_PATH + # subprocess.run(('make-html'), cwd=working_path) + # --clean + with ctx.cd(str(working_path)): + ctx.run('make-html') + +@task(run_sphinx) +def publish(ctx): + with ctx.cd(str(CODEREVIEW_SOURCE_PATH.joinpath('gh-pages'))): + ctx.run('update-gh-pages') + +#################################################################################################### + +@task() +def make_readme(ctx): + from setup_data import long_description + with open('README.rst', 'w') as fh: + fh.write(long_description) + # import subprocess + # subprocess.call(('rst2html', 'README.rst', 'README.html')) + ctx.run('rst2html README.rst README.html') + +#################################################################################################### + +@task +def update_authors(ctx): + # Keep authors in the order of appearance and use awk to filter out dupes + ctx.run("git log --format='- %aN <%aE>' --reverse | awk '!x[$0]++' > AUTHORS") diff --git a/tasks/release.py b/tasks/release.py new file mode 100644 index 0000000..f25e113 --- /dev/null +++ b/tasks/release.py @@ -0,0 +1,200 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path +import re +import shutil + +from invoke import task + +#################################################################################################### + +STANDARD_PACKAGES = ( + 'argparse', + 'atexit', + 'datetime', + 'glob', + 'hashlib', + 'importlib', + 'json', + 'logging', + 'operator', + 'os', + 'pathlib', + 'shutil', + 'signal', + 'stat', + 'subprocess', + 'sys', + 'time', + 'traceback', +) + +@task() +def show_import(ctx): + package = ctx.Package + with ctx.cd(package): + result = ctx.run("grep -r -h -E '^(import|from) [a-zA-Z]' . | sort | uniq", hide='out') + imports = set() + for line in result.stdout.split('\n'): + if line.startswith('from'): + position = line.find('import') + line = line[5:position] + elif line.startswith('import'): + line = line[7:] + # print('|{}|'.format(line)) + position = line.find('.') + if position != -1: + line = line[:position] + line = line.strip() + if line: + imports.add(line) + imports -= set(STANDARD_PACKAGES) + imports -= set((package,)) + for item in sorted(imports): + print(item) + +@task +def find_package(ctx, name): + ctx.run('pip freeze | grep -i {}'.format(name)) + +#################################################################################################### + +@task() +def update_git_sha(ctx): + # Fixme: wrong workflow, must tag the last commit + result = ctx.run('git describe --tags --abbrev=0 --always', hide='out') + tag = result.stdout.strip() + print(f"Tag is {tag}") + if tag.startswith('v'): + version = tag[1:] + version = version.replace('-branched', '') + else: + version = tag + if not re.match('\d+(\.\d+(\.\d+)?)?', version): + raise ValueError('Invalid version {}'.format(version)) + result = ctx.run('git rev-parse HEAD', hide='out') + sha = result.stdout.strip() + print(f"SHA {sha}") + print(f"Tag {tag}") + print(f"Version {version}") + filename = Path(ctx.Package, '__init__.py') + with open(str(filename), 'r') as fh: + lines = fh.readlines() + with open(filename, 'w') as fh: + for line in lines: + if line.startswith('__version__'): + line = "__version__ = '{}'\n".format(version) + if line.startswith('GIT_TAG'): + line = "GIT_TAG = '{}'\n".format(tag) + if line.startswith('GIT_SHA'): + line = "GIT_SHA = '{}'\n".format(sha) + fh.write(line) + +#################################################################################################### + +@task() +def clean(ctx): + for directory in ('build', 'dist', 'PySpice.egg-info'): + shutil.rmtree(directory, ignore_errors=True) + +@task() +def show_python_site(ctx): + ctx.run('python3 -m site') + +@task(update_git_sha) +def build(ctx): + ctx.run('python3 setup.py build') + +@task(build) +def install(ctx): + ctx.run('python3 setup.py install') + +# @task(clean, build) +# def sdist(ctx): +# ctx.run('python3 setup.py sdist') + +# @task(clean, build) +# def bdist(ctx): +# ctx.run('python3 setup.py bdist') + +@task(clean, build) +def wheel(ctx): + ctx.run('python3 setup.py sdist') + ctx.run('python3 setup.py bdist_wheel') + +@task(wheel) +def upload(ctx): + # Twine documentation: https://pypi.org/project/twine/ + # ctx.run('twine register dist/*whl') + # Sign using + # BA24CE0F65CB8C67 Fabrice SALVAIRE + # registered on key servers: hkps://hkps.pool.sks-keyservers.net hkp://pgp.mit.edu + ctx.run('gpg --detach-sign --armor dist/*tar.gz') + ctx.run('gpg --detach-sign --armor dist/*whl') + # Binary wheel 'CodeReview-1.1.0-cp310-cp310-linux_x86_64.whl' has an unsupported platform tag 'linux_x86_64'. + # see https://github.com/pypa/manylinux + # see https://github.com/pypa/auditwheel + ctx.run('twine upload dist/*tar.gz') + +def _get_pipy_json(): + import requests + response = requests.get('https://pypi.org/pypi/PySpice/json') + assert(response.status_code == requests.codes.ok) + return response.json() + +@task() +def get_pypi_json(ctx): + import json + print(json.dumps(_get_pipy_json(), sort_keys=True, indent=4)) + +@task() +def get_wheel(ctx): + # https://dzone.com/articles/package-signing-in-pip-it-works-in-a-roundabout-so + data = _get_pipy_json() + version = data['info']['version'] + filename = data['urls'][0]['filename'] + wheel_url = data['urls'][0]['url'] + print('Get version {}'.format(version)) + ctx.run('curl --output {} {}'.format(filename, wheel_url)) + asc_suffix = '.asc' + filename_asc = filename + asc_suffix + ctx.run('curl --output {} {}'.format(filename_asc, wheel_url + asc_suffix)) + # ctx.run('gpg --keyserver pgp.mit.edu --search-keys {}'.format(key_id)) + ctx.run('gpg --verify {} {}'.format(filename_asc, filename)) + +#################################################################################################### + +@task() +def get_github_tar_sha(ctx): + # Fixme: check git sha + result = ctx.run('git describe --tags --abbrev=0 --always', hide='out') + tag = result.stdout.strip() + url = 'https://github.com/FabriceSalvaire/PySpice/archive/{}.tar.gz'.format(tag) + print('Get', url) + import hashlib + import requests + response = requests.get(url, allow_redirects=True) + assert(response.status_code == requests.codes.ok) + sha = hashlib.sha256(response.content) + print(sha.hexdigest()) + diff --git a/tasks/scripts/Makefile b/tasks/scripts/Makefile new file mode 100644 index 0000000..45e869a --- /dev/null +++ b/tasks/scripts/Makefile @@ -0,0 +1,18 @@ +# -*- Makefile -*- + +#################################################################################################### + +all: code-review.rcc CodeReviewRessource.py + +#################################################################################################### + +%.rcc : %.qrc + rcc-qt5 -binary $< -o $@ + +CodeReviewRessource.py : code-review.qrc + pyrcc5 -o $@ $< + +#################################################################################################### + +clean: + rm *.py *.rcc diff --git a/tasks/scripts/get-material-icon b/tasks/scripts/get-material-icon new file mode 100755 index 0000000..61673a4 --- /dev/null +++ b/tasks/scripts/get-material-icon @@ -0,0 +1,184 @@ +#! /usr/bin/env python3 + +#################################################################################################### + +# https://material.io/tools/icons/static/icons/baseline-save-24px.svg +# https://material.io/tools/icons/static/icons/baseline-save-black-36.zip + +# https://material.io/tools/icons/static/icons/outline-save-black-36.zip +# https://material.io/tools/icons/static/icons/round-save-black-36.zip +# https://material.io/tools/icons/static/icons/sharp-save-black-36.zip +# https://material.io/tools/icons/static/icons/twotone-save-black-36.zip + +# https://material.io/tools/icons/static/icons/baseline-save-white-36.zip + +# 1x/baseline_save_black_18dp.png: PNG image data, 18 x 18, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_24dp.png: PNG image data, 24 x 24, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_36dp.png: PNG image data, 36 x 36, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_48dp.png: PNG image data, 48 x 48, 8-bit gray+alpha, non-interlaced + +# 2x/baseline_save_black_18dp.png: PNG image data, 36 x 36, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_24dp.png: PNG image data, 48 x 48, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_36dp.png: PNG image data, 72 x 72, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_48dp.png: PNG image data, 96 x 96, 8-bit gray+alpha, non-interlaced + +#################################################################################################### + +from pathlib import Path +from zipfile import ZipFile +import argparse +import os +import shutil +import tempfile +import urllib3 + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Fetch material icon.') + +parser.add_argument( + 'src_name', metavar='NAME', + help='icon name', +) + +parser.add_argument( + '--dst-name', + default=None, + help='dst name, default is same name', +) + +parser.add_argument( + '--style', + default='baseline', + help='style: [baseline], outline, round, twotone, sharp', +) + +parser.add_argument( + '--color', + default='black', + help='color: [black], white', +) + +args = parser.parse_args() + +#################################################################################################### + +urllib3.disable_warnings() + +#################################################################################################### + +class MaterialIconFetcher: + + SCALE = (1, 2) + DP = (18, 24, 36, 48) + + ############################################## + + def __init__(self, icons_path, theme): + + self._icons_path = Path(str(icons_path)).resolve() + self._theme = str(theme) + self._theme_path = self._icons_path.joinpath(self._theme) + + if not self._icons_path.exists(): + os.mkdir(self._icons_path) + if not self._theme_path.exists(): + os.mkdir(self._theme_path) + + self._http = urllib3.PoolManager() + + # with tempfile.TemporaryDirectory() as tmp_directory: + self._tmp_directory = tempfile.TemporaryDirectory() + self._tmp_directory_path = Path(self._tmp_directory.name) + print('tmp_directory', self._tmp_directory_path) + + ############################################## + + def _fetch_ressource(self, url): + print('Fetch', url, '...') + request = self._http.request('GET', url) + return request.data + + ############################################## + + def _fetch_png_icon(self, **kwargs): + + # https://material.io/tools/icons/static/icons/baseline-save-black-36.zip + root = 'https://material.io/tools/icons/static/icons/' + url_pattern = '{style}-{name}-{color}-{dp}.zip' + filename = url_pattern.format(**kwargs) + url = root + filename + + data = self._fetch_ressource(url) + + return filename, data + + ############################################## + + def _extract_png_archive(self, **kwargs): + + filename, data = self._fetch_png_icon(**kwargs) + zip_path = self._tmp_directory_path.joinpath(filename) + with open(zip_path, 'wb') as fh: + fh.write(data) + with ZipFile(zip_path, 'r') as zip_archive: + zip_archive.extractall(self._tmp_directory_path) + + ############################################## + + def fetch_icon(self, src_name, dst_name, style, color): + + kwargs = dict(src_name=src_name, dst_name=dst_name, style=style, color=color) + + for dp in self.DP: + self._extract_png_archive(name=src_name, dp=dp, **kwargs) + + print() + for scale in self.SCALE: + for dp in self.DP: + dst_kwargs = dict(kwargs) + dst_kwargs.update(dict(scale=scale, dp=dp)) + + # 1x/baseline_save_black_18dp.png + filename_pattern = '{style}_{src_name}_{color}_{dp}dp.png' + src_path = self._tmp_directory_path.joinpath( + '{scale}x'.format(**dst_kwargs), + filename_pattern.format(**dst_kwargs), + ) + + if scale > 1: + size_directory = '{dp}x{dp}@{scale}'.format(**dst_kwargs) + else: + size_directory = '{dp}x{dp}'.format(**dst_kwargs) + size_path = self._theme_path.joinpath(size_directory) + if not size_path.exists(): + os.mkdir(size_path) + filename_pattern = '{dst_name}-{color}.png' + filename = filename_pattern.format(**dst_kwargs) + dst_path = size_path.joinpath(filename) + + # print('Copy', src_path, dst_path) + shutil.copyfile(src_path, dst_path) + + rcc_pattern = 'icons/{}/{}/{}' + rcc_line = rcc_pattern.format(self._theme, size_directory, filename) + if dp != 36: + rcc_line = ''.format(rcc_line) + rcc_line = ' '*8 + rcc_line + print(rcc_line) + +#################################################################################################### + +root_path = Path(__file__).resolve().parents[1] +icons_path = root_path.joinpath('share', 'icons') +theme = 'material' +print('Icons path:', icons_path, theme) + +fetcher = MaterialIconFetcher(icons_path, theme) + +fetcher.fetch_icon( + args.src_name, + args.dst_name or args.src_name.replace('_', '-'), + args.style, + args.color, +) diff --git a/tasks/scripts/make-icon-png b/tasks/scripts/make-icon-png new file mode 100755 index 0000000..b982a8d --- /dev/null +++ b/tasks/scripts/make-icon-png @@ -0,0 +1,80 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path +import argparse +import subprocess + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Generate PNG files from a SVG.') +parser.add_argument( + 'svg_path', metavar='file.svg', + help='SVG PATH', +) +args = parser.parse_args() + +#################################################################################################### + +theme = 'material' +DP = (36, 48) +SCALE = (1, 2) + +svg_path = Path(args.svg_path).resolve() +theme_path = svg_path.parents[1].joinpath(theme) +print('Theme path:', theme_path) + +#################################################################################################### + +inkscape_options = [ + '--export-area-page', + '--export-background=white', + '--export-background-opacity=0', +] + +#################################################################################################### + +def run_inkscape(svg_path, dp, scale): + filename = svg_path.name.replace('.svg', '.png') + if scale > 1: + size_directory = '{dp}x{dp}@{scale}'.format(dp=dp, scale=scale) + else: + size_directory = '{dp}x{dp}'.format(dp=dp) + png_path = theme_path.joinpath(size_directory, filename) + command = ( + 'inkscape', + *inkscape_options, + '--export-png={}'.format(png_path), + # --export-width= + '--export-height={}'.format(dp*scale), + str(svg_path), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + +#################################################################################################### + +for dp in DP: + for scale in SCALE: + run_inkscape(svg_path, dp, scale) diff --git a/tasks/scripts/make-logo-png b/tasks/scripts/make-logo-png new file mode 100755 index 0000000..d5dd866 --- /dev/null +++ b/tasks/scripts/make-logo-png @@ -0,0 +1,67 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path +import argparse +import subprocess + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Generate PNG files from a SVG.') +parser.add_argument( + 'svg_path', metavar='file.svg', + help='SVG PATH', +) +args = parser.parse_args() + +svg_path = Path(args.svg_path).resolve() + +#################################################################################################### + +sizes = (32, 64, 96, 128, 256, 512) + +inkscape_options = [ + '--export-area-page', + '--export-background=white', + '--export-background-opacity=0', +] + +#################################################################################################### + +def run_inkscape(svg_path, size): + command = ( + 'inkscape', + *inkscape_options, + '--export-png=logo-{}.png'.format(size), + # --export-width= + '--export-height={}'.format(size), + str(svg_path), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + +#################################################################################################### + +for size in sizes: + run_inkscape(svg_path, size) diff --git a/tasks/scripts/translate b/tasks/scripts/translate new file mode 100755 index 0000000..d1f0d4c --- /dev/null +++ b/tasks/scripts/translate @@ -0,0 +1,534 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Program to manage and translate Qt translation files. + +It features: + +* lupdate, linguist, lrelease +* conversion to/from .po file +* poedit +* translation using Google Translation + +Actions are run in the right order. + +""" + +#################################################################################################### + +from pathlib import Path +import argparse +import os +import subprocess + +from googletrans import Translator +# https://py-googletrans.readthedocs.io/en/latest/ + +#################################################################################################### + +DEFAULT_LANGUAGES = ( + 'fr_FR', +) + +# Fixme: project dependent +source_path = Path(__file__).resolve().parents[1] +qml_path = source_path.joinpath('qml') +py_path = source_path.joinpath('CodeReview') +translation_path = source_path.joinpath('share', 'translations') +base_name = 'code-review' + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Translate Qt Application.') + +parser.add_argument( + '--language', + default=None, + help='languages, default {}'.format(DEFAULT_LANGUAGES), +) + +parser.add_argument( + '--update', + default=False, + action='store_true', + help='run lupdate', +) + +parser.add_argument( + '--translate', + default=False, + action='store_true', + help='translate using Google Translate', +) + +parser.add_argument( + '--convert-to-po', + default=False, + action='store_true', + help='convert to .po file', +) + +parser.add_argument( + '--convert-from-po', + default=False, + action='store_true', + help='convert from .po file', +) + +parser.add_argument( + '--poedit', + default=False, + action='store_true', + help='run poedit', +) + +parser.add_argument( + '--linguist', + default=False, + action='store_true', + help='run linguist', +) + +parser.add_argument( + '--release', + default=False, + action='store_true', + help='run lrelease', +) + +args = parser.parse_args() + +#################################################################################################### + +def _clean_path(path): + return Path(str(path)).resolve() + +#################################################################################################### + +class TanslationManager: + + ############################################## + + def __init__(self, translation_path, base_name, py_path, qml_path): + + self._translation_path = _clean_path(translation_path) + self._base_name = base_name + self._py_path = _clean_path(py_path) + self._qml_path = _clean_path(qml_path) + + if not self._translation_path.exists(): + os.mkdir(self._translation_path) + + ############################################## + + def ts_path(self, scope, language): + if scope: + filename = '{}.{}.{}.ts'.format(self._base_name, scope, language) + else: + filename = '{}.{}.ts'.format(self._base_name, language) + return self._translation_path.joinpath(filename) + + def po_path(self, scope, language): + filename = '{}.{}.{}.po'.format(self._base_name, scope, language) + return self._translation_path.joinpath(filename) + + ############################################## + + def run_lupdate(self, language): + + # Usage: + # lupdate [options] [project-file]... + # lupdate [options] [source-file|path|@lst-file]... -ts ts-files|@lst-file + # + # lupdate is part of Qt's Linguist tool chain. It extracts translatable + # messages from Qt UI files, C++, Java and JavaScript/QtScript source code. + # Extracted messages are stored in textual translation source files (typically + # Qt TS XML). New and modified messages can be merged into existing TS files. + # + # Options: + # -help Display this information and exit. + # -no-obsolete + # Drop all obsolete and vanished strings. + # -extensions [,]... + # Process files with the given extensions only. + # The extension list must be separated with commas, not with whitespace. + # Default: 'java,jui,ui,c,c++,cc,cpp,cxx,ch,h,h++,hh,hpp,hxx,js,qs,qml,qrc'. + # -pluralonly + # Only include plural form messages. + # -silent + # Do not explain what is being done. + # -no-sort + # Do not sort contexts in TS files. + # -no-recursive + # Do not recursively scan the following directories. + # -recursive + # Recursively scan the following directories (default). + # -I or -I + # Additional location to look for include files. + # May be specified multiple times. + # -locations {absolute|relative|none} + # Specify/override how source code references are saved in TS files. + # Guessed from existing TS files if not specified. + # Default is absolute for new files. + # -no-ui-lines + # Do not record line numbers in references to UI files. + # -disable-heuristic {sametext|similartext|number} + # Disable the named merge heuristic. Can be specified multiple times. + # -pro + # Name of a .pro file. Useful for files with .pro file syntax but + # different file suffix. Projects are recursed into and merged. + # -pro-out + # Virtual output directory for processing subsequent .pro files. + # -pro-debug + # Trace processing .pro files. Specify twice for more verbosity. + # -source-language [_] + # Specify the language of the source strings for new files. + # Defaults to POSIX if not specified. + # -target-language [_] + # Specify the language of the translations for new files. + # Guessed from the file name if not specified. + # -tr-function-alias {+=,=}[,{+=,=}]... + # With +=, recognize as an alternative spelling of . + # With =, recognize as the only spelling of . + # Available s (with their currently defined aliases) are: + # Q_DECLARE_TR_FUNCTIONS (=Q_DECLARE_TR_FUNCTIONS) + # QT_TR_N_NOOP (=QT_TR_N_NOOP) + # QT_TRID_N_NOOP (=QT_TRID_N_NOOP) + # QT_TRANSLATE_N_NOOP (=QT_TRANSLATE_N_NOOP) + # QT_TRANSLATE_N_NOOP3 (=QT_TRANSLATE_N_NOOP3) + # QT_TR_NOOP (=QT_TR_NOOP) + # QT_TRID_NOOP (=QT_TRID_NOOP) + # QT_TRANSLATE_NOOP (=QT_TRANSLATE_NOOP) + # QT_TRANSLATE_NOOP3 (=QT_TRANSLATE_NOOP3) + # QT_TR_NOOP_UTF8 (=QT_TR_NOOP_UTF8) + # QT_TRANSLATE_NOOP_UTF8 (=QT_TRANSLATE_NOOP_UTF8) + # QT_TRANSLATE_NOOP3_UTF8 (=QT_TRANSLATE_NOOP3_UTF8) + # findMessage (=findMessage) + # qtTrId (=qtTrId) + # tr (=tr) + # trUtf8 (=trUtf8) + # translate (=translate) + # qsTr (=qsTr) + # qsTrId (=qsTrId) + # qsTranslate (=qsTranslate) + # -ts ... + # Specify the output file(s). This will override the TRANSLATIONS. + # -version + # Display the version of lupdate and exit. + # @lst-file + # Read additional file names (one per line) or includepaths (one per + # line, and prefixed with -I) from lst-file. + + command = ( + 'lupdate-qt5', + '-extensions', 'qml,js', + '-source-language', 'en_GB', + '-target-language', language, + str(self._qml_path), + '-ts', str(self.ts_path('qml', language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + py_filenames = [] + for root, _, filenames in os.walk(self._py_path): + root = Path(root) + for filename in filenames: + filename = Path(filename) + if filename.suffix == '.py': + filename = str(root.joinpath(filename)) + py_filenames.append(filename) + + command = ( + 'pylupdate5', + '-verbose', + *py_filenames, + '-ts', str(self.ts_path('py', language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + # Fixme: pylupdate5 don't support qml and folder !!! + + # print('Fix obsolete') + # with open(ts_path, 'r') as fh: + # content = fh.readlines() + # with open(ts_path, 'w') as fh: + # for line in content: + # line = line.replace('type="obsolete"', '') + # line = line.replace('', '') + # fh.write(line) + + ############################################## + + def run_linguist(self, language): + + for scope in ('qml', 'py'): + command = ( + 'linguist-qt5', + str(self.ts_path(scope, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_poedit(self, language): + + command = ( + 'poedit', + str(self.po_path(language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_lrelease(self, language): + + # Usage: + # lrelease [options] project-file + # lrelease [options] ts-files [-qm qm-file] + # + # lrelease is part of Qt's Linguist tool chain. It can be used as a + # stand-alone tool to convert XML-based translations files in the TS + # format into the 'compiled' QM format used by QTranslator objects. + # + # Options: + # -help Display this information and exit + # -idbased + # Use IDs instead of source strings for message keying + # -compress + # Compress the QM files + # -nounfinished + # Do not include unfinished translations + # -removeidentical + # If the translated text is the same as + # the source text, do not include the message + # -markuntranslated + # If a message has no real translation, use the source text + # prefixed with the given string instead + # -silent + # Do not explain what is being done + # -version + # Display the version of lrelease and exit + + command = ( + 'lconvert-qt5', + '-i', + str(self.ts_path('qml', language)), + str(self.ts_path('py', language)), + '-o', str(self.ts_path(None, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + command = ( + 'lrelease-qt5', + str(self.ts_path(None, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_lconvert(self, language, format): + + # Usage: + # lconvert [options] [...] + # + # lconvert is part of Qt's Linguist tool chain. It can be used as a + # stand-alone tool to convert and filter translation data files. + # The following file formats are supported: + # + # qm - Traductions Qt compilées + # pot - Fichiers de modèle de localisation GNU Gettext + # qph - Qt Linguist "livre de phrases" + # ts - Sources de traduction Qt + # po - Fichiers de localisation GNU Gettext + # xlf - Fichiers de localisation XLIFF + # + # If multiple input files are specified, they are merged with + # translations from later files taking precedence. + # + # Options: + # -h + # -help Display this information and exit. + # + # -i + # -input-file + # Specify input file. Use if might start with a dash. + # This option can be used several times to merge inputs. + # May be '-' (standard input) for use in a pipe. + # + # -o + # -output-file + # Specify output file. Default is '-' (standard output). + # + # -if + # -input-format + # Specify input format for subsequent s. + # The format is auto-detected from the file name and defaults to 'ts'. + # + # -of + # -output-format + # Specify output format. See -if. + # + # -drop-tags + # Drop named extra tags when writing TS or XLIFF files. + # May be specified repeatedly. + # + # -drop-translations + # Drop existing translations and reset the status to 'unfinished'. + # Note: this implies --no-obsolete. + # + # -source-language [_] + # Specify/override the language of the source strings. Defaults to + # POSIX if not specified and the file does not name it yet. + # + # -target-language [_] + # Specify/override the language of the translation. + # The target language is guessed from the file name if this option + # is not specified and the file contents name no language yet. + # + # -no-obsolete + # Drop obsolete messages. + # + # -no-finished + # Drop finished messages. + # + # -no-untranslated + # Drop untranslated messages. + # + # -sort-contexts + # Sort contexts in output TS file alphabetically. + # + # -locations {absolute|relative|none} + # Override how source code references are saved in TS files. + # Default is absolute. + # + # -no-ui-lines + # Drop line numbers from references to UI files. + # + # -verbose + # be a bit more verbose + # + # Long options can be specified with only one leading dash, too. + # + # Return value: + # 0 on success + # 1 on command line parse failures + # 2 on read failures + # 3 on write failures + + is_po = format == 'po' + + for scope in ('qml', 'py'): + command = ( + 'lconvert-qt5', + '-i' if is_po else '-o', str(self.ts_path(scope, language)), + '-o' if is_po else '-i', str(self.po_path(language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def translate(self, language): + for scope in ('qml', 'py'): + self.translate_scope(scope, language) + + ############################################## + + def translate_scope(self, scope, language): + + print('Translate {} {} ...'.format(scope, language)) + + language_code = language[:2] + if language_code == 'en': + return + + translator = Translator( + service_urls=[ + 'translate.google.' + language_code, + 'translate.google.com', + ], + ) + + def translate_string(source): + return translator.translate(source, src='en', dest=language_code).text + + ts_path = self.ts_path(scope, language) + with open(ts_path, 'r') as fh: + lines = list(fh.readlines()) + + with open(ts_path, 'w') as fh: + source = None + for line in lines: + striped_line = line.strip() + if striped_line.startswith(''): + source = striped_line[striped_line.find('>')+1:striped_line.rfind('<')] + elif striped_line.startswith(''): + old_translation = striped_line[striped_line.find('>')+1:striped_line.rfind('<')] + if not old_translation: + translation = translate_string(source) + print() + print(source) + print(translation) + line = line.replace(' type="unfinished">', '>' + translation) + fh.write(line) + +#################################################################################################### + +manager = TanslationManager( + translation_path, + base_name, + py_path, + qml_path, +) + +if not args.language: + languages = DEFAULT_LANGUAGES +else: + languages = [x for x in [x.strip() for x in args.languages.split(',')] if x] + +if args.update: + for language in languages: + manager.run_lupdate(language) +if args.translate: + for language in languages: + manager.translate(language) +if args.convert_to_po: + for language in languages: + manager.run_lconvert(language, 'po') +if args.poedit: + for language in languages: + manager.run_poedit(language) +if args.convert_from_po: + for language in languages: + manager.run_lconvert(language, 'ts') +if args.linguist: + for language in languages: + manager.run_linguist(language) +if args.release: + for language in languages: + manager.run_lrelease(language) diff --git a/tasks/scripts/upload-to-pypi b/tasks/scripts/upload-to-pypi new file mode 100755 index 0000000..36398ab --- /dev/null +++ b/tasks/scripts/upload-to-pypi @@ -0,0 +1,11 @@ +#! /bin/bash + +# bdist +# python setup.py check --verbose --metadata --restructuredtext --strict && \ +# python setup.py register sdist upload + +python setup.py bdist_wheel + +twine register dist/*whl +gpg --detach-sign -a dist/*whl +twine upload dist/*