Merge pull request #383 from campagnola/release-0.10.0

Release 0.10.0
This commit is contained in:
Luke Campagnola 2016-11-04 23:01:47 -07:00 committed by GitHub
commit 4a3fb81535
26 changed files with 779 additions and 177 deletions

View File

@ -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;
@ -135,6 +138,9 @@ script:
- source activate test_env
# Check system info
- python -c "import pyqtgraph as pg; pg.systemInfo()"
# Run unit tests
- start_test "unit tests";
PYTHONPATH=. py.test --cov pyqtgraph -sv;

View File

@ -1,31 +1,55 @@
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
Fixed installation issues with more recent pip versions.

View File

@ -39,6 +39,7 @@ Contributors
* Martin Fitzpatrick
* Daniel Lidstrom
* Eric Dill
* Vincent LeSaux
Requirements
------------

View File

@ -1,8 +0,0 @@
RawImageWidget
==============
.. autoclass:: pyqtgraph.RawImageWidget
:members:
.. automethod:: pyqtgraph.RawImageWidget.__init__

View File

@ -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)

View File

@ -51,7 +51,7 @@
<item row="0" column="0">
<widget class="QStackedWidget" name="stack">
<property name="currentIndex">
<number>2</number>
<number>1</number>
</property>
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout_3">
@ -74,13 +74,6 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="RawImageGLWidget" name="rawGLImg" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
<item row="4" column="0">
@ -340,12 +333,6 @@
<extends>QDoubleSpinBox</extends>
<header>pyqtgraph</header>
</customwidget>
<customwidget>
<class>RawImageGLWidget</class>
<extends>QWidget</extends>
<header>pyqtgraph.widgets.RawImageWidget</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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()

View File

@ -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'))
@ -113,6 +112,7 @@ def printExc(msg='', indent=4, prefix='|'):
print(exc)
print(" "*indent + prefix + '='*30 + '<<')
def printTrace(msg='', indent=4, prefix='|'):
"""Print an error message followed by an indented stack trace"""
trace = backtrace(1)
@ -128,6 +128,29 @@ 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.
Finds 'Q...' classes by default."""

View File

@ -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"""

View File

@ -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()

View File

@ -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):

View File

@ -285,8 +285,6 @@ class WidgetParameterItem(ParameterItem):
self.updateDisplayLabel()
class EventProxy(QtCore.QObject):
def __init__(self, qobj, callback):
QtCore.QObject.__init__(self)
@ -297,8 +295,6 @@ class EventProxy(QtCore.QObject):
return self.callback(obj, ev)
class SimpleParameter(Parameter):
itemClass = WidgetParameterItem

View File

@ -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

View File

@ -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

View File

@ -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):
"""

View File

@ -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,41 +85,47 @@ 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(

252
tools/pg-release.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 "

38
tools/shell.py Normal file
View File

@ -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()