diff --git a/.travis.yml b/.travis.yml
index e90828f0..2c7b7769 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,10 +17,10 @@ env:
# Enable python 2 and python 3 builds
# Note that the 2.6 build doesn't get flake8, and runs old versions of
# Pyglet and GLFW to make sure we deal with those correctly
- - PYTHON=2.6 QT=pyqt TEST=standard
- - PYTHON=2.7 QT=pyqt TEST=extra
+ - PYTHON=2.6 QT=pyqt4 TEST=standard
+ - PYTHON=2.7 QT=pyqt4 TEST=extra
- PYTHON=2.7 QT=pyside TEST=standard
- - PYTHON=3.4 QT=pyqt TEST=standard
+ - PYTHON=3.4 QT=pyqt5 TEST=standard
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
#- PYTHON=3.2 QT=pyqt5 TEST=standard
@@ -56,9 +56,12 @@ install:
- echo ${TEST}
- echo ${PYTHON}
- - if [ "${QT}" == "pyqt" ]; then
+ - if [ "${QT}" == "pyqt5" ]; then
conda install pyqt --yes;
fi;
+ - if [ "${QT}" == "pyqt4" ]; then
+ conda install pyqt=4 --yes;
+ fi;
- if [ "${QT}" == "pyside" ]; then
conda install pyside --yes;
fi;
@@ -134,6 +137,9 @@ before_script:
script:
- source activate test_env
+
+ # Check system info
+ - python -c "import pyqtgraph as pg; pg.systemInfo()"
# Run unit tests
- start_test "unit tests";
diff --git a/CHANGELOG b/CHANGELOG
index c5c562a4..df027011 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,29 +1,53 @@
-pyqtgraph-0.9.11 [unreleased]
+pyqtgraph-0.10.0 [unreleased]
+
+ New Features:
+ - PyQt5 support
+ - Options for interpreting image data as either row-major or col-major
+ - InfiniteLine and LinearRegionItem can have attached labels
+ - DockArea:
+ - Dock titles can be changed after creation
+ - Added Dock.sigClosed
+ - Added TextItem.setColor()
+ - FillBetweenItem supports finite-connected curves (those that exclude nan/inf)
+
+ API / behavior changes:
+ - Improved ImageItem performance for some data types by scaling LUT instead of image
+ - Change the defaut color kwarg to None in TextItem.setText() to avoid changing
+ the color every time the text is changed.
+ - FFT plots skip first sample if x-axis uses log scaling
+ - Multiprocessing system adds bytes and unicode to the default list of no-proxy data types
+ - Version number scheme changed to be PEP440-compliant (only affects installations from non-
+ release git commits)
Bugfixes:
+ - Fix for numpy API change that caused casting errors for inplace operations
- Fixed git version string generation on python3
- Fixed setting default values for out-of-bound points in pg.interpolateArray
- Fixed plot downsampling bug on python 3
+ - Fixed invalid slice in ImageItem.getHistogram
- DockArea:
- Fixed adding Docks to DockArea after all Docks have been removed
- Fixed DockArea save/restoreState when area is empty
- Properly remove select box when export dialog is closed using window decorations
- - Remove all modifications to builtins
+ - Remove all modifications to python builtins
+ - Better Python 2.6 compatibility
- Fix SpinBox decimals
-
- API / behavior changes:
- - Change the defaut color kwarg to None in TextItem.setText() to avoid changing
- the color everytime the text is changed.
-
- New Features:
- - Preliminary PyQt5 support
- - DockArea:
- - Dock titles can be changed after creation
- - Added Dock.sigClosed
- - Added TextItem.setColor()
+ - Fixed numerous issues with ImageItem automatic downsampling
+ - Fixed PlotItem average curves using incorrect stepMode
+ - Fixed TableWidget eating key events
+ - Prevent redundant updating of flowchart nodes with multiple inputs
+ - Ignore wheel events in GraphicsView if mouse interaction is disabled
+ - Correctly pass calls to QWidget.close() up the inheritance chain
+ - ColorMap forces color inputs to be sorted
+ - Fixed memory mapping for RemoteGraphicsView in OSX
+ - Fixed QPropertyAnimation str/bytes handling
+ - Fixed __version__ string update when using `setup.py install` with newer setuptools
Maintenance:
+ - Image comparison system for unit testing plus tests for several graphics items
+ - Travis CI and coveralls/codecov support
- Add examples to unit tests
+
pyqtgraph-0.9.10
diff --git a/README.md b/README.md
index 68ef9ced..30268796 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ Contributors
* Martin Fitzpatrick
* Daniel Lidstrom
* Eric Dill
+ * Vincent LeSaux
Requirements
------------
diff --git a/doc/source/widgets/rawimagewidget.rst b/doc/source/widgets/rawimagewidget.rst
deleted file mode 100644
index 29fda791..00000000
--- a/doc/source/widgets/rawimagewidget.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-RawImageWidget
-==============
-
-.. autoclass:: pyqtgraph.RawImageWidget
- :members:
-
- .. automethod:: pyqtgraph.RawImageWidget.__init__
-
diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py
index 3516472f..e7189bf5 100644
--- a/examples/VideoSpeedTest.py
+++ b/examples/VideoSpeedTest.py
@@ -25,14 +25,22 @@ else:
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([])
-#mw = QtGui.QMainWindow()
-#mw.resize(800,800)
win = QtGui.QMainWindow()
win.setWindowTitle('pyqtgraph example: VideoSpeedTest')
ui = VideoTemplate.Ui_MainWindow()
ui.setupUi(win)
win.show()
+
+try:
+ from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget
+except ImportError:
+ ui.rawGLRadio.setEnabled(False)
+ ui.rawGLRadio.setText(ui.rawGLRadio.text() + " (OpenGL not available)")
+else:
+ ui.rawGLImg = RawImageGLWidget()
+ ui.stack.addWidget(ui.rawGLImg)
+
ui.maxSpin1.setOpts(value=255, step=1)
ui.minSpin1.setOpts(value=0, step=1)
diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui
index 6bde7fe2..7da18327 100644
--- a/examples/VideoTemplate.ui
+++ b/examples/VideoTemplate.ui
@@ -51,7 +51,7 @@
-
- 2
+ 1
@@ -74,13 +74,6 @@
-
-
- -
-
-
-
-
-
@@ -340,12 +333,6 @@
QDoubleSpinBox
-
- RawImageGLWidget
- QWidget
- pyqtgraph.widgets.RawImageWidget
- 1
-
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py
index e2481df7..b93bedeb 100644
--- a/examples/VideoTemplate_pyqt.py
+++ b/examples/VideoTemplate_pyqt.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file './examples/VideoTemplate.ui'
+# Form implementation generated from reading ui file 'examples/VideoTemplate.ui'
#
-# Created: Mon Feb 17 20:39:30 2014
-# by: PyQt4 UI code generator 4.10.3
+# Created by: PyQt4 UI code generator 4.11.4
#
# WARNING! All changes made in this file will be lost!
@@ -69,14 +68,6 @@ class Ui_MainWindow(object):
self.rawImg.setObjectName(_fromUtf8("rawImg"))
self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1)
self.stack.addWidget(self.page_2)
- self.page_3 = QtGui.QWidget()
- self.page_3.setObjectName(_fromUtf8("page_3"))
- self.gridLayout_5 = QtGui.QGridLayout(self.page_3)
- self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5"))
- self.rawGLImg = RawImageGLWidget(self.page_3)
- self.rawGLImg.setObjectName(_fromUtf8("rawGLImg"))
- self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1)
- self.stack.addWidget(self.page_3)
self.gridLayout.addWidget(self.stack, 0, 0, 1, 1)
self.rawGLRadio = QtGui.QRadioButton(self.centralwidget)
self.rawGLRadio.setObjectName(_fromUtf8("rawGLRadio"))
@@ -193,7 +184,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
- self.stack.setCurrentIndex(2)
+ self.stack.setCurrentIndex(1)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
@@ -217,5 +208,5 @@ class Ui_MainWindow(object):
self.rgbCheck.setText(_translate("MainWindow", "RGB", None))
self.label_5.setText(_translate("MainWindow", "Image size", None))
-from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget
-from pyqtgraph import GradientWidget, SpinBox, GraphicsView
+from pyqtgraph import GradientWidget, GraphicsView, SpinBox
+from pyqtgraph.widgets.RawImageWidget import RawImageWidget
diff --git a/examples/VideoTemplate_pyqt5.py b/examples/VideoTemplate_pyqt5.py
new file mode 100644
index 00000000..63153fb5
--- /dev/null
+++ b/examples/VideoTemplate_pyqt5.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'examples/VideoTemplate.ui'
+#
+# Created by: PyQt5 UI code generator 5.5.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(695, 798)
+ self.centralwidget = QtWidgets.QWidget(MainWindow)
+ self.centralwidget.setObjectName("centralwidget")
+ self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
+ self.gridLayout_2.setObjectName("gridLayout_2")
+ self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.downsampleCheck.setObjectName("downsampleCheck")
+ self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
+ self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.scaleCheck.setObjectName("scaleCheck")
+ self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1)
+ self.gridLayout = QtWidgets.QGridLayout()
+ self.gridLayout.setObjectName("gridLayout")
+ self.rawRadio = QtWidgets.QRadioButton(self.centralwidget)
+ self.rawRadio.setObjectName("rawRadio")
+ self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1)
+ self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget)
+ self.gfxRadio.setChecked(True)
+ self.gfxRadio.setObjectName("gfxRadio")
+ self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1)
+ self.stack = QtWidgets.QStackedWidget(self.centralwidget)
+ self.stack.setObjectName("stack")
+ self.page = QtWidgets.QWidget()
+ self.page.setObjectName("page")
+ self.gridLayout_3 = QtWidgets.QGridLayout(self.page)
+ self.gridLayout_3.setObjectName("gridLayout_3")
+ self.graphicsView = GraphicsView(self.page)
+ self.graphicsView.setObjectName("graphicsView")
+ self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1)
+ self.stack.addWidget(self.page)
+ self.page_2 = QtWidgets.QWidget()
+ self.page_2.setObjectName("page_2")
+ self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2)
+ self.gridLayout_4.setObjectName("gridLayout_4")
+ self.rawImg = RawImageWidget(self.page_2)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth())
+ self.rawImg.setSizePolicy(sizePolicy)
+ self.rawImg.setObjectName("rawImg")
+ self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1)
+ self.stack.addWidget(self.page_2)
+ self.gridLayout.addWidget(self.stack, 0, 0, 1, 1)
+ self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget)
+ self.rawGLRadio.setObjectName("rawGLRadio")
+ self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1)
+ self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4)
+ self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget)
+ self.dtypeCombo.setObjectName("dtypeCombo")
+ self.dtypeCombo.addItem("")
+ self.dtypeCombo.addItem("")
+ self.dtypeCombo.addItem("")
+ self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1)
+ self.label = QtWidgets.QLabel(self.centralwidget)
+ self.label.setObjectName("label")
+ self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1)
+ self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.rgbLevelsCheck.setObjectName("rgbLevelsCheck")
+ self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1)
+ self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ self.minSpin2 = SpinBox(self.centralwidget)
+ self.minSpin2.setEnabled(False)
+ self.minSpin2.setObjectName("minSpin2")
+ self.horizontalLayout_2.addWidget(self.minSpin2)
+ self.label_3 = QtWidgets.QLabel(self.centralwidget)
+ self.label_3.setAlignment(QtCore.Qt.AlignCenter)
+ self.label_3.setObjectName("label_3")
+ self.horizontalLayout_2.addWidget(self.label_3)
+ self.maxSpin2 = SpinBox(self.centralwidget)
+ self.maxSpin2.setEnabled(False)
+ self.maxSpin2.setObjectName("maxSpin2")
+ self.horizontalLayout_2.addWidget(self.maxSpin2)
+ self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1)
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.minSpin1 = SpinBox(self.centralwidget)
+ self.minSpin1.setObjectName("minSpin1")
+ self.horizontalLayout.addWidget(self.minSpin1)
+ self.label_2 = QtWidgets.QLabel(self.centralwidget)
+ self.label_2.setAlignment(QtCore.Qt.AlignCenter)
+ self.label_2.setObjectName("label_2")
+ self.horizontalLayout.addWidget(self.label_2)
+ self.maxSpin1 = SpinBox(self.centralwidget)
+ self.maxSpin1.setObjectName("maxSpin1")
+ self.horizontalLayout.addWidget(self.maxSpin1)
+ self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1)
+ self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_3.setObjectName("horizontalLayout_3")
+ self.minSpin3 = SpinBox(self.centralwidget)
+ self.minSpin3.setEnabled(False)
+ self.minSpin3.setObjectName("minSpin3")
+ self.horizontalLayout_3.addWidget(self.minSpin3)
+ self.label_4 = QtWidgets.QLabel(self.centralwidget)
+ self.label_4.setAlignment(QtCore.Qt.AlignCenter)
+ self.label_4.setObjectName("label_4")
+ self.horizontalLayout_3.addWidget(self.label_4)
+ self.maxSpin3 = SpinBox(self.centralwidget)
+ self.maxSpin3.setEnabled(False)
+ self.maxSpin3.setObjectName("maxSpin3")
+ self.horizontalLayout_3.addWidget(self.maxSpin3)
+ self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1)
+ self.lutCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.lutCheck.setObjectName("lutCheck")
+ self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1)
+ self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.alphaCheck.setObjectName("alphaCheck")
+ self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1)
+ self.gradient = GradientWidget(self.centralwidget)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth())
+ self.gradient.setSizePolicy(sizePolicy)
+ self.gradient.setObjectName("gradient")
+ self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2)
+ spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1)
+ self.fpsLabel = QtWidgets.QLabel(self.centralwidget)
+ font = QtGui.QFont()
+ font.setPointSize(12)
+ self.fpsLabel.setFont(font)
+ self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
+ self.fpsLabel.setObjectName("fpsLabel")
+ self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
+ self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget)
+ self.rgbCheck.setObjectName("rgbCheck")
+ self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1)
+ self.label_5 = QtWidgets.QLabel(self.centralwidget)
+ self.label_5.setObjectName("label_5")
+ self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1)
+ self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_4.setObjectName("horizontalLayout_4")
+ self.framesSpin = QtWidgets.QSpinBox(self.centralwidget)
+ self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
+ self.framesSpin.setProperty("value", 10)
+ self.framesSpin.setObjectName("framesSpin")
+ self.horizontalLayout_4.addWidget(self.framesSpin)
+ self.widthSpin = QtWidgets.QSpinBox(self.centralwidget)
+ self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.widthSpin.setMaximum(10000)
+ self.widthSpin.setProperty("value", 512)
+ self.widthSpin.setObjectName("widthSpin")
+ self.horizontalLayout_4.addWidget(self.widthSpin)
+ self.heightSpin = QtWidgets.QSpinBox(self.centralwidget)
+ self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
+ self.heightSpin.setMaximum(10000)
+ self.heightSpin.setProperty("value", 512)
+ self.heightSpin.setObjectName("heightSpin")
+ self.horizontalLayout_4.addWidget(self.heightSpin)
+ self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2)
+ self.sizeLabel = QtWidgets.QLabel(self.centralwidget)
+ self.sizeLabel.setText("")
+ self.sizeLabel.setObjectName("sizeLabel")
+ self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1)
+ MainWindow.setCentralWidget(self.centralwidget)
+
+ self.retranslateUi(MainWindow)
+ self.stack.setCurrentIndex(1)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ _translate = QtCore.QCoreApplication.translate
+ MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
+ self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample"))
+ self.scaleCheck.setText(_translate("MainWindow", "Scale Data"))
+ self.rawRadio.setText(_translate("MainWindow", "RawImageWidget"))
+ self.gfxRadio.setText(_translate("MainWindow", "GraphicsView + ImageItem"))
+ self.rawGLRadio.setText(_translate("MainWindow", "RawGLImageWidget"))
+ self.dtypeCombo.setItemText(0, _translate("MainWindow", "uint8"))
+ self.dtypeCombo.setItemText(1, _translate("MainWindow", "uint16"))
+ self.dtypeCombo.setItemText(2, _translate("MainWindow", "float"))
+ self.label.setText(_translate("MainWindow", "Data type"))
+ self.rgbLevelsCheck.setText(_translate("MainWindow", "RGB"))
+ self.label_3.setText(_translate("MainWindow", "<--->"))
+ self.label_2.setText(_translate("MainWindow", "<--->"))
+ self.label_4.setText(_translate("MainWindow", "<--->"))
+ self.lutCheck.setText(_translate("MainWindow", "Use Lookup Table"))
+ self.alphaCheck.setText(_translate("MainWindow", "alpha"))
+ self.fpsLabel.setText(_translate("MainWindow", "FPS"))
+ self.rgbCheck.setText(_translate("MainWindow", "RGB"))
+ self.label_5.setText(_translate("MainWindow", "Image size"))
+
+from pyqtgraph import GradientWidget, GraphicsView, SpinBox
+from pyqtgraph.widgets.RawImageWidget import RawImageWidget
diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py
index faebd546..4af85249 100644
--- a/examples/VideoTemplate_pyside.py
+++ b/examples/VideoTemplate_pyside.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file './examples/VideoTemplate.ui'
+# Form implementation generated from reading ui file 'examples/VideoTemplate.ui'
#
-# Created: Mon Feb 17 20:39:30 2014
-# by: pyside-uic 0.2.14 running on PySide 1.1.2
+# Created: Wed Oct 26 09:21:01 2016
+# by: pyside-uic 0.2.15 running on PySide 1.2.2
#
# WARNING! All changes made in this file will be lost!
@@ -55,14 +55,6 @@ class Ui_MainWindow(object):
self.rawImg.setObjectName("rawImg")
self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1)
self.stack.addWidget(self.page_2)
- self.page_3 = QtGui.QWidget()
- self.page_3.setObjectName("page_3")
- self.gridLayout_5 = QtGui.QGridLayout(self.page_3)
- self.gridLayout_5.setObjectName("gridLayout_5")
- self.rawGLImg = RawImageGLWidget(self.page_3)
- self.rawGLImg.setObjectName("rawGLImg")
- self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1)
- self.stack.addWidget(self.page_3)
self.gridLayout.addWidget(self.stack, 0, 0, 1, 1)
self.rawGLRadio = QtGui.QRadioButton(self.centralwidget)
self.rawGLRadio.setObjectName("rawGLRadio")
@@ -179,7 +171,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
- self.stack.setCurrentIndex(2)
+ self.stack.setCurrentIndex(1)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
@@ -203,5 +195,5 @@ class Ui_MainWindow(object):
self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
self.label_5.setText(QtGui.QApplication.translate("MainWindow", "Image size", None, QtGui.QApplication.UnicodeUTF8))
-from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget
-from pyqtgraph import GradientWidget, SpinBox, GraphicsView
+from pyqtgraph.widgets.RawImageWidget import RawImageWidget
+from pyqtgraph import SpinBox, GradientWidget, GraphicsView
diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py
index 275877eb..c2cb2ba2 100644
--- a/examples/optics/pyoptic.py
+++ b/examples/optics/pyoptic.py
@@ -89,7 +89,7 @@ def wlPen(wl):
return pen
-class ParamObj:
+class ParamObj(object):
# Just a helper for tracking parameters and responding to changes
def __init__(self):
self.__params = {}
@@ -109,7 +109,8 @@ class ParamObj:
pass
def __getitem__(self, item):
- return self.getParam(item)
+ # bug in pyside 1.2.2 causes getitem to be called inside QGraphicsObject.parentItem:
+ return self.getParam(item) # PySide bug: https://bugreports.qt.io/browse/PYSIDE-441
def getParam(self, param):
return self.__params[param]
diff --git a/examples/text.py b/examples/text.py
index 43302e96..bf9bd6b9 100644
--- a/examples/text.py
+++ b/examples/text.py
@@ -46,7 +46,6 @@ def update():
global curvePoint, index
index = (index + 1) % len(x)
curvePoint.setPos(float(index)/(len(x)-1))
- #text2.viewRangeChanged()
text2.setText('[%0.1f, %0.1f]' % (x[index], y[index]))
timer = QtCore.QTimer()
diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py
index 43058619..0da24d7c 100644
--- a/pyqtgraph/debug.py
+++ b/pyqtgraph/debug.py
@@ -83,8 +83,9 @@ class Tracer(object):
funcname = cls.__name__ + "." + funcname
return "%s: %s %s: %s" % (callline, filename, lineno, funcname)
+
def warnOnException(func):
- """Decorator which catches/ignores exceptions and prints a stack trace."""
+ """Decorator that catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds):
try:
func(*args, **kwds)
@@ -92,11 +93,9 @@ def warnOnException(func):
printExc('Ignored exception:')
return w
+
def getExc(indent=4, prefix='| ', skip=1):
- lines = (traceback.format_stack()[:-skip]
- + [" ---- exception caught ---->\n"]
- + traceback.format_tb(sys.exc_info()[2])
- + traceback.format_exception_only(*sys.exc_info()[:2]))
+ lines = formatException(*sys.exc_info(), skip=skip)
lines2 = []
for l in lines:
lines2.extend(l.strip('\n').split('\n'))
@@ -112,6 +111,7 @@ def printExc(msg='', indent=4, prefix='|'):
print(" "*indent + prefix + '='*30 + '>>')
print(exc)
print(" "*indent + prefix + '='*30 + '<<')
+
def printTrace(msg='', indent=4, prefix='|'):
"""Print an error message followed by an indented stack trace"""
@@ -126,7 +126,30 @@ def printTrace(msg='', indent=4, prefix='|'):
def backtrace(skip=0):
return ''.join(traceback.format_stack()[:-(skip+1)])
+
+
+def formatException(exctype, value, tb, skip=0):
+ """Return a list of formatted exception strings.
+ Similar to traceback.format_exception, but displays the entire stack trace
+ rather than just the portion downstream of the point where the exception is
+ caught. In particular, unhandled exceptions that occur during Qt signal
+ handling do not usually show the portion of the stack that emitted the
+ signal.
+ """
+ lines = traceback.format_exception(exctype, value, tb)
+ lines = [lines[0]] + traceback.format_stack()[:-(skip+1)] + [' --- exception caught here ---\n'] + lines[1:]
+ return lines
+
+
+def printException(exctype, value, traceback):
+ """Print an exception with its full traceback.
+
+ Set `sys.excepthook = printException` to ensure that exceptions caught
+ inside Qt signal handlers are printed with their full stack trace.
+ """
+ print(''.join(formatException(exctype, value, traceback, skip=1)))
+
def listObjs(regex='Q', typ=None):
"""List all objects managed by python gc with class name matching regex.
diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py
index 876bf858..9392b037 100644
--- a/pyqtgraph/flowchart/library/Filters.py
+++ b/pyqtgraph/flowchart/library/Filters.py
@@ -164,8 +164,15 @@ class Gaussian(CtrlNode):
import scipy.ndimage
except ImportError:
raise Exception("GaussianFilter node requires the package scipy.ndimage.")
- return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
+ if hasattr(data, 'implements') and data.implements('MetaArray'):
+ info = data.infoCopy()
+ filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value())
+ if 'values' in info[0]:
+ info[0]['values'] = info[0]['values'][:filt.shape[0]]
+ return metaarray.MetaArray(filt, info=info)
+ else:
+ return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
class Derivative(CtrlNode):
"""Returns the pointwise derivative of the input"""
diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py
index 9b880940..b2587ded 100644
--- a/pyqtgraph/graphicsItems/TextItem.py
+++ b/pyqtgraph/graphicsItems/TextItem.py
@@ -48,6 +48,7 @@ class TextItem(GraphicsObject):
self.textItem = QtGui.QGraphicsTextItem()
self.textItem.setParentItem(self)
self._lastTransform = None
+ self._lastScene = None
self._bounds = QtCore.QRectF()
if html is None:
self.setColor(color)
@@ -149,9 +150,18 @@ class TextItem(GraphicsObject):
self.updateTransform()
def paint(self, p, *args):
- # this is not ideal because it causes another update to be scheduled.
+ # this is not ideal because it requires the transform to be updated at every draw.
# ideally, we would have a sceneTransformChanged event to react to..
- self.updateTransform()
+ s = self.scene()
+ ls = self._lastScene
+ if s is not ls:
+ if ls is not None:
+ ls.sigPrepareForPaint.disconnect(self.updateTransform)
+ self._lastScene = s
+ if s is not None:
+ s.sigPrepareForPaint.connect(self.updateTransform)
+ self.updateTransform()
+ p.setTransform(self.sceneTransform())
if self.border.style() != QtCore.Qt.NoPen or self.fill.style() != QtCore.Qt.NoBrush:
p.setPen(self.border)
@@ -191,5 +201,3 @@ class TextItem(GraphicsObject):
self._lastTransform = pt
self.updateTextPos()
-
-
diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py
index 99e644b0..de9a1624 100644
--- a/pyqtgraph/parametertree/Parameter.py
+++ b/pyqtgraph/parametertree/Parameter.py
@@ -312,7 +312,8 @@ class Parameter(QtCore.QObject):
If blockSignals is True, no signals will be emitted until the tree has been completely restored.
This prevents signal handlers from responding to a partially-rebuilt network.
"""
- childState = state.get('children', [])
+ state = state.copy()
+ childState = state.pop('children', [])
## list of children may be stored either as list or dict.
if isinstance(childState, dict):
diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py
index d8a5f1a6..31717481 100644
--- a/pyqtgraph/parametertree/parameterTypes.py
+++ b/pyqtgraph/parametertree/parameterTypes.py
@@ -284,8 +284,6 @@ class WidgetParameterItem(ParameterItem):
self.widget.setOpts(**opts)
self.updateDisplayLabel()
-
-
class EventProxy(QtCore.QObject):
def __init__(self, qobj, callback):
@@ -296,8 +294,6 @@ class EventProxy(QtCore.QObject):
def eventFilter(self, obj, ev):
return self.callback(obj, ev)
-
-
class SimpleParameter(Parameter):
itemClass = WidgetParameterItem
diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py
index f4404671..c8a41dec 100644
--- a/pyqtgraph/tests/image_testing.py
+++ b/pyqtgraph/tests/image_testing.py
@@ -59,7 +59,7 @@ if sys.version[0] >= '3':
else:
import httplib
import urllib
-from ..Qt import QtGui, QtCore, QtTest
+from ..Qt import QtGui, QtCore, QtTest, QT_LIB
from .. import functions as fn
from .. import GraphicsLayoutWidget
from .. import ImageItem, TextItem
@@ -212,7 +212,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
- pxCount=0, maxPxDiff=None, avgPxDiff=None,
+ pxCount=-1, maxPxDiff=None, avgPxDiff=None,
imgDiff=None):
"""Check that two images match.
@@ -234,7 +234,8 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
pxThreshold : float
Minimum value difference at which two pixels are considered different
pxCount : int or None
- Maximum number of pixels that may differ
+ Maximum number of pixels that may differ. Default is 0 for Qt4 and
+ 1% of image size for Qt5.
maxPxDiff : float or None
Maximum allowed difference between pixels
avgPxDiff : float or None
@@ -247,6 +248,14 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert im1.shape[2] == 4
assert im1.dtype == im2.dtype
+ if pxCount == -1:
+ if QT_LIB == 'PyQt5':
+ # Qt5 generates slightly different results; relax the tolerance
+ # until test images are updated.
+ pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
+ else:
+ pxCount = 0
+
diff = im1.astype(float) - im2.astype(float)
if imgDiff is not None:
assert np.abs(diff).sum() <= imgDiff
diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py
index 3de063fc..30496839 100644
--- a/pyqtgraph/widgets/MatplotlibWidget.py
+++ b/pyqtgraph/widgets/MatplotlibWidget.py
@@ -6,7 +6,10 @@ if not USE_PYQT5:
matplotlib.rcParams['backend.qt4']='PySide'
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
- from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
+ try:
+ from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
+ except ImportError:
+ from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
else:
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py
index 970b570b..657701f9 100644
--- a/pyqtgraph/widgets/RawImageWidget.py
+++ b/pyqtgraph/widgets/RawImageWidget.py
@@ -3,7 +3,9 @@ try:
from ..Qt import QtOpenGL
from OpenGL.GL import *
HAVE_OPENGL = True
-except ImportError:
+except Exception:
+ # Would prefer `except ImportError` here, but some versions of pyopengl generate
+ # AttributeError upon import
HAVE_OPENGL = False
from .. import functions as fn
@@ -59,6 +61,7 @@ class RawImageWidget(QtGui.QWidget):
#p.drawPixmap(self.rect(), self.pixmap)
p.end()
+
if HAVE_OPENGL:
class RawImageGLWidget(QtOpenGL.QGLWidget):
"""
diff --git a/setup.py b/setup.py
index 7ca1be26..a59f7dd5 100644
--- a/setup.py
+++ b/setup.py
@@ -42,10 +42,22 @@ try:
from setuptools import setup
from setuptools.command import install
except ImportError:
+ sys.stderr.write("Warning: could not import setuptools; falling back to distutils.\n")
from distutils.core import setup
from distutils.command import install
+# Work around mbcs bug in distutils.
+# http://bugs.python.org/issue10945
+import codecs
+try:
+ codecs.lookup('mbcs')
+except LookupError:
+ ascii = codecs.lookup('ascii')
+ func = lambda name, enc=ascii: {True: enc}.get(name=='mbcs')
+ codecs.register(func)
+
+
path = os.path.split(__file__)[0]
sys.path.insert(0, os.path.join(path, 'tools'))
import setupHelpers as helpers
@@ -62,11 +74,9 @@ version, forcedVersion, gitVersion, initVersion = helpers.getVersionStrings(pkg=
class Build(build.build):
"""
* Clear build path before building
- * Set version string in __init__ after building
"""
def run(self):
- global path, version, initVersion, forcedVersion
- global buildVersion
+ global path
## Make sure build directory is clean
buildPath = os.path.join(path, self.build_lib)
@@ -75,43 +85,49 @@ class Build(build.build):
ret = build.build.run(self)
- # If the version in __init__ is different from the automatically-generated
- # version string, then we will update __init__ in the build directory
- if initVersion == version:
- return ret
-
- try:
- initfile = os.path.join(buildPath, 'pyqtgraph', '__init__.py')
- data = open(initfile, 'r').read()
- open(initfile, 'w').write(re.sub(r"__version__ = .*", "__version__ = '%s'" % version, data))
- buildVersion = version
- except:
- if forcedVersion:
- raise
- buildVersion = initVersion
- sys.stderr.write("Warning: Error occurred while setting version string in build path. "
- "Installation will use the original version string "
- "%s instead.\n" % (initVersion)
- )
- sys.excepthook(*sys.exc_info())
- return ret
-
class Install(install.install):
"""
* Check for previously-installed version before installing
+ * Set version string in __init__ after building. This helps to ensure that we
+ know when an installation came from a non-release code base.
"""
def run(self):
+ global path, version, initVersion, forcedVersion, installVersion
+
name = self.config_vars['dist_name']
- path = self.install_libbase
- if os.path.exists(path) and name in os.listdir(path):
+ path = os.path.join(self.install_libbase, 'pyqtgraph')
+ if os.path.exists(path):
raise Exception("It appears another version of %s is already "
"installed at %s; remove this before installing."
% (name, path))
print("Installing to %s" % path)
- return install.install.run(self)
+ rval = install.install.run(self)
+ # If the version in __init__ is different from the automatically-generated
+ # version string, then we will update __init__ in the install directory
+ if initVersion == version:
+ return rval
+
+ try:
+ initfile = os.path.join(path, '__init__.py')
+ data = open(initfile, 'r').read()
+ open(initfile, 'w').write(re.sub(r"__version__ = .*", "__version__ = '%s'" % version, data))
+ installVersion = version
+ except:
+ sys.stderr.write("Warning: Error occurred while setting version string in build path. "
+ "Installation will use the original version string "
+ "%s instead.\n" % (initVersion)
+ )
+ if forcedVersion:
+ raise
+ installVersion = initVersion
+ sys.excepthook(*sys.exc_info())
+
+ return rval
+
+
setup(
version=version,
cmdclass={'build': Build,
diff --git a/tools/pg-release.py b/tools/pg-release.py
new file mode 100644
index 00000000..ac32b199
--- /dev/null
+++ b/tools/pg-release.py
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+import os, sys, argparse, random
+from shell import shell, ssh
+
+
+
+description="Build release packages for pyqtgraph."
+
+epilog = """
+Package build is done in several steps:
+
+ * Attempt to clone branch release-x.y.z from source-repo
+ * Merge release branch into master
+ * Write new version numbers into the source
+ * Roll over unreleased CHANGELOG entries
+ * Commit and tag new release
+ * Build HTML documentation
+ * Build source package
+ * Build deb packages (if running on Linux)
+ * Build Windows exe installers
+
+Release packages may be published by using the --publish flag:
+
+ * Uploads release files to website
+ * Pushes tagged git commit to github
+ * Uploads source package to pypi
+
+Building source packages requires:
+
+ *
+ *
+ * python-sphinx
+
+Building deb packages requires several dependencies:
+
+ * build-essential
+ * python-all, python3-all
+ * python-stdeb, python3-stdeb
+
+Note: building windows .exe files should be possible on any OS. However,
+Debian/Ubuntu systems do not include the necessary wininst*.exe files; these
+must be manually copied from the Python source to the distutils/command
+submodule path (/usr/lib/pythonX.X/distutils/command). Additionally, it may be
+necessary to rename (or copy / link) wininst-9.0-amd64.exe to
+wininst-6.0-amd64.exe.
+
+"""
+
+path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+build_dir = os.path.join(path, 'release-build')
+pkg_dir = os.path.join(path, 'release-packages')
+
+ap = argparse.ArgumentParser(description=description, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter)
+ap.add_argument('version', help='The x.y.z version to generate release packages for. '
+ 'There must be a corresponding pyqtgraph-x.y.z branch in the source repository.')
+ap.add_argument('--publish', metavar='', help='Publish previously built package files (must be stored in pkg-dir/version) and tagged release commit (from build-dir).', action='store_const', const=True, default=False)
+ap.add_argument('--source-repo', metavar='', help='Repository from which release and master branches will be cloned. Default is the repo containing this script.', default=path)
+ap.add_argument('--build-dir', metavar='', help='Directory where packages will be staged and built. Default is source_root/release-build.', default=build_dir)
+ap.add_argument('--pkg-dir', metavar='', help='Directory where packages will be stored. Default is source_root/release-packages.', default=pkg_dir)
+ap.add_argument('--skip-pip-test', metavar='', help='Skip testing pip install.', action='store_const', const=True, default=False)
+ap.add_argument('--no-deb', metavar='', help='Skip building Debian packages.', action='store_const', const=True, default=False)
+ap.add_argument('--no-exe', metavar='', help='Skip building Windows exe installers.', action='store_const', const=True, default=False)
+
+
+
+def build(args):
+ if os.path.exists(args.build_dir):
+ sys.stderr.write("Please remove the build directory %s before proceeding, or specify a different path with --build-dir.\n" % args.build_dir)
+ sys.exit(-1)
+ if os.path.exists(args.pkg_dir):
+ sys.stderr.write("Please remove the package directory %s before proceeding, or specify a different path with --pkg-dir.\n" % args.pkg_dir)
+ sys.exit(-1)
+
+ # Clone source repository and tag the release branch
+ shell('''
+ # Clone and merge release branch into previous master
+ mkdir -p {build_dir}
+ cd {build_dir}
+ rm -rf pyqtgraph
+ git clone --depth 1 -b master {source_repo} pyqtgraph
+ cd pyqtgraph
+ git checkout -b release-{version}
+ git pull {source_repo} release-{version}
+ git checkout master
+ git merge --no-ff --no-commit release-{version}
+
+ # Write new version number into the source
+ sed -i "s/__version__ = .*/__version__ = '{version}'/" pyqtgraph/__init__.py
+ sed -i "s/version = .*/version = '{version}'/" doc/source/conf.py
+ sed -i "s/release = .*/release = '{version}'/" doc/source/conf.py
+
+ # make sure changelog mentions unreleased changes
+ grep "pyqtgraph-{version}.*unreleased.*" CHANGELOG
+ sed -i "s/pyqtgraph-{version}.*unreleased.*/pyqtgraph-{version}/" CHANGELOG
+
+ # Commit and tag new release
+ git commit -a -m "PyQtGraph release {version}"
+ git tag pyqtgraph-{version}
+
+ # Build HTML documentation
+ cd doc
+ make clean
+ make html
+ cd ..
+ find ./ -name "*.pyc" -delete
+
+ # package source distribution
+ python setup.py sdist
+
+ mkdir -p {pkg_dir}
+ cp dist/*.tar.gz {pkg_dir}
+
+ # source package build complete.
+ '''.format(**args.__dict__))
+
+
+ if args.skip_pip_test:
+ args.pip_test = 'skipped'
+ else:
+ shell('''
+ # test pip install source distribution
+ rm -rf release-{version}-virtenv
+ virtualenv --system-site-packages release-{version}-virtenv
+ . release-{version}-virtenv/bin/activate
+ echo "PATH: $PATH"
+ echo "ENV: $VIRTUAL_ENV"
+ pip install --no-index --no-deps dist/pyqtgraph-{version}.tar.gz
+ deactivate
+
+ # pip install test passed
+ '''.format(**args.__dict__))
+ args.pip_test = 'passed'
+
+
+ if 'linux' in sys.platform and not args.no_deb:
+ shell('''
+ # build deb packages
+ cd {build_dir}/pyqtgraph
+ python setup.py --command-packages=stdeb.command sdist_dsc
+ cd deb_dist/pyqtgraph-{version}
+ sed -i "s/^Depends:.*/Depends: python (>= 2.6), python-qt4 | python-pyside, python-numpy/" debian/control
+ dpkg-buildpackage
+ cd ../../
+ mv deb_dist {pkg_dir}/pyqtgraph-{version}-deb
+
+ # deb package build complete.
+ '''.format(**args.__dict__))
+ args.deb_status = 'built'
+ else:
+ args.deb_status = 'skipped'
+
+
+ if not args.no_exe:
+ shell("""
+ # Build windows executables
+ cd {build_dir}/pyqtgraph
+ python setup.py build bdist_wininst --plat-name=win32
+ python setup.py build bdist_wininst --plat-name=win-amd64
+ cp dist/*.exe {pkg_dir}
+ """.format(**args.__dict__))
+ args.exe_status = 'built'
+ else:
+ args.exe_status = 'skipped'
+
+
+ print(unindent("""
+
+ ======== Build complete. =========
+
+ * Source package: built
+ * Pip install test: {pip_test}
+ * Debian packages: {deb_status}
+ * Windows installers: {exe_status}
+ * Package files in {pkg_dir}
+
+ Next steps to publish:
+
+ * Test all packages
+ * Run script again with --publish
+
+ """).format(**args.__dict__))
+
+
+def publish(args):
+
+
+ if not os.path.isfile(os.path.expanduser('~/.pypirc')):
+ print(unindent("""
+ Missing ~/.pypirc file. Should look like:
+ -----------------------------------------
+
+ [distutils]
+ index-servers =
+ pypi
+
+ [pypi]
+ username:your_username
+ password:your_password
+
+ """))
+ sys.exit(-1)
+
+ ### Upload everything to server
+ shell("""
+ # Uploading documentation..
+ cd {build_dir}/pyqtgraph
+ rsync -rv doc/build/* pyqtgraph.org:/www/code/pyqtgraph/pyqtgraph/documentation/build/
+
+ # Uploading release packages to website
+ rsync -v {pkg_dir}/{version} pyqtgraph.org:/www/code/pyqtgraph/downloads/
+
+ # Push to github
+ git push --tags https://github.com/pyqtgraph/pyqtgraph master:master
+
+ # Upload to pypi..
+ python setup.py sdist upload
+
+ """.format(**args.__dict__))
+
+ print(unindent("""
+
+ ======== Upload complete. =========
+
+ Next steps to publish:
+ - update website
+ - mailing list announcement
+ - new conda recipe (http://conda.pydata.org/docs/build.html)
+ - contact deb maintainer (gianfranco costamagna)
+ - other package maintainers?
+
+ """).format(**args.__dict__))
+
+
+def unindent(msg):
+ ind = 1e6
+ lines = msg.split('\n')
+ for line in lines:
+ if len(line.strip()) == 0:
+ continue
+ ind = min(ind, len(line) - len(line.lstrip()))
+ return '\n'.join([line[ind:] for line in lines])
+
+
+if __name__ == '__main__':
+ args = ap.parse_args()
+ args.build_dir = os.path.abspath(args.build_dir)
+ args.pkg_dir = os.path.join(os.path.abspath(args.pkg_dir), args.version)
+
+ if args.publish:
+ publish(args)
+ else:
+ build(args)
diff --git a/tools/rebuildUi.py b/tools/rebuildUi.py
index 98751412..2ce80d87 100644
--- a/tools/rebuildUi.py
+++ b/tools/rebuildUi.py
@@ -1,30 +1,53 @@
-import os, sys
-## Search the package tree for all .ui files, compile each to
-## a .py for pyqt and pyside
+"""
+Script for compiling Qt Designer .ui files to .py
+
+
+
+"""
+import os, sys, subprocess, tempfile
pyqtuic = 'pyuic4'
pysideuic = 'pyside-uic'
pyqt5uic = 'pyuic5'
-for path, sd, files in os.walk('.'):
- for f in files:
- base, ext = os.path.splitext(f)
- if ext != '.ui':
- continue
- ui = os.path.join(path, f)
+usage = """Compile .ui files to .py for all supported pyqt/pyside versions.
- py = os.path.join(path, base + '_pyqt.py')
- if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime:
- os.system('%s %s > %s' % (pyqtuic, ui, py))
- print(py)
+ Usage: python rebuildUi.py [.ui files|search paths]
- py = os.path.join(path, base + '_pyside.py')
- if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime:
- os.system('%s %s > %s' % (pysideuic, ui, py))
- print(py)
+ May specify a list of .ui files and/or directories to search recursively for .ui files.
+"""
- py = os.path.join(path, base + '_pyqt5.py')
- if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime:
- os.system('%s %s > %s' % (pyqt5uic, ui, py))
- print(py)
+args = sys.argv[1:]
+if len(args) == 0:
+ print(usage)
+ sys.exit(-1)
+
+uifiles = []
+for arg in args:
+ if os.path.isfile(arg) and arg.endswith('.ui'):
+ uifiles.append(arg)
+ elif os.path.isdir(arg):
+ # recursively search for ui files in this directory
+ for path, sd, files in os.walk(arg):
+ for f in files:
+ if not f.endswith('.ui'):
+ continue
+ uifiles.append(os.path.join(path, f))
+ else:
+ print('Argument "%s" is not a directory or .ui file.' % arg)
+ sys.exit(-1)
+# rebuild all requested ui files
+for ui in uifiles:
+ base, _ = os.path.splitext(ui)
+ for compiler, ext in [(pyqtuic, '_pyqt.py'), (pysideuic, '_pyside.py'), (pyqt5uic, '_pyqt5.py')]:
+ py = base + ext
+ if os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime:
+ print("Skipping %s; already compiled." % py)
+ else:
+ cmd = '%s %s > %s' % (compiler, ui, py)
+ print(cmd)
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError:
+ os.remove(py)
diff --git a/tools/release_instructions.md b/tools/release_instructions.md
new file mode 100644
index 00000000..b3b53efa
--- /dev/null
+++ b/tools/release_instructions.md
@@ -0,0 +1,34 @@
+PyQtGraph Release Procedure
+---------------------------
+
+1. Create a release-x.x.x branch
+
+2. Run pyqtgraph/tools/pg-release.py script (this has only been tested on linux)
+ - creates clone of master
+ - merges release branch into master
+ - updates version numbers in code
+ - creates pyqtgraph-x.x.x tag
+ - creates release commit
+ - builds documentation
+ - builds source package
+ - tests pip install
+ - builds windows .exe installers (note: it may be necessary to manually
+ copy wininst*.exe files from the python source packages)
+ - builds deb package (note: official debian packages are built elsewhere;
+ these locally-built deb packages may be phased out)
+
+3. test build files
+ - test setup.py, pip on OSX
+ - test setup.py, pip, 32/64 exe on windows
+ - test setup.py, pip, deb on linux (py2, py3)
+
+4. Run pg-release.py script again with --publish flag
+ - website upload
+ - github push + release
+ - pip upload
+
+5. publish
+ - update website
+ - mailing list announcement
+ - new conda recipe (http://conda.pydata.org/docs/build.html)
+ - contact various package maintainers
diff --git a/tools/setVersion.py b/tools/setVersion.py
deleted file mode 100644
index b62aca01..00000000
--- a/tools/setVersion.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import re, os, sys
-
-version = sys.argv[1]
-
-replace = [
- ("pyqtgraph/__init__.py", r"__version__ = .*", "__version__ = '%s'" % version),
- #("setup.py", r" version=.*,", " version='%s'," % version), # setup.py automatically detects version
- ("doc/source/conf.py", r"version = .*", "version = '%s'" % version),
- ("doc/source/conf.py", r"release = .*", "release = '%s'" % version),
- #("tools/debian/control", r"^Version: .*", "Version: %s" % version)
- ]
-
-path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
-
-for filename, search, sub in replace:
- filename = os.path.join(path, filename)
- data = open(filename, 'r').read()
- if re.search(search, data) is None:
- print('Error: Search expression "%s" not found in file %s.' % (search, filename))
- os._exit(1)
- open(filename, 'w').write(re.sub(search, sub, data))
-
-print("Updated version strings to %s" % version)
-
-
-
diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py
index af478d97..939bca4e 100644
--- a/tools/setupHelpers.py
+++ b/tools/setupHelpers.py
@@ -358,18 +358,33 @@ def getGitVersion(tagPrefix):
if not os.path.isdir(os.path.join(path, '.git')):
return None
- gitVersion = check_output(['git', 'describe', '--tags']).strip().decode('utf-8')
+ v = check_output(['git', 'describe', '--tags', '--dirty', '--match=%s*'%tagPrefix]).strip().decode('utf-8')
- # any uncommitted modifications?
+ # chop off prefix
+ assert v.startswith(tagPrefix)
+ v = v[len(tagPrefix):]
+
+ # split up version parts
+ parts = v.split('-')
+
+ # has working tree been modified?
modified = False
- status = check_output(['git', 'status', '--porcelain'], universal_newlines=True).strip().split('\n')
- for line in status:
- if line != '' and line[:2] != '??':
- modified = True
- break
-
+ if parts[-1] == 'dirty':
+ modified = True
+ parts = parts[:-1]
+
+ # have commits been added on top of last tagged version?
+ # (git describe adds -NNN-gXXXXXXX if this is the case)
+ local = None
+ if len(parts) > 2 and re.match(r'\d+', parts[-2]) and re.match(r'g[0-9a-f]{7}', parts[-1]):
+ local = parts[-1]
+ parts = parts[:-2]
+
+ gitVersion = '-'.join(parts)
+ if local is not None:
+ gitVersion += '+' + local
if modified:
- gitVersion = gitVersion + '+'
+ gitVersion += 'm'
return gitVersion
@@ -393,11 +408,11 @@ def getVersionStrings(pkg):
"""
## Determine current version string from __init__.py
- initVersion = getInitVersion(pkgroot='pyqtgraph')
+ initVersion = getInitVersion(pkgroot=pkg)
## If this is a git checkout, try to generate a more descriptive version string
try:
- gitVersion = getGitVersion(tagPrefix='pyqtgraph-')
+ gitVersion = getGitVersion(tagPrefix=pkg+'-')
except:
gitVersion = None
sys.stderr.write("This appears to be a git checkout, but an error occurred "
diff --git a/tools/shell.py b/tools/shell.py
new file mode 100644
index 00000000..76667980
--- /dev/null
+++ b/tools/shell.py
@@ -0,0 +1,38 @@
+import os, sys
+import subprocess as sp
+
+
+def shell(cmd):
+ """Run each line of a shell script; raise an exception if any line returns
+ a nonzero value.
+ """
+ pin, pout = os.pipe()
+ proc = sp.Popen('/bin/bash', stdin=sp.PIPE)
+ for line in cmd.split('\n'):
+ line = line.strip()
+ if line.startswith('#'):
+ print('\033[33m> ' + line + '\033[0m')
+ else:
+ print('\033[32m> ' + line + '\033[0m')
+ if line.startswith('cd '):
+ os.chdir(line[3:])
+ proc.stdin.write((line + '\n').encode('utf-8'))
+ proc.stdin.write(('echo $? 1>&%d\n' % pout).encode('utf-8'))
+ ret = ""
+ while not ret.endswith('\n'):
+ ret += os.read(pin, 1)
+ ret = int(ret.strip())
+ if ret != 0:
+ print("\033[31mLast command returned %d; bailing out.\033[0m" % ret)
+ sys.exit(-1)
+ proc.stdin.close()
+ proc.wait()
+
+
+def ssh(host, cmd):
+ """Run commands on a remote host by ssh.
+ """
+ proc = sp.Popen(['ssh', host], stdin=sp.PIPE)
+ proc.stdin.write(cmd)
+ proc.wait()
+