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/*