From ef6cd9be88855b62d07def21efd5cf45fef7d926 Mon Sep 17 00:00:00 2001 From: cjtk Date: Wed, 31 Dec 2014 10:32:36 +1100 Subject: [PATCH 01/92] Fix bug in LayoutWidget.py getWidget tries to get self.row which doesn't exist, get self.rows instead --- pyqtgraph/widgets/LayoutWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py index 65d04d3f..91cd1600 100644 --- a/pyqtgraph/widgets/LayoutWidget.py +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -75,7 +75,7 @@ class LayoutWidget(QtGui.QWidget): def getWidget(self, row, col): """Return the widget in (*row*, *col*)""" - return self.row[row][col] + return self.rows[row][col] #def itemIndex(self, item): #for i in range(self.layout.count()): From e98f3582a84a4bbfec1eaed3855d5ca72174be77 Mon Sep 17 00:00:00 2001 From: duguxy Date: Sat, 15 Aug 2015 17:12:00 +0800 Subject: [PATCH 02/92] Fix: flowchart saveFile and loadFile in python3 --- pyqtgraph/flowchart/Flowchart.py | 8 ++++---- pyqtgraph/flowchart/Node.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 17e2bde4..53731df0 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -514,7 +514,6 @@ class Flowchart(Node): return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") - fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() @@ -535,7 +534,8 @@ class Flowchart(Node): self.fileDialog.fileSelected.connect(self.saveFile) return #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") - fileName = unicode(fileName) + if not fileName.endswith('.fc'): + fileName += '.fc' configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) @@ -660,7 +660,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(unicode(fileName)) + self.setCurrentFile(fileName) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -689,7 +689,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = unicode(fileName) + self.currentFileName = fileName if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index fc7b04d3..9399fe2e 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -374,7 +374,7 @@ class Node(QtCore.QObject): pos = self.graphicsItem().pos() state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} termsEditable = self._allowAddInput | self._allowAddOutput - for term in self._inputs.values() + self._outputs.values(): + for term in list(self._inputs.values()) + list(self._outputs.values()): termsEditable |= term._renamable | term._removable | term._multiable if termsEditable: state['terminals'] = self.saveTerminals() From eb55e439a350473d250bc0a3180f57b008889d0f Mon Sep 17 00:00:00 2001 From: duguxy Date: Fri, 18 Sep 2015 11:49:04 +0800 Subject: [PATCH 03/92] Fix flowchat save load support --- pyqtgraph/flowchart/Flowchart.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 53731df0..c57503f3 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -24,6 +24,7 @@ from .. import configfile as configfile from .. import dockarea as dockarea from . import FlowchartGraphicsView from .. import functions as fn +from ..python2_3 import asUnicode def strDict(d): return dict([(str(k), v) for k, v in d.items()]) @@ -660,7 +661,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(fileName) + self.setCurrentFile(asUnicode(fileName)) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -689,7 +690,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = fileName + self.currentFileName = asUnicode(fileName) if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: From 9fa0d0e7244a4f60685e523bb986befe0c5c876c Mon Sep 17 00:00:00 2001 From: duguxy Date: Fri, 18 Sep 2015 19:53:09 +0800 Subject: [PATCH 04/92] Fix flowchart s&l on python2 --- pyqtgraph/flowchart/Flowchart.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index c57503f3..2149a58a 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -515,6 +515,7 @@ class Flowchart(Node): return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + fileName = asUnicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() @@ -537,6 +538,7 @@ class Flowchart(Node): #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") if not fileName.endswith('.fc'): fileName += '.fc' + fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) From cf2329b75e7fe63e3e3cb616f6475634d8c2a04b Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 28 Sep 2016 17:00:10 -0600 Subject: [PATCH 05/92] Fix issue with Python3 and changes in how it handles zip. --- pyqtgraph/colormap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index f943e2fe..585d7ea1 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -141,7 +141,7 @@ class ColorMap(object): pos, color = self.getStops(mode=self.BYTE) color = [QtGui.QColor(*x) for x in color] - g.setStops(zip(pos, color)) + g.setStops(list(zip(pos, color))) #if self.colorMode == 'rgb': #ticks = self.listTicks() From b9aea3daf145009f0fb6e03103270f3da20b3fb5 Mon Sep 17 00:00:00 2001 From: Pieter Date: Thu, 16 Feb 2017 12:40:21 +0100 Subject: [PATCH 06/92] add warnings for remote exceptions --- pyqtgraph/multiprocess/remoteproxy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 208e17f4..6d738f0a 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -1,6 +1,7 @@ import os, time, sys, traceback, weakref import numpy as np import threading +import warnings try: import __builtin__ as builtins import cPickle as pickle @@ -21,6 +22,9 @@ class NoResultError(Exception): because the call has not yet returned.""" pass +class RemoteExceptionWarning(UserWarning): + """Emitted when a request to a remote object results in an Exception """ + pass class RemoteEventHandler(object): """ @@ -502,9 +506,9 @@ class RemoteEventHandler(object): #print ''.join(result) exc, excStr = result if exc is not None: - print("===== Remote process raised exception on request: =====") - print(''.join(excStr)) - print("===== Local Traceback to request follows: =====") + warnings.warn("===== Remote process raised exception on request: =====", RemoteExceptionWarning) + warnings.warn(''.join(excStr), RemoteExceptionWarning) + warnings.warn("===== Local Traceback to request follows: =====", RemoteExceptionWarning) raise exc else: print(''.join(excStr)) From 9a05b74f250647bb4020c2866532210ff9174104 Mon Sep 17 00:00:00 2001 From: Lorenz Drescher Date: Fri, 21 Apr 2017 17:41:22 +0200 Subject: [PATCH 07/92] Correct wrong function call in LayoutWidget.addLabel and LayoutWidget.addLayout Previously LayoutWidget.addLabel and LayoutWidget.addLayout called a function "addItem", that didn't exist. Corrected to call LayoutWidget.addWidget. This fixes #242 --- pyqtgraph/widgets/LayoutWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py index 65d04d3f..7181778d 100644 --- a/pyqtgraph/widgets/LayoutWidget.py +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -39,7 +39,7 @@ class LayoutWidget(QtGui.QWidget): Returns the created widget. """ text = QtGui.QLabel(text, **kargs) - self.addItem(text, row, col, rowspan, colspan) + self.addWidget(text, row, col, rowspan, colspan) return text def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): @@ -49,7 +49,7 @@ class LayoutWidget(QtGui.QWidget): Returns the created widget. """ layout = LayoutWidget(**kargs) - self.addItem(layout, row, col, rowspan, colspan) + self.addWidget(layout, row, col, rowspan, colspan) return layout def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1): From c238be004ebb4a16917dc53cf5f53a6b52a4be1e Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Thu, 15 Dec 2016 09:26:19 -0500 Subject: [PATCH 08/92] add test case for the PlotDataItem.clear() in stepMode --- pyqtgraph/graphicsItems/tests/test_PlotDataItem.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index 8851a0a2..b506a654 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -9,16 +9,16 @@ def test_fft(): x = np.linspace(0, 1, 1000) y = np.sin(2 * np.pi * f * x) pd = pg.PlotDataItem(x, y) - pd.setFftMode(True) + pd.setFftMode(True) x, y = pd.getData() assert abs(x[np.argmax(y)] - f) < 0.03 - + x = np.linspace(0, 1, 1001) y = np.sin(2 * np.pi * f * x) pd.setData(x, y) x, y = pd.getData() assert abs(x[np.argmax(y)]- f) < 0.03 - + pd.setLogMode(True, False) x, y = pd.getData() assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01 @@ -58,3 +58,9 @@ def test_clear(): assert pdi.xData == None assert pdi.yData == None + +def test_clear_in_step_mode(): + w = pg.PlotWidget() + c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) + w.addItem(c) + c.clear() From b13062f081464d6950990d7e3add969e43e7d934 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Wed, 14 Dec 2016 22:39:39 -0500 Subject: [PATCH 09/92] In PlotDataItem.clear() use corresponding curve.clear() and scatter.clear() Otherwise when stepMode is True curve.setData([]) causes exception: "len(X) must be len(Y)+1 ..." --- pyqtgraph/graphicsItems/PlotDataItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 2faa9ac1..1bf48f5d 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -627,9 +627,9 @@ class PlotDataItem(GraphicsObject): #self.yClean = None self.xDisp = None self.yDisp = None - self.curve.setData([]) - self.scatter.setData([]) - + self.curve.clear() + self.scatter.clear() + def appendData(self, *args, **kargs): pass From 266b0d0b4761d608f83b235cb597a11c14b635a3 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Mon, 8 Oct 2018 10:51:18 +0800 Subject: [PATCH 10/92] Update the installation document * Add the method to directly install the latest commit or any branch on the GitHub. --- doc/source/installation.rst | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 37c0ae0e..e3e1f1fc 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -9,18 +9,28 @@ There are many different ways to install pyqtgraph, depending on your needs: Some users may need to call ``pip3`` instead. This method should work on all platforms. -* To get access to the very latest features and bugfixes, clone pyqtgraph from - github:: +* To get access to the very latest features and bugfixes you have three choice:: + + 1. Clone pyqtgraph from github:: $ git clone https://github.com/pyqtgraph/pyqtgraph - - Now you can install pyqtgraph from the source:: - + + Now you can install pyqtgraph from the source:: + $ python setup.py install - ..or you can simply place the pyqtgraph folder someplace importable, such as - inside the root of another project. PyQtGraph does not need to be "built" or - compiled in any way. + 2. Directly install from GitHub repo:: + + $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop + + You can change to ``develop`` of the above command to the branch + name or the commit you prefer. + + 3. + You can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. + * Packages for pyqtgraph are also available in a few other forms: * **Anaconda**: ``conda install pyqtgraph`` From d261c2f0f2fb317ecc7e4a580147dc95db8f8524 Mon Sep 17 00:00:00 2001 From: Jim Crowell Date: Wed, 10 Oct 2018 10:29:16 -0400 Subject: [PATCH 11/92] fixed bug in graphicsItems/ImageItem.py: degenerate images (max==min) would raise exception in getHistogram() --- pyqtgraph/graphicsItems/ImageItem.py | 139 ++++++++++++++------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index a5a761bb..2ebce2c7 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -16,23 +16,23 @@ __all__ = ['ImageItem'] class ImageItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` - + GraphicsObject displaying an image. Optimized for rapid update (ie video display). This item displays either a 2D numpy array (height, width) or - a 3D array (height, width, RGBa). This array is optionally scaled (see + a 3D array (height, width, RGBa). This array is optionally scaled (see :func:`setLevels `) and/or colored with a lookup table (see :func:`setLookupTable `) before being displayed. - - ImageItem is frequently used in conjunction with - :class:`HistogramLUTItem ` or + + ImageItem is frequently used in conjunction with + :class:`HistogramLUTItem ` or :class:`HistogramLUTWidget ` to provide a GUI for controlling the levels and lookup table used to display the image. """ - + sigImageChanged = QtCore.Signal() sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu - + def __init__(self, image=None, **kargs): """ See :func:`setImage ` for all allowed initialization arguments. @@ -41,23 +41,23 @@ class ImageItem(GraphicsObject): self.menu = None self.image = None ## original image data self.qimage = None ## rendered image for display - + self.paintMode = None - + self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None self.autoDownsample = False - + self.axisOrder = getConfigOption('imageAxisOrder') - + # In some cases, we use a modified lookup table to handle both rescaling # and LUT more efficiently self._effectiveLut = None - + self.drawKernel = None self.border = None self.removable = False - + if image is not None: self.setImage(image, **kargs) else: @@ -66,32 +66,32 @@ class ImageItem(GraphicsObject): def setCompositionMode(self, mode): """Change the composition mode of the item (see QPainter::CompositionMode in the Qt documentation). This is useful when overlaying multiple ImageItems. - + ============================================ ============================================================ **Most common arguments:** QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it is opaque. Otherwise, it uses the alpha channel to blend the image with the background. - QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to reflect the lightness or darkness of the background. - QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels are added together. QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. ============================================ ============================================================ """ self.paintMode = mode self.update() - + def setBorder(self, b): self.border = fn.mkPen(b) self.update() - + def width(self): if self.image is None: return None axis = 0 if self.axisOrder == 'col-major' else 1 return self.image.shape[axis] - + def height(self): if self.image is None: return None @@ -111,10 +111,10 @@ class ImageItem(GraphicsObject): def setLevels(self, levels, update=True): """ Set image scaling levels. Can be one of: - + * [blackLevel, whiteLevel] * [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] - + Only the first format is compatible with lookup tables. See :func:`makeARGB ` for more details on how levels are applied. """ @@ -125,18 +125,18 @@ class ImageItem(GraphicsObject): self._effectiveLut = None if update: self.updateImage() - + def getLevels(self): return self.levels #return self.whiteLevel, self.blackLevel def setLookupTable(self, lut, update=True): """ - Set the lookup table (numpy array) to use for this image. (see + Set the lookup table (numpy array) to use for this image. (see :func:`makeARGB ` for more information on how this is used). - Optionally, lut can be a callable that accepts the current image as an + Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use. - + Ordinarily, this table is supplied by a :class:`HistogramLUTItem ` or :class:`GradientEditorItem `. """ @@ -149,7 +149,7 @@ class ImageItem(GraphicsObject): def setAutoDownsample(self, ads): """ Set the automatic downsampling mode for this ImageItem. - + Added in version 0.9.9 """ self.autoDownsample = ads @@ -198,44 +198,44 @@ class ImageItem(GraphicsObject): """ Update the image displayed by this item. For more information on how the image is processed before displaying, see :func:`makeARGB ` - + ================= ========================================================================= **Arguments:** - image (numpy array) Specifies the image data. May be 2D (width, height) or + image (numpy array) Specifies the image data. May be 2D (width, height) or 3D (width, height, RGBa). The array dtype must be integer or floating point of any bit depth. For 3D arrays, the third dimension must be of length 3 (RGB) or 4 (RGBA). See *notes* below. - autoLevels (bool) If True, this forces the image to automatically select + autoLevels (bool) If True, this forces the image to automatically select levels based on the maximum and minimum values in the data. By default, this argument is true unless the levels argument is given. lut (numpy array) The color lookup table to use when displaying the image. See :func:`setLookupTable `. levels (min, max) The minimum and maximum values to use when rescaling the image - data. By default, this will be set to the minimum and maximum values + data. By default, this will be set to the minimum and maximum values in the image. If the image array has dtype uint8, no rescaling is necessary. opacity (float 0.0-1.0) compositionMode See :func:`setCompositionMode ` border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the - screen resolution. This improves performance for large images and + screen resolution. This improves performance for large images and reduces aliasing. If autoDownsample is not specified, then ImageItem will choose whether to downsample the image based on its size. ================= ========================================================================= - - - **Notes:** - + + + **Notes:** + For backward compatibility, image data is assumed to be in column-major order (column, row). However, most image data is stored in row-major order (row, column) and will need to be transposed before calling setImage():: - + imageitem.setImage(imagedata.T) - + This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or by changing the ``imageAxisOrder`` :ref:`global configuration option `. - - + + """ profile = debug.Profiler() @@ -292,7 +292,7 @@ class ImageItem(GraphicsObject): def dataTransform(self): """Return the transform that maps from this image's input array to its local coordinate system. - + This transform corrects for the transposition that occurs when image data is interpreted in row-major order. """ @@ -307,7 +307,7 @@ class ImageItem(GraphicsObject): def inverseDataTransform(self): """Return the transform that maps from this image's local coordinate system to its input array. - + See dataTransform() for more information. """ tr = QtGui.QTransform() @@ -339,7 +339,7 @@ class ImageItem(GraphicsObject): def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. - + ## can we make any assumptions here that speed things up? ## dtype, range, size are all the same? defaults = { @@ -350,11 +350,11 @@ class ImageItem(GraphicsObject): def render(self): # Convert data to QImage for display. - + profile = debug.Profiler() if self.image is None or self.image.size == 0: return - + # Request a lookup table if this image has only one channel if self.image.ndim == 2 or self.image.shape[2] == 1: if isinstance(self.lut, collections.Callable): @@ -385,7 +385,7 @@ class ImageItem(GraphicsObject): image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) - + # Check if downsampling reduced the image size to zero due to inf values. if image.size == 0: return @@ -403,27 +403,27 @@ class ImageItem(GraphicsObject): levdiff = maxlev - minlev levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 if lut is None: - efflut = fn.rescaleData(ind, scale=255./levdiff, + efflut = fn.rescaleData(ind, scale=255./levdiff, offset=minlev, dtype=np.ubyte) else: lutdtype = np.min_scalar_type(lut.shape[0]-1) efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff, offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1)) efflut = lut[efflut] - + self._effectiveLut = efflut lut = self._effectiveLut levels = None - + # Convert single-channel image to 2D array if image.ndim == 3 and image.shape[-1] == 1: image = image[..., 0] - + # Assume images are in column-major order for backward compatibility # (most images are in row-major order) if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) - + argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) @@ -453,26 +453,26 @@ class ImageItem(GraphicsObject): self.render() self.qimage.save(fileName, *args) - def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, + def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, targetHistogramSize=500, **kwds): """Returns x and y arrays containing the histogram values for the current image. For an explanation of the return format, see numpy.histogram(). - + The *step* argument causes pixels to be skipped when computing the histogram to save time. If *step* is 'auto', then a step is chosen such that the analyzed data has dimensions roughly *targetImageSize* for each axis. - - The *bins* argument and any extra keyword arguments are passed to + + The *bins* argument and any extra keyword arguments are passed to np.histogram(). If *bins* is 'auto', then a bin number is automatically chosen based on the image characteristics: - - * Integer images will have approximately *targetHistogramSize* bins, + + * Integer images will have approximately *targetHistogramSize* bins, with each bin having an integer width. * All other types will have *targetHistogramSize* bins. - + If *perChannel* is True, then the histogram is computed once per channel and the output is a list of the results. - + This method is also used when automatically computing levels. """ if self.image is None or self.image.size == 0: @@ -483,10 +483,13 @@ class ImageItem(GraphicsObject): if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] - + if bins == 'auto': mn = np.nanmin(stepData) mx = np.nanmax(stepData) + if mx == mn: + # degenerate image, arange will fail + mx += 1 if np.isnan(mn) or np.isnan(mx): # the data are all-nan return None, None @@ -497,7 +500,7 @@ class ImageItem(GraphicsObject): else: # for float data, let numpy select the bins. bins = np.linspace(mn, mx, 500) - + if len(bins) == 0: bins = [mn, mx] @@ -524,7 +527,7 @@ class ImageItem(GraphicsObject): (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation) """ self.setFlag(self.ItemIgnoresTransformations, b) - + def setScaledMode(self): self.setPxMode(False) @@ -534,14 +537,14 @@ class ImageItem(GraphicsObject): if self.qimage is None: return None return QtGui.QPixmap.fromImage(self.qimage) - + def pixelSize(self): """return scene-size of a single pixel in the image""" br = self.sceneBoundingRect() if self.image is None: return 1,1 return br.width()/self.width(), br.height()/self.height() - + def viewTransformChanged(self): if self.autoDownsample: self.qimage = None @@ -582,7 +585,7 @@ class ImageItem(GraphicsObject): self.menu.addAction(remAct) self.menu.remAct = remAct return self.menu - + def hoverEvent(self, ev): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. @@ -595,7 +598,7 @@ class ImageItem(GraphicsObject): #print(ev.device()) #print(ev.pointerType()) #print(ev.pressure()) - + def drawAt(self, pos, ev=None): pos = [int(pos.x()), int(pos.y())] dk = self.drawKernel @@ -604,7 +607,7 @@ class ImageItem(GraphicsObject): sy = [0,dk.shape[1]] tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] - + for i in [0,1]: dx1 = -min(0, tx[i]) dx2 = min(0, self.image.shape[0]-tx[i]) @@ -620,7 +623,7 @@ class ImageItem(GraphicsObject): ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) mask = self.drawMask src = dk - + if isinstance(self.drawMode, collections.Callable): self.drawMode(dk, self.image, mask, ss, ts, ev) else: @@ -636,7 +639,7 @@ class ImageItem(GraphicsObject): else: raise Exception("Unknown draw mode '%s'" % self.drawMode) self.updateImage() - + def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): self.drawKernel = kernel self.drawKernelCenter = center From fc5e0cd9f41ae64042fafefd1055c234c4b84162 Mon Sep 17 00:00:00 2001 From: Stefan Ecklebe Date: Fri, 12 Oct 2018 15:32:23 +0200 Subject: [PATCH 12/92] Fixed issue #481 --- pyqtgraph/opengl/GLViewWidget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 92332cf5..65b381a1 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -236,6 +236,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): glPopMatrix() def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None): + if pos is not None: + self.opts['center'] = pos if distance is not None: self.opts['distance'] = distance if elevation is not None: From 0f149f38c2707c0877c19c5be09349344cbd3379 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 14 Oct 2018 18:49:03 +0200 Subject: [PATCH 13/92] No warning for arrays with zeros in logscale NumPy evaluates log10(0) to -inf, so there is no reason to show the user a RuntimeWarning. Before, if visualizing data arrays containing zeros in logscale, a RuntimeWarning was shown. --- pyqtgraph/graphicsItems/PlotDataItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6797af64..d8a7aed5 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -514,11 +514,13 @@ class PlotDataItem(GraphicsObject): # Ignore the first bin for fft data if we have a logx scale if self.opts['logMode'][0]: x=x[1:] - y=y[1:] - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) + y=y[1:] + + with np.errstate(divide='ignore'): + if self.opts['logMode'][0]: + x = np.log10(x) + if self.opts['logMode'][1]: + y = np.log10(y) ds = self.opts['downsample'] if not isinstance(ds, int): From 16616c77b7aa637712c949245d9d4945d88592a0 Mon Sep 17 00:00:00 2001 From: Tran Duy Hoa Date: Fri, 26 Oct 2018 23:06:23 +0200 Subject: [PATCH 14/92] Fix bug in GLViewWidget.py call debug.printExc() instead of pyqtgraph.debug.printExc() --- pyqtgraph/opengl/GLViewWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 92332cf5..bbdf9df4 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -427,7 +427,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): ver = glGetString(GL_VERSION).split()[0] if int(ver.split('.')[0]) < 2: from .. import debug - pyqtgraph.debug.printExc() + debug.printExc() raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) else: raise From b575b56edfee7afec66683bc9d06a2675c409745 Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Wed, 16 Jan 2019 16:43:04 +0200 Subject: [PATCH 15/92] avoid calling setLabel repeatedly for AxisItem --- pyqtgraph/graphicsItems/AxisItem.py | 393 ++++++++++++++-------------- 1 file changed, 202 insertions(+), 191 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 19c5e1f0..a0d0bcbd 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -14,10 +14,10 @@ class AxisItem(GraphicsWidget): GraphicsItem showing a single plot axis with ticks, values, and label. Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. Ticks can be extended to draw a grid. - If maxTickLength is negative, ticks point into the plot. + If maxTickLength is negative, ticks point into the plot. """ - - def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True): + + def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): """ ============== =============================================================== **Arguments:** @@ -26,11 +26,19 @@ class AxisItem(GraphicsWidget): into the plot, positive values draw outward. linkView (ViewBox) causes the range of values displayed in the axis to be linked to the visible range of a ViewBox. - showValues (bool) Whether to display values adjacent to ticks + showValues (bool) Whether to display values adjacent to ticks pen (QPen) Pen used when drawing ticks. + text The text (excluding units) to display on the label for this + axis. + units The units for this axis. Units should generally be given + without any scaling prefix (eg, 'V' instead of 'mV'). The + scaling prefix will be automatically prepended based on the + range of data displayed. + **args All extra keyword arguments become CSS style options for + the tag which will surround the axis label and units. ============== =============================================================== """ - + GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None @@ -39,15 +47,15 @@ class AxisItem(GraphicsWidget): raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") if orientation in ['left', 'right']: self.label.rotate(-90) - + self.style = { - 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis + 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis 'tickTextWidth': 30, ## space reserved for tick text - 'tickTextHeight': 18, + 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed 'tickFont': None, - 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick - 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. + 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. (0, 0.8), ## never fill more than 80% of the axis (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis @@ -58,93 +66,93 @@ class AxisItem(GraphicsWidget): 'maxTickLevel': 2, 'maxTextLevel': 2, } - - self.textWidth = 30 ## Keeps track of maximum width / height of tick text + + self.textWidth = 30 ## Keeps track of maximum width / height of tick text self.textHeight = 18 - + # If the user specifies a width / height, remember that setting # indefinitely. self.fixedWidth = None self.fixedHeight = None - - self.labelText = '' - self.labelUnits = '' - self.labelUnitPrefix='' - self.labelStyle = {} + + self.labelText = text + self.labelUnits = units + self.labelUnitPrefix = unitPrefix + self.labelStyle = args self.logMode = False self.tickFont = None - + self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickSpacing = None # used to override default tickSpacing method self.scale = 1.0 self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 - + self.setRange(0, 1) - + if pen is None: self.setPen() else: self.setPen(pen) - + self._linkedView = None if linkView is not None: self.linkToView(linkView) - + self.showLabel(False) - + self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) def setStyle(self, **kwds): """ Set various style options. - + =================== ======================================================= Keyword Arguments: - tickLength (int) The maximum length of ticks in pixels. - Positive values point toward the text; negative + tickLength (int) The maximum length of ticks in pixels. + Positive values point toward the text; negative values point away. tickTextOffset (int) reserved spacing between text and axis in px tickTextWidth (int) Horizontal space reserved for tick text in px tickTextHeight (int) Vertical space reserved for tick text in px autoExpandTextSpace (bool) Automatically expand text space if the tick strings become too long. - tickFont (QFont or None) Determines the font used for tick + tickFont (QFont or None) Determines the font used for tick values. Use None for the default font. - stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis - line is drawn only as far as the last tick. - Otherwise, the line is drawn to the edge of the + stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis + line is drawn only as far as the last tick. + Otherwise, the line is drawn to the edge of the AxisItem boundary. textFillLimits (list of (tick #, % fill) tuples). This structure - determines how the AxisItem decides how many ticks + determines how the AxisItem decides how many ticks should have text appear next to them. Each tuple in the list specifies what fraction of the axis length may be occupied by text, given the number of ticks that already have text displayed. For example:: - + [(0, 0.8), # Never fill more than 80% of the axis - (2, 0.6), # If we already have 2 ticks with text, + (2, 0.6), # If we already have 2 ticks with text, # fill no more than 60% of the axis - (4, 0.4), # If we already have 4 ticks with text, + (4, 0.4), # If we already have 4 ticks with text, # fill no more than 40% of the axis - (6, 0.2)] # If we already have 6 ticks with text, + (6, 0.2)] # If we already have 6 ticks with text, # fill no more than 20% of the axis - + showValues (bool) indicates whether text is displayed adjacent to ticks. =================== ======================================================= - + Added in version 0.9.9 """ for kwd,value in kwds.items(): if kwd not in self.style: raise NameError("%s is not a valid style argument." % kwd) - + if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'): if not isinstance(value, int): raise ValueError("Argument '%s' must be int" % kwd) - + if kwd == 'tickTextOffset': if self.orientation in ('left', 'right'): self.style['tickTextOffset'][0] = value @@ -158,19 +166,19 @@ class AxisItem(GraphicsWidget): self.style[kwd] = value else: self.style[kwd] = value - + self.picture = None self._adjustSize() self.update() - + def close(self): self.scene().removeItem(self.label) self.label = None self.scene().removeItem(self) - + def setGrid(self, grid): """Set the alpha value (0-255) for the grid, or False to disable. - + When grid lines are enabled, the axis tick lines are extended to cover the extent of the linked ViewBox, if any. """ @@ -178,28 +186,28 @@ class AxisItem(GraphicsWidget): self.picture = None self.prepareGeometryChange() self.update() - + def setLogMode(self, log): """ If *log* is True, then ticks are displayed on a logarithmic scale and values - are adjusted accordingly. (This is usually accessed by changing the log mode + are adjusted accordingly. (This is usually accessed by changing the log mode of a :func:`PlotItem `) """ self.logMode = log self.picture = None self.update() - + def setTickFont(self, font): self.tickFont = font self.picture = None self.prepareGeometryChange() ## Need to re-allocate space depending on font size? - + self.update() - + def resizeEvent(self, ev=None): #s = self.size() - + ## Set the position of the label nudge = 5 br = self.label.boundingRect() @@ -218,7 +226,7 @@ class AxisItem(GraphicsWidget): p.setY(int(self.size().height()-br.height()+nudge)) self.label.setPos(p) self.picture = None - + def showLabel(self, show=True): """Show/hide the label text for this axis.""" #self.drawLabel = show @@ -229,10 +237,10 @@ class AxisItem(GraphicsWidget): self._updateHeight() if self.autoSIPrefix: self.updateAutoSIPrefix() - + def setLabel(self, text=None, units=None, unitPrefix=None, **args): """Set the text displayed adjacent to the axis. - + ============== ============================================================= **Arguments:** text The text (excluding units) to display on the label for this @@ -244,23 +252,26 @@ class AxisItem(GraphicsWidget): **args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= - + The final text generated for the label will look like:: - + {text} (prefix{units}) - - Each extra keyword argument will become a CSS option in the above template. + + Each extra keyword argument will become a CSS option in the above template. For example, you can set the font size and color of the label:: - + labelStyle = {'color': '#FFF', 'font-size': '14pt'} axis.setLabel('label text', units='V', **labelStyle) - + """ + show_label = False if text is not None: self.labelText = text - self.showLabel() + show_label = True if units is not None: self.labelUnits = units + show_label = True + if show_label: self.showLabel() if unitPrefix is not None: self.labelUnitPrefix = unitPrefix @@ -270,7 +281,7 @@ class AxisItem(GraphicsWidget): self._adjustSize() self.picture = None self.update() - + def labelString(self): if self.labelUnits == '': if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0: @@ -280,13 +291,13 @@ class AxisItem(GraphicsWidget): else: #print repr(self.labelUnitPrefix), repr(self.labelUnits) units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits)) - + s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units)) - + style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) - + return asUnicode("%s") % (style, asUnicode(s)) - + def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized @@ -305,22 +316,22 @@ class AxisItem(GraphicsWidget): if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed - + def _adjustSize(self): if self.orientation in ['left', 'right']: self._updateWidth() else: self._updateHeight() - + def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added. - + If *height* is None, then the value will be determined automatically based on the size of the tick text.""" self.fixedHeight = h self._updateHeight() - + def _updateHeight(self): if not self.isVisible(): h = 0 @@ -338,20 +349,20 @@ class AxisItem(GraphicsWidget): h += self.label.boundingRect().height() * 0.8 else: h = self.fixedHeight - + self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None - + def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added. - + If *width* is None, then the value will be determined automatically based on the size of the tick text.""" self.fixedWidth = w self._updateWidth() - + def _updateWidth(self): if not self.isVisible(): w = 0 @@ -369,20 +380,20 @@ class AxisItem(GraphicsWidget): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate else: w = self.fixedWidth - + self.setMaximumWidth(w) self.setMinimumWidth(w) self.picture = None - + def pen(self): if self._pen is None: return fn.mkPen(getConfigOption('foreground')) return fn.mkPen(self._pen) - + def setPen(self, *args, **kwargs): """ Set the pen used for drawing text, axes, ticks, and grid lines. - If no arguments are given, the default foreground color will be used + If no arguments are given, the default foreground color will be used (see :func:`setConfigOption `). """ self.picture = None @@ -393,44 +404,44 @@ class AxisItem(GraphicsWidget): self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6] self.setLabel() self.update() - + def setScale(self, scale=None): """ - Set the value scaling for this axis. - + Set the value scaling for this axis. + Setting this value causes the axis to draw ticks and tick labels as if - the view coordinate system were scaled. By default, the axis scaling is + the view coordinate system were scaled. By default, the axis scaling is 1.0. """ # Deprecated usage, kept for backward compatibility - if scale is None: + if scale is None: scale = 1.0 self.enableAutoSIPrefix(True) - + if scale != self.scale: self.scale = scale self.setLabel() self.picture = None self.update() - + def enableAutoSIPrefix(self, enable=True): """ - Enable (or disable) automatic SI prefix scaling on this axis. - - When enabled, this feature automatically determines the best SI prefix + Enable (or disable) automatic SI prefix scaling on this axis. + + When enabled, this feature automatically determines the best SI prefix to prepend to the label units, while ensuring that axis values are scaled - accordingly. - - For example, if the axis spans values from -0.1 to 0.1 and has units set + accordingly. + + For example, if the axis spans values from -0.1 to 0.1 and has units set to 'V' then the axis would display values -100 to 100 and the units would appear as 'mV' - + This feature is enabled by default, and is only available when a suffix (unit string) is provided to display on the label. """ self.autoSIPrefix = enable self.updateAutoSIPrefix() - + def updateAutoSIPrefix(self): if self.label.isVisible(): (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale))) @@ -440,12 +451,12 @@ class AxisItem(GraphicsWidget): self.setLabel(unitPrefix=prefix) else: scale = 1.0 - + self.autoSIPrefixScale = scale self.picture = None self.update() - - + + def setRange(self, mn, mx): """Set the range of values displayed by the axis. Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView `""" @@ -456,14 +467,14 @@ class AxisItem(GraphicsWidget): self.updateAutoSIPrefix() self.picture = None self.update() - + def linkedView(self): """Return the ViewBox this axis is linked to""" if self._linkedView is None: return None else: return self._linkedView() - + def linkToView(self, view): """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view.""" oldView = self.linkedView() @@ -476,11 +487,11 @@ class AxisItem(GraphicsWidget): if oldView is not None: oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) view.sigXRangeChanged.connect(self.linkedViewChanged) - + if oldView is not None: oldView.sigResized.disconnect(self.linkedViewChanged) view.sigResized.connect(self.linkedViewChanged) - + def linkedViewChanged(self, view, newRange=None): if self.orientation in ['right', 'left']: if newRange is None: @@ -496,7 +507,7 @@ class AxisItem(GraphicsWidget): self.setRange(*newRange[::-1]) else: self.setRange(*newRange) - + def boundingRect(self): linkedView = self.linkedView() if linkedView is None or self.grid is False: @@ -515,7 +526,7 @@ class AxisItem(GraphicsWidget): return rect else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) - + def paint(self, p, opt, widget): profiler = debug.Profiler() if self.picture is None: @@ -544,26 +555,26 @@ class AxisItem(GraphicsWidget): [ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ], ... ] - + If *ticks* is None, then the default tick system will be used instead. """ self._tickLevels = ticks self.picture = None self.update() - + def setTickSpacing(self, major=None, minor=None, levels=None): """ - Explicitly determine the spacing of major and minor ticks. This + Explicitly determine the spacing of major and minor ticks. This overrides the default behavior of the tickSpacing method, and disables - the effect of setTicks(). Arguments may be either *major* and *minor*, - or *levels* which is a list of (spacing, offset) tuples for each + the effect of setTicks(). Arguments may be either *major* and *minor*, + or *levels* which is a list of (spacing, offset) tuples for each tick level desired. - + If no arguments are given, then the default behavior of tickSpacing is enabled. - + Examples:: - + # two levels, all offsets = 0 axis.setTickSpacing(5, 1) # three levels, all offsets = 0 @@ -571,7 +582,7 @@ class AxisItem(GraphicsWidget): # reset to default axis.setTickSpacing() """ - + if levels is None: if major is None: levels = None @@ -580,16 +591,16 @@ class AxisItem(GraphicsWidget): self._tickSpacing = levels self.picture = None self.update() - + def tickSpacing(self, minVal, maxVal, size): """Return values describing the desired spacing and offset of ticks. - - This method is called whenever the axis needs to be redrawn and is a + + This method is called whenever the axis needs to be redrawn and is a good method to override in subclasses that require control over tick locations. - + The return value must be a list of tuples, one for each set of ticks:: - + [ (major tick spacing, offset), (minor tick spacing, offset), @@ -600,32 +611,32 @@ class AxisItem(GraphicsWidget): # First check for override tick spacing if self._tickSpacing is not None: return self._tickSpacing - + dif = abs(maxVal - minVal) if dif == 0: return [] - + ## decide optimal minor tick spacing in pixels (this is just aesthetics) optimalTickCount = max(2., np.log(size)) - - ## optimal minor tick spacing + + ## optimal minor tick spacing optimalSpacing = dif / optimalTickCount - + ## the largest power-of-10 spacing which is smaller than optimal p10unit = 10 ** np.floor(np.log10(optimalSpacing)) - + ## Determine major/minor tick spacings which flank the optimal spacing. intervals = np.array([1., 2., 10., 20., 100.]) * p10unit minorIndex = 0 while intervals[minorIndex+1] <= optimalSpacing: minorIndex += 1 - + levels = [ (intervals[minorIndex+2], 0), (intervals[minorIndex+1], 0), #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] - + if self.style['maxTickLevel'] >= 2: ## decide whether to include the last level of ticks minSpacing = min(size / 20., 30.) @@ -633,16 +644,16 @@ class AxisItem(GraphicsWidget): if dif / intervals[minorIndex] <= maxTickCount: levels.append((intervals[minorIndex], 0)) return levels - - - + + + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection ### Determine major/minor tick spacings which flank the optimal spacing. #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit #minorIndex = 0 #while intervals[minorIndex+1] <= optimalSpacing: #minorIndex += 1 - + ### make sure we never see 5 and 2 at the same time #intIndexes = [ #[0,1,3], @@ -651,42 +662,42 @@ class AxisItem(GraphicsWidget): #[3,4,6], #[3,5,6], #][minorIndex] - + #return [ #(intervals[intIndexes[2]], 0), #(intervals[intIndexes[1]], 0), #(intervals[intIndexes[0]], 0) #] - + def tickValues(self, minVal, maxVal, size): """ Return the values and spacing of ticks to draw:: - - [ - (spacing, [major ticks]), - (spacing, [minor ticks]), - ... + + [ + (spacing, [major ticks]), + (spacing, [minor ticks]), + ... ] - + By default, this method calls tickSpacing to determine the correct tick locations. This is a good method to override in subclasses. """ minVal, maxVal = sorted((minVal, maxVal)) - - minVal *= self.scale + + minVal *= self.scale maxVal *= self.scale #size *= self.scale - + ticks = [] tickLevels = self.tickSpacing(minVal, maxVal, size) allValues = np.array([]) for i in range(len(tickLevels)): spacing, offset = tickLevels[i] - + ## determine starting tick start = (np.ceil((minVal-offset) / spacing) * spacing) + offset - + ## determine number of ticks num = int((maxVal-start) / spacing) + 1 values = (np.arange(num) * spacing + start) / self.scale @@ -696,11 +707,11 @@ class AxisItem(GraphicsWidget): values = list(filter(lambda x: all(np.abs(allValues-x) > spacing/self.scale*0.01), values)) allValues = np.concatenate([allValues, values]) ticks.append((spacing/self.scale, values)) - + if self.logMode: return self.logTickValues(minVal, maxVal, size, ticks) - - + + #nticks = [] #for t in ticks: #nvals = [] @@ -708,24 +719,24 @@ class AxisItem(GraphicsWidget): #nvals.append(v/self.scale) #nticks.append((t[0]/self.scale,nvals)) #ticks = nticks - + return ticks - + def logTickValues(self, minVal, maxVal, size, stdTicks): - + ## start with the tick spacing given by tickValues(). ## Any level whose spacing is < 1 needs to be converted to log scale - + ticks = [] for (spacing, t) in stdTicks: if spacing >= 1.0: ticks.append((spacing, t)) - + if len(ticks) < 3: v1 = int(np.floor(minVal)) v2 = int(np.ceil(maxVal)) #major = list(range(v1+1, v2)) - + minor = [] for v in range(v1, v2): minor.extend(v + np.log10(np.arange(1, 10))) @@ -734,21 +745,21 @@ class AxisItem(GraphicsWidget): return ticks def tickStrings(self, values, scale, spacing): - """Return the strings that should be placed next to ticks. This method is called + """Return the strings that should be placed next to ticks. This method is called when redrawing the axis and is a good method to override in subclasses. - The method is called with a list of tick values, a scaling factor (see below), and the - spacing between ticks (this is required since, in some instances, there may be only + The method is called with a list of tick values, a scaling factor (see below), and the + spacing between ticks (this is required since, in some instances, there may be only one tick and thus no other way to determine the tick spacing) - + The scale argument is used when the axis label is displaying units which may have an SI scaling prefix. When determining the text to display, use value*scale to correctly account for this prefix. For example, if the axis label's units are set to 'V', then a tick value of 0.001 might - be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and + be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and thus the tick should display 0.001 * 1000 = 1. """ if self.logMode: return self.logTickStrings(values, scale, spacing) - + places = max(0, np.ceil(-np.log10(spacing*scale))) strings = [] for v in values: @@ -759,27 +770,27 @@ class AxisItem(GraphicsWidget): vstr = ("%%0.%df" % places) % vs strings.append(vstr) return strings - + def logTickStrings(self, values, scale, spacing): return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] - + def generateDrawSpecs(self, p): """ Calls tickValues() and tickStrings() to determine where and how ticks should - be drawn, then generates from this a set of drawing commands to be + be drawn, then generates from this a set of drawing commands to be interpreted by drawPicture(). """ profiler = debug.Profiler() #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) - + linkedView = self.linkedView() if linkedView is None or self.grid is False: tickBounds = bounds else: tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) - + if self.orientation == 'left': span = (bounds.topRight(), bounds.bottomRight()) tickStart = tickBounds.right() @@ -805,7 +816,7 @@ class AxisItem(GraphicsWidget): tickDir = 1 axis = 1 #print tickStart, tickStop, span - + ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: @@ -830,7 +841,7 @@ class AxisItem(GraphicsWidget): for val, strn in level: values.append(val) strings.append(strn) - + ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: @@ -843,29 +854,29 @@ class AxisItem(GraphicsWidget): else: xScale = bounds.width() / dif offset = self.range[0] * xScale - + xRange = [x * xScale - offset for x in self.range] xMin = min(xRange) xMax = max(xRange) - + profiler('init') - + tickPositions = [] # remembers positions of previously drawn ticks - + ## compute coordinates to draw ticks ## draw three different intervals, long ticks first tickSpecs = [] for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] - + ## length of tick tickLength = self.style['tickLength'] / ((i*0.5)+1.0) - + lineAlpha = 255 / (i+1) if self.grid is not False: lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.) - + for v in ticks: ## determine actual position to draw this tick x = (v * xScale) - offset @@ -873,7 +884,7 @@ class AxisItem(GraphicsWidget): tickPositions[i].append(None) continue tickPositions[i].append(x) - + p1 = [x, x] p2 = [x, x] p1[axis] = tickStart @@ -887,7 +898,7 @@ class AxisItem(GraphicsWidget): tickSpecs.append((tickPen, Point(p1), Point(p2))) profiler('compute ticks') - + if self.style['stopAxisAtTick'][0] is True: stop = max(span[0].y(), min(map(min, tickPositions))) if axis == 0: @@ -902,7 +913,7 @@ class AxisItem(GraphicsWidget): span[1].setX(stop) axisSpec = (self.pen(), span[0], span[1]) - + textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text #if self.style['autoExpandTextSpace'] is True: #textWidth = self.textWidth @@ -910,15 +921,15 @@ class AxisItem(GraphicsWidget): #else: #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text - + textSize2 = 0 textRects = [] textSpecs = [] ## list of draw - + # If values are hidden, return early if not self.style['showValues']: return (axisSpec, tickSpecs, textSpecs) - + for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)): ## Get the list of strings to display for this level if tickStrings is None: @@ -926,10 +937,10 @@ class AxisItem(GraphicsWidget): strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) else: strings = tickStrings[i] - + if len(strings) == 0: continue - + ## ignore strings belonging to ticks that were previously ignored for j in range(len(strings)): if tickPositions[i][j] is None: @@ -945,10 +956,10 @@ class AxisItem(GraphicsWidget): ## boundingRect is usually just a bit too large ## (but this probably depends on per-font metrics?) br.setHeight(br.height() * 0.8) - + rects.append(br) textRects.append(rects[-1]) - + if len(textRects) > 0: ## measure all text, make sure there's enough room if axis == 0: @@ -973,7 +984,7 @@ class AxisItem(GraphicsWidget): break if finished: break - + #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) # Determine exactly where tick text should be drawn @@ -1006,24 +1017,24 @@ class AxisItem(GraphicsWidget): #p.drawText(rect, textFlags, vstr) textSpecs.append((rect, textFlags, vstr)) profiler('compute text') - + ## update max text size if needed. self._updateMaxTextSize(textSize2) - + return (axisSpec, tickSpecs, textSpecs) - + def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): profiler = debug.Profiler() p.setRenderHint(p.Antialiasing, False) p.setRenderHint(p.TextAntialiasing, True) - + ## draw long line along axis pen, p1, p2 = axisSpec p.setPen(pen) p.drawLine(p1, p2) p.translate(0.5,0) ## resolves some damn pixel ambiguity - + ## draw ticks for pen, p1, p2 in tickSpecs: p.setPen(pen) @@ -1045,7 +1056,7 @@ class AxisItem(GraphicsWidget): self._updateWidth() else: self._updateHeight() - + def hide(self): GraphicsWidget.hide(self) if self.orientation in ['left', 'right']: @@ -1054,23 +1065,23 @@ class AxisItem(GraphicsWidget): self._updateHeight() def wheelEvent(self, ev): - if self.linkedView() is None: + if self.linkedView() is None: return if self.orientation in ['left', 'right']: self.linkedView().wheelEvent(ev, axis=1) else: self.linkedView().wheelEvent(ev, axis=0) ev.accept() - + def mouseDragEvent(self, event): - if self.linkedView() is None: + if self.linkedView() is None: return if self.orientation in ['left', 'right']: return self.linkedView().mouseDragEvent(event, axis=1) else: return self.linkedView().mouseDragEvent(event, axis=0) - + def mouseClickEvent(self, event): - if self.linkedView() is None: + if self.linkedView() is None: return return self.linkedView().mouseClickEvent(event) From 5a53539be0d05e2d04ac8191f3700e5eb9d1481d Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Wed, 16 Jan 2019 17:04:06 +0200 Subject: [PATCH 16/92] enforce enableMenu in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 493 +++++++++++---------- 1 file changed, 256 insertions(+), 237 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0982cb37..434a3980 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -35,21 +35,21 @@ class WeakList(object): i -= 1 class ChildGroup(ItemGroup): - + def __init__(self, parent): ItemGroup.__init__(self, parent) - - # Used as callback to inform ViewBox when items are added/removed from - # the group. - # Note 1: We would prefer to override itemChange directly on the + + # Used as callback to inform ViewBox when items are added/removed from + # the group. + # Note 1: We would prefer to override itemChange directly on the # ViewBox, but this causes crashes on PySide. # Note 2: We might also like to use a signal rather than this callback - # mechanism, but this causes a different PySide crash. + # mechanism, but this causes a different PySide crash. self.itemsChangedListeners = WeakList() - + # excempt from telling view when transform changes self._GraphicsObject__inform_view_on_change = False - + def itemChange(self, change, value): ret = ItemGroup.itemChange(self, change, value) if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange: @@ -68,19 +68,19 @@ class ChildGroup(ItemGroup): class ViewBox(GraphicsWidget): """ **Bases:** :class:`GraphicsWidget ` - - Box that allows internal scaling/panning of children by mouse drag. + + Box that allows internal scaling/panning of children by mouse drag. This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. - + Features: - + * Scaling contents by mouse or auto-scale when contents change * View linking--multiple views display the same data ranges * Configurable by context menu * Item coordinate mapping methods - + """ - + sigYRangeChanged = QtCore.Signal(object, object) sigXRangeChanged = QtCore.Signal(object, object) sigRangeChangedManually = QtCore.Signal(object) @@ -88,20 +88,20 @@ class ViewBox(GraphicsWidget): sigStateChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object) sigResized = QtCore.Signal(object) - + ## mouse modes PanMode = 3 RectMode = 1 - + ## axes XAxis = 0 YAxis = 1 XYAxes = 2 - + ## for linking views together NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False): """ ============== ============================================================= @@ -114,15 +114,15 @@ class ViewBox(GraphicsWidget): *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` *invertX* (bool) See :func:`invertX ` - *enableMenu* (bool) Whether to display a context menu when + *enableMenu* (bool) Whether to display a context menu when right-clicking on the ViewBox background. *name* (str) Used to register this ViewBox so that it appears in the "Link axis" dropdown inside other ViewBox context menus. This allows the user to manually link - the axes of any other view to this one. + the axes of any other view to this one. ============== ============================================================= """ - + GraphicsWidget.__init__(self, parent) self.name = None self.linksBlocked = False @@ -131,60 +131,60 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. self._lastScene = None ## stores reference to the last known scene this view was a part of. - + self.state = { - + ## separating targetRange and viewRange allows the view to be resized ## while keeping all previously viewed contents visible 'targetRange': [[0,1], [0,1]], ## child coord. range visible [[xmin, xmax], [ymin, ymax]] 'viewRange': [[0,1], [0,1]], ## actual range viewed - + 'yInverted': invertY, 'xInverted': invertX, 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. - 'autoRange': [True, True], ## False if auto range is disabled, + 'autoRange': [True, True], ## False if auto range is disabled, ## otherwise float gives the fraction of data that is visible 'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled - 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot + 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot 'linkedViews': [None, None], ## may be None, "viewName", or weakref.ref(view) ## a name string indicates that the view *should* link to another, but no view with that name exists yet. - + 'mouseEnabled': [enableMouse, enableMouse], - 'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode, + 'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode, 'enableMenu': enableMenu, 'wheelScaleFactor': -1.0 / 8.0, 'background': None, - + # Limits 'limits': { - 'xLimits': [None, None], # Maximum and minimum visible X values - 'yLimits': [None, None], # Maximum and minimum visible Y values + 'xLimits': [None, None], # Maximum and minimum visible X values + 'yLimits': [None, None], # Maximum and minimum visible Y values 'xRange': [None, None], # Maximum and minimum X range - 'yRange': [None, None], # Maximum and minimum Y range + 'yRange': [None, None], # Maximum and minimum Y range } - + } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._itemBoundsCache = weakref.WeakKeyDictionary() - + self.locateGroup = None ## items displayed when using ViewBox.locate(item) - + self.setFlag(self.ItemClipsChildrenToShape) self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses - + ## childGroup is required so that ViewBox has local coordinates similar to device coordinates. ## this is a workaround for a Qt + OpenGL bug that causes improper clipping ## https://bugreports.qt.nokia.com/browse/QTBUG-23723 self.childGroup = ChildGroup(self) self.childGroup.itemsChangedListeners.append(self) - + self.background = QtGui.QGraphicsRectItem(self.rect()) self.background.setParentItem(self) self.background.setZValue(-1e6) self.background.setPen(fn.mkPen(None)) self.updateBackground() - + ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -192,36 +192,39 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.setZValue(1e9) self.rbScaleBox.hide() self.addItem(self.rbScaleBox, ignoreBounds=True) - + ## show target rect for debugging self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.target.setPen(fn.mkPen('r')) self.target.setParentItem(self) self.target.hide() - + self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" - + self.setZValue(-100) self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - + self.setAspectLocked(lockAspect) - + self.border = fn.mkPen(border) - self.menu = ViewBoxMenu(self) - + if enableMenu: + self.menu = ViewBoxMenu(self) + else: + self.menu = None + self.register(name) if name is None: self.updateViewLists() - + def register(self, name): """ - Add this ViewBox to the registered list of views. - + Add this ViewBox to the registered list of views. + This allows users to manually link the axes of any other ViewBox to - this one. The specified *name* will appear in the drop-down lists for + this one. The specified *name* will appear in the drop-down lists for axis linking in the context menus of all other views. - + The same can be accomplished by initializing the ViewBox with the *name* attribute. """ ViewBox.AllViews[self] = None @@ -248,7 +251,7 @@ class ViewBox(GraphicsWidget): def implements(self, interface): return interface == 'ViewBox' - + # removed due to https://bugreports.qt-project.org/browse/PYSIDE-86 #def itemChange(self, change, value): ## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt @@ -263,9 +266,9 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - + def checkSceneChange(self): - # ViewBox needs to receive sigPrepareForPaint from its scene before + # ViewBox needs to receive sigPrepareForPaint from its scene before # being painted. However, we have no way of being informed when the # scene has changed in order to make this connection. The usual way # to do this is via itemChange(), but bugs prevent this approach @@ -280,16 +283,16 @@ class ViewBox(GraphicsWidget): scene.sigPrepareForPaint.connect(self.prepareForPaint) self.prepareForPaint() self._lastScene = scene - + def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) # don't check whether auto range is enabled here--only check when setting dirty flag. - if self._autoRangeNeedsUpdate: # and autoRangeEnabled: + if self._autoRangeNeedsUpdate: # and autoRangeEnabled: self.updateAutoRange() self.updateMatrix() - + def getState(self, copy=True): - """Return the current state of the ViewBox. + """Return the current state of the ViewBox. Linked views are always converted to view names in the returned state.""" state = self.state.copy() views = [] @@ -305,7 +308,7 @@ class ViewBox(GraphicsWidget): return deepcopy(state) else: return state - + def setState(self, state): """Restore the state of this ViewBox. (see also getState)""" @@ -313,17 +316,24 @@ class ViewBox(GraphicsWidget): self.setXLink(state['linkedViews'][0]) self.setYLink(state['linkedViews'][1]) del state['linkedViews'] - + self.state.update(state) + + if self.state['enableMenu'] and self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + else: + self.menu = None + self.updateViewRange() self.sigStateChanged.emit(self) def setBackgroundColor(self, color): """ Set the background color of the ViewBox. - + If color is None, then no background will be drawn. - + Added in version 0.9.9 """ self.background.setVisible(color is not None) @@ -348,10 +358,10 @@ class ViewBox(GraphicsWidget): self.setMouseMode(ViewBox.PanMode) else: raise Exception('graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")' % mode) - + def innerSceneItem(self): return self.childGroup - + def setMouseEnabled(self, x=None, y=None): """ Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False. @@ -362,17 +372,24 @@ class ViewBox(GraphicsWidget): if y is not None: self.state['mouseEnabled'][1] = y self.sigStateChanged.emit(self) - + def mouseEnabled(self): return self.state['mouseEnabled'][:] - + def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu + if enableMenu: + if self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + else: + self.menu.setParent(None) + self.menu = None self.sigStateChanged.emit(self) def menuEnabled(self): - return self.state.get('enableMenu', True) - + return self.state.get('enableMenu', True) + def addItem(self, item, ignoreBounds=False): """ Add a QGraphicsItem to this view. The view will include this item when determining how to set its range @@ -387,7 +404,7 @@ class ViewBox(GraphicsWidget): if not ignoreBounds: self.addedItems.append(item) self.updateAutoRange() - + def removeItem(self, item): """Remove an item from this view.""" try: @@ -402,7 +419,7 @@ class ViewBox(GraphicsWidget): self.removeItem(i) for ch in self.childGroup.childItems(): ch.setParentItem(None) - + def resizeEvent(self, ev): self._matrixNeedsUpdate = True self.linkedXChanged() @@ -413,7 +430,7 @@ class ViewBox(GraphicsWidget): self.sigStateChanged.emit(self) self.background.setRect(self.rect()) self.sigResized.emit(self) - + def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy @@ -427,13 +444,13 @@ class ViewBox(GraphicsWidget): except: print("make qrectf failed:", self.state['viewRange']) raise - + def targetRange(self): return [x[:] for x in self.state['targetRange']] ## return copy - - def targetRect(self): + + def targetRect(self): """ - Return the region which has been requested to be visible. + Return the region which has been requested to be visible. (this is not necessarily the same as the region that is *actually* visible-- resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ) """ @@ -455,30 +472,30 @@ class ViewBox(GraphicsWidget): def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. - Must specify at least one of *rect*, *xRange*, or *yRange*. - + Must specify at least one of *rect*, *xRange*, or *yRange*. + ================== ===================================================================== **Arguments:** *rect* (QRectF) The full range that should be visible in the view box. *xRange* (min,max) The range that should be visible along the x-axis. *yRange* (min,max) The range that should be visible along the y-axis. - *padding* (float) Expand the view by a fraction of the requested range. + *padding* (float) Expand the view by a fraction of the requested range. By default, this value is set between 0.02 and 0.1 depending on the size of the ViewBox. - *update* (bool) If True, update the range of the ViewBox immediately. + *update* (bool) If True, update the range of the ViewBox immediately. Otherwise, the update is deferred until before the next render. *disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left unchanged. ================== ===================================================================== - + """ #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding #import traceback #traceback.print_stack() - + changes = {} # axes setRequested = [False, False] - + if rect is not None: changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]} setRequested = [True, True] @@ -492,27 +509,27 @@ class ViewBox(GraphicsWidget): if len(changes) == 0: print(rect) raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) - + # Update axes one at a time changed = [False, False] for ax, range in changes.items(): mn = min(range) mx = max(range) - - # If we requested 0 range, try to preserve previous scale. + + # If we requested 0 range, try to preserve previous scale. # Otherwise just pick an arbitrary scale. - if mn == mx: + if mn == mx: dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0] if dy == 0: dy = 1 mn -= dy*0.5 mx += dy*0.5 xpad = 0.0 - + # Make sure no nan/inf get through if not all(np.isfinite([mn, mx])): raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx))) - + # Apply padding if padding is None: xpad = self.suggestPadding(ax) @@ -521,20 +538,20 @@ class ViewBox(GraphicsWidget): p = (mx-mn) * xpad mn -= p mx += p - + # Set target range if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] changed[ax] = True - - # Update viewRange to match targetRange as closely as possible while + + # Update viewRange to match targetRange as closely as possible while # accounting for aspect ratio constraint lockX, lockY = setRequested if lockX and lockY: lockX = False lockY = False self.updateViewRange(lockX, lockY) - + # Disable auto-range for each axis that was requested to be set if disableAutoRange: xOff = False if setRequested[0] else None @@ -545,11 +562,11 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): self.sigStateChanged.emit(self) - + # Update target rect for debugging if self.target.isVisible(): self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - + # If ortho axes have auto-visible-only, update them now # Note that aspect ratio constraints and auto-visible probably do not work together.. if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): @@ -559,15 +576,15 @@ class ViewBox(GraphicsWidget): def setYRange(self, min, max, padding=None, update=True): """ - Set the visible Y range of the view to [*min*, *max*]. + Set the visible Y range of the view to [*min*, *max*]. The *padding* argument causes the range to be set larger by the fraction specified. (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox) """ self.setRange(yRange=[min, max], update=update, padding=padding) - + def setXRange(self, min, max, padding=None, update=True): """ - Set the visible X range of the view to [*min*, *max*]. + Set the visible X range of the view to [*min*, *max*]. The *padding* argument causes the range to be set larger by the fraction specified. (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox) """ @@ -576,9 +593,9 @@ class ViewBox(GraphicsWidget): def autoRange(self, padding=None, items=None, item=None): """ Set the range of the view box to make all children visible. - Note that this is not the same as enableAutoRange, which causes the view to + Note that this is not the same as enableAutoRange, which causes the view to automatically auto-range whenever its contents are changed. - + ============== ============================================================ **Arguments:** padding The fraction of the total data range to add on to the final @@ -593,10 +610,10 @@ class ViewBox(GraphicsWidget): else: print("Warning: ViewBox.autoRange(item=__) is deprecated. Use 'items' argument instead.") bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect() - + if bounds is not None: self.setRange(bounds, padding=padding) - + def suggestPadding(self, axis): l = self.width() if axis==0 else self.height() if l > 0: @@ -604,31 +621,31 @@ class ViewBox(GraphicsWidget): else: padding = 0.02 return padding - + def setLimits(self, **kwds): """ Set limits that constrain the possible view ranges. - - **Panning limits**. The following arguments define the region within the + + **Panning limits**. The following arguments define the region within the viewbox coordinate system that may be accessed by panning the view. - + =========== ============================================================ xMin Minimum allowed x-axis value xMax Maximum allowed x-axis value yMin Minimum allowed y-axis value yMax Maximum allowed y-axis value - =========== ============================================================ - + =========== ============================================================ + **Scaling limits**. These arguments prevent the view being zoomed in or out too far. - + =========== ============================================================ minXRange Minimum allowed left-to-right span across the view. maxXRange Maximum allowed left-to-right span across the view. minYRange Minimum allowed top-to-bottom span across the view. maxYRange Maximum allowed top-to-bottom span across the view. =========== ============================================================ - + Added in version 0.9.9 """ update = False @@ -648,28 +665,28 @@ class ViewBox(GraphicsWidget): if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: self.state['limits'][lname][mnmx] = kwds[kwd] update = True - + if update: self.updateViewRange() - + def scaleBy(self, s=None, center=None, x=None, y=None): """ Scale by *s* around given center point (or center of view). *s* may be a Point or tuple (x, y). - - Optionally, x or y may be specified individually. This allows the other + + Optionally, x or y may be specified individually. This allows the other axis to be left unaffected (note that using a scale factor of 1.0 may cause slight changes due to floating-point error). """ if s is not None: x, y = s[0], s[1] - + affect = [x is not None, y is not None] if not any(affect): return - + scale = Point([1.0 if x is None else x, 1.0 if y is None else y]) - + if self.state['aspectLocked'] is not False: scale[0] = scale[1] @@ -678,21 +695,21 @@ class ViewBox(GraphicsWidget): center = Point(vr.center()) else: center = Point(center) - + tl = center + (vr.topLeft()-center) * scale br = center + (vr.bottomRight()-center) * scale - + if not affect[0]: self.setYRange(tl.y(), br.y(), padding=0) elif not affect[1]: self.setXRange(tl.x(), br.x(), padding=0) else: self.setRange(QtCore.QRectF(tl, br), padding=0) - + def translateBy(self, t=None, x=None, y=None): """ Translate the view by *t*, which may be a Point or tuple (x, y). - + Alternately, x or y may be specified independently, leaving the other axis unchanged (note that using a translation of 0 may still cause small changes due to floating-point error). @@ -708,7 +725,7 @@ class ViewBox(GraphicsWidget): y = vr.top()+y, vr.bottom()+y if x is not None or y is not None: self.setRange(xRange=x, yRange=y, padding=0) - + def enableAutoRange(self, axis=None, enable=True, x=None, y=None): """ Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both @@ -724,15 +741,15 @@ class ViewBox(GraphicsWidget): if y is not None: self.enableAutoRange(ViewBox.YAxis, y) return - + if enable is True: enable = 1.0 - + if axis is None: axis = ViewBox.XYAxes - + needAutoRangeUpdate = False - + if axis == ViewBox.XYAxes or axis == 'xy': axes = [0, 1] elif axis == ViewBox.XAxis or axis == 'x': @@ -741,18 +758,18 @@ class ViewBox(GraphicsWidget): axes = [1] else: raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.') - + for ax in axes: if self.state['autoRange'][ax] != enable: # If we are disabling, do one last auto-range to make sure that # previously scheduled auto-range changes are enacted if enable is False and self._autoRangeNeedsUpdate: self.updateAutoRange() - + self.state['autoRange'][ax] = enable self._autoRangeNeedsUpdate |= (enable is not False) self.update() - + self.sigStateChanged.emit(self) def disableAutoRange(self, axis=None): @@ -784,30 +801,30 @@ class ViewBox(GraphicsWidget): self.state['autoVisibleOnly'][1] = y if y is True: self.state['autoVisibleOnly'][0] = False - + if x is not None or y is not None: self.updateAutoRange() def updateAutoRange(self): ## Break recursive loops when auto-ranging. - ## This is needed because some items change their size in response + ## This is needed because some items change their size in response ## to a view change. if self._updatingRange: return - + self._updatingRange = True try: targetRect = self.viewRange() if not any(self.state['autoRange']): return - + fractionVisible = self.state['autoRange'][:] for i in [0,1]: if type(fractionVisible[i]) is bool: fractionVisible[i] = 1.0 childRange = None - + order = [0,1] if self.state['autoVisibleOnly'][0] is True: order = [1,0] @@ -820,11 +837,11 @@ class ViewBox(GraphicsWidget): oRange = [None, None] oRange[ax] = targetRect[1-ax] childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange) - + else: if childRange is None: childRange = self.childrenBounds(frac=fractionVisible) - + ## Make corrections to range xr = childRange[ax] if xr is not None: @@ -839,32 +856,32 @@ class ViewBox(GraphicsWidget): childRange[ax][1] += wp targetRect[ax] = childRange[ax] args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] - + # check for and ignore bad ranges for k in ['xRange', 'yRange']: if k in args: if not np.all(np.isfinite(args[k])): r = args.pop(k) #print("Warning: %s is invalid: %s" % (k, str(r)) - + if len(args) == 0: return args['padding'] = 0 args['disableAutoRange'] = False - + self.setRange(**args) finally: self._autoRangeNeedsUpdate = False self._updatingRange = False - + def setXLink(self, view): """Link this view's X axis to another view. (see LinkView)""" self.linkView(self.XAxis, view) - + def setYLink(self, view): """Link this view's Y axis to another view. (see LinkView)""" self.linkView(self.YAxis, view) - + def linkView(self, axis, view): """ Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. @@ -896,8 +913,8 @@ class ViewBox(GraphicsWidget): except (TypeError, RuntimeError): ## This can occur if the view has been deleted already pass - - + + if view is None or isinstance(view, basestring): self.state['linkedViews'][axis] = view else: @@ -910,10 +927,10 @@ class ViewBox(GraphicsWidget): else: if self.autoRangeEnabled()[axis] is False: slot() - - + + self.sigStateChanged.emit(self) - + def blockLink(self, b): self.linksBlocked = b ## prevents recursive plot-change propagation @@ -926,7 +943,7 @@ class ViewBox(GraphicsWidget): ## called when y range of linked view has changed view = self.linkedView(1) self.linkedViewChanged(view, ViewBox.YAxis) - + def linkedView(self, ax): ## Return the linked view for axis *ax*. ## this method _always_ returns either a ViewBox or None. @@ -939,19 +956,19 @@ class ViewBox(GraphicsWidget): def linkedViewChanged(self, view, axis): if self.linksBlocked or view is None: return - + #print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis] vr = view.viewRect() vg = view.screenGeometry() sg = self.screenGeometry() if vg is None or sg is None: return - + view.blockLink(True) try: if axis == ViewBox.XAxis: overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left()) - if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, + if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, ## then just replicate the view x1 = vr.left() x2 = vr.right() @@ -966,7 +983,7 @@ class ViewBox(GraphicsWidget): self.setXRange(x1, x2, padding=0) else: overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top()) - if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, + if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, ## then just replicate the view y1 = vr.top() y2 = vr.bottom() @@ -981,7 +998,7 @@ class ViewBox(GraphicsWidget): self.setYRange(y1, y2, padding=0) finally: view.blockLink(False) - + def screenGeometry(self): """return the screen geometry of the viewbox""" v = self.getViewWidget() @@ -996,7 +1013,7 @@ class ViewBox(GraphicsWidget): def itemsChanged(self): ## called when items are added/removed from self.childGroup self.updateAutoRange() - + def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False): @@ -1008,7 +1025,7 @@ class ViewBox(GraphicsWidget): key = 'xy'[ax] + 'Inverted' if self.state[key] == inv: return - + self.state[key] = inv self._matrixNeedsUpdate = True # updateViewRange won't detect this for us self.updateViewRange() @@ -1024,7 +1041,7 @@ class ViewBox(GraphicsWidget): def yInverted(self): return self.state['yInverted'] - + def invertX(self, b=True): """ By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. @@ -1033,14 +1050,14 @@ class ViewBox(GraphicsWidget): def xInverted(self): return self.state['xInverted'] - + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. By default, the ratio is set to 1; x and y both have the same scaling. This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio. """ - + if not lock: if self.state['aspectLocked'] == False: return @@ -1059,16 +1076,16 @@ class ViewBox(GraphicsWidget): self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now self.updateViewRange() - + self.updateAutoRange() self.updateViewRange() self.sigStateChanged.emit(self) - + def childTransform(self): """ Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) - """ + """ self.updateMatrix() m = self.childGroup.transform() return m @@ -1094,7 +1111,7 @@ class ViewBox(GraphicsWidget): """Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" self.updateMatrix() return self.mapToScene(self.mapFromView(obj)) - + def mapFromItemToView(self, item, obj): """Maps *obj* from the local coordinate system of *item* to the view coordinates""" self.updateMatrix() @@ -1109,17 +1126,17 @@ class ViewBox(GraphicsWidget): def mapViewToDevice(self, obj): self.updateMatrix() return self.mapToDevice(self.mapFromView(obj)) - + def mapDeviceToView(self, obj): self.updateMatrix() return self.mapToView(self.mapFromDevice(obj)) - + def viewPixelSize(self): """Return the (width, height) of a screen pixel in view coordinates.""" o = self.mapToView(Point(0,0)) px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] return (px.length(), py.length()) - + def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() @@ -1133,12 +1150,12 @@ class ViewBox(GraphicsWidget): s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor s = [(None if m is False else s) for m in mask] center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) - + self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(mask) ev.accept() - + def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): ev.accept() @@ -1146,8 +1163,9 @@ class ViewBox(GraphicsWidget): def raiseContextMenu(self, ev): menu = self.getMenu(ev) - self.scene().addParentContextMenus(self, menu, ev) - menu.popup(ev.screenPos().toPoint()) + if menu is not None: + self.scene().addParentContextMenus(self, menu, ev) + menu.popup(ev.screenPos().toPoint()) def getMenu(self, ev): return self.menu @@ -1158,7 +1176,7 @@ class ViewBox(GraphicsWidget): def mouseDragEvent(self, ev, axis=None): ## if axis is specified, event will only affect that axis. ev.accept() ## we accept all buttons - + pos = ev.pos() lastPos = ev.lastPos() dif = pos - lastPos @@ -1189,7 +1207,7 @@ class ViewBox(GraphicsWidget): tr = self.mapToView(tr) - self.mapToView(Point(0,0)) x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None - + self._resetTarget() if x is not None or y is not None: self.translateBy(x=x, y=y) @@ -1198,18 +1216,18 @@ class ViewBox(GraphicsWidget): #print "vb.rightDrag" if self.state['aspectLocked'] is not False: mask[0] = 0 - + dif = ev.screenPos() - ev.lastScreenPos() dif = np.array([dif.x(), dif.y()]) dif[0] *= -1 s = ((mask * 0.02) + 1) ** dif - + tr = self.childGroup.transform() tr = fn.invertQTransform(tr) - + x = s[0] if mouseEnabled[0] == 1 else None y = s[1] if mouseEnabled[1] == 1 else None - + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) self._resetTarget() self.scaleBy(x=x, y=y, center=center) @@ -1223,7 +1241,7 @@ class ViewBox(GraphicsWidget): ctrl-A : zooms out to the default "full" view of the plot ctrl-+ : moves forward in the zooming stack (if it exists) ctrl-- : moves backward in the zooming stack (if it exists) - + """ ev.accept() if ev.text() == '-': @@ -1259,12 +1277,12 @@ class ViewBox(GraphicsWidget): """Return a list of all children and grandchildren of this ViewBox""" if item is None: item = self.childGroup - + children = [item] for ch in item.childItems(): children.extend(self.allChildren(ch)) return children - + def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): """Return the bounding range of all children. [[xmin, xmax], [ymin, ymax]] @@ -1273,19 +1291,19 @@ class ViewBox(GraphicsWidget): profiler = debug.Profiler() if items is None: items = self.addedItems - + ## measure pixel dimensions in view box px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()] - + ## First collect all boundary information itemBounds = [] for item in items: if not item.isVisible() or not item.scene() is self.scene(): continue - + useX = True useY = True - + if hasattr(item, 'dataBounds'): if frac is None: frac = (1.0, 1.0) @@ -1301,23 +1319,23 @@ class ViewBox(GraphicsWidget): bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) bounds = self.mapFromItemToView(item, bounds).boundingRect() - + if not any([useX, useY]): continue - + ## If we are ignoring only one axis, we need to check for rotations if useX != useY: ## != means xor ang = round(item.transformAngle()) if ang == 0 or ang == 180: pass elif ang == 90 or ang == 270: - useX, useY = useY, useX + useX, useY = useY, useX else: ## Item is rotated at non-orthogonal angle, ignore bounds entirely. ## Not really sure what is the expected behavior in this case. - continue ## need to check for item rotations and decide how best to apply this boundary. - - + continue ## need to check for item rotations and decide how best to apply this boundary. + + itemBounds.append((bounds, useX, useY, pxPad)) else: if int(item.flags() & item.ItemHasNoContents) > 0: @@ -1326,7 +1344,7 @@ class ViewBox(GraphicsWidget): bounds = item.boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect() itemBounds.append((bounds, True, True, 0)) - + ## determine tentative new range range = [None, None] for bounds, useX, useY, px in itemBounds: @@ -1341,7 +1359,7 @@ class ViewBox(GraphicsWidget): else: range[0] = [bounds.left(), bounds.right()] profiler() - + ## Now expand any bounds that have a pixel margin ## This must be done _after_ we have a good estimate of the new range ## to ensure that the pixel size is roughly accurate. @@ -1363,7 +1381,7 @@ class ViewBox(GraphicsWidget): range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize) return range - + def childrenBoundingRect(self, *args, **kwds): range = self.childrenBounds(*args, **kwds) tr = self.targetRange() @@ -1371,31 +1389,31 @@ class ViewBox(GraphicsWidget): range[0] = tr[0] if range[1] is None: range[1] = tr[1] - + bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0]) return bounds - + def updateViewRange(self, forceX=False, forceY=False): - ## Update viewRange to match targetRange as closely as possible, given - ## aspect ratio constraints. The *force* arguments are used to indicate + ## Update viewRange to match targetRange as closely as possible, given + ## aspect ratio constraints. The *force* arguments are used to indicate ## which axis (if any) should be unchanged when applying constraints. viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] - + #-------- Make correction for aspect ratio constraint ---------- - + # aspect is (widget w/h) / (view range w/h) aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: - + ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 ## This is the view range aspect ratio we need to obey aspect constraint viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect viewRatio = 1 if viewRatio == 0 else viewRatio - + # Decide which range to keep unchanged #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] if forceX: @@ -1403,11 +1421,11 @@ class ViewBox(GraphicsWidget): elif forceY: ax = 1 else: - # if we are not required to keep a particular axis unchanged, + # if we are not required to keep a particular axis unchanged, # then make the entire target range visible ax = 0 if targetRatio > viewRatio else 1 - - if ax == 0: + + if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: @@ -1420,27 +1438,27 @@ class ViewBox(GraphicsWidget): changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - + # ----------- Make corrections for view limits ----------- - + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] - + for axis in [0, 1]: if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: continue - + # max range cannot be larger than bounds, if they are given if limits[axis][0] is not None and limits[axis][1] is not None: if maxRng[axis] is not None: maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) else: maxRng[axis] = limits[axis][1]-limits[axis][0] - + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) #print "Starting range:", viewRange[axis] - + # Apply xRange, yRange diff = viewRange[axis][1] - viewRange[axis][0] if maxRng[axis] is not None and diff > maxRng[axis]: @@ -1451,12 +1469,12 @@ class ViewBox(GraphicsWidget): changed[axis] = True else: delta = 0 - + viewRange[axis][0] -= delta/2. viewRange[axis][1] += delta/2. - + #print "after applying min/max:", viewRange[axis] - + # Apply xLimits, yLimits mn, mx = limits[axis] if mn is not None and viewRange[axis][0] < mn: @@ -1469,23 +1487,23 @@ class ViewBox(GraphicsWidget): viewRange[axis][0] += delta viewRange[axis][1] += delta changed[axis] = True - + #print "after applying edge limits:", viewRange[axis] changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange - + # emit range change signals if changed[0]: self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) if changed[1]: self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - + if any(changed): self._matrixNeedsUpdate = True self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: @@ -1493,14 +1511,14 @@ class ViewBox(GraphicsWidget): link = self.linkedView(ax) if link is not None: link.linkedViewChanged(self, ax) - + def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() - + vr = self.viewRect() if vr.height() == 0 or vr.width() == 0: return @@ -1510,30 +1528,30 @@ class ViewBox(GraphicsWidget): if self.state['xInverted']: scale = scale * Point(-1, 1) m = QtGui.QTransform() - + ## First center the viewport at 0 center = bounds.center() m.translate(center.x(), center.y()) - + ## Now scale and translate properly m.scale(scale[0], scale[1]) st = Point(vr.center()) m.translate(-st[0], -st[1]) - + self.childGroup.setTransform(m) - + self.sigTransformChanged.emit(self) ## segfaults here: 1 self._matrixNeedsUpdate = False def paint(self, p, opt, widget): self.checkSceneChange() - + if self.border is not None: bounds = self.shape() p.setPen(self.border) #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) p.drawPath(bounds) - + #p.setPen(fn.mkPen('r')) #path = QtGui.QPainterPath() #path.addRect(self.targetRect()) @@ -1547,27 +1565,28 @@ class ViewBox(GraphicsWidget): else: self.background.show() self.background.setBrush(fn.mkBrush(bg)) - + def updateViewLists(self): try: self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - + def cmpViews(a, b): wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) alpha = cmp(a.name, b.name) return wins + alpha - + ## make a sorted list of all named views nv = list(ViewBox.NamedViews.values()) sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList - + if self in nv: nv.remove(self) - - self.menu.setViewList(nv) - + + if self.menu is not None: + self.menu.setViewList(nv) + for ax in [0,1]: link = self.state['linkedViews'][ax] if isinstance(link, basestring): ## axis has not been linked yet; see if it's possible now @@ -1601,7 +1620,7 @@ class ViewBox(GraphicsWidget): for k in ViewBox.AllViews: if isQObjectAlive(k) and getConfigOption('crashWarning'): sys.stderr.write('Warning: ViewBox should be closed before application exit.\n') - + try: k.destroyed.disconnect() except RuntimeError: ## signal is already disconnected. @@ -1610,7 +1629,7 @@ class ViewBox(GraphicsWidget): pass except AttributeError: # PySide has deleted signal pass - + def locate(self, item, timeout=3.0, children=False): """ Temporarily display the bounding rect of an item and lines connecting to the center of the view. @@ -1618,16 +1637,16 @@ class ViewBox(GraphicsWidget): if allChildren is True, then the bounding rect of all item's children will be shown instead. """ self.clearLocate() - + if item.scene() is not self.scene(): raise Exception("Item does not share a scene with this ViewBox.") - + c = self.viewRect().center() if children: br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect() else: br = self.mapFromItemToView(item, item.boundingRect()).boundingRect() - + g = ItemGroup() g.setParentItem(self.childGroup) self.locateGroup = g @@ -1638,11 +1657,11 @@ class ViewBox(GraphicsWidget): line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y()) line.setParentItem(g) g.lines.append(line) - + for item in g.childItems(): item.setPen(fn.mkPen(color='y', width=3)) g.setZValue(1000000) - + if children: g.path = QtGui.QGraphicsPathItem(g.childrenShape()) else: @@ -1650,9 +1669,9 @@ class ViewBox(GraphicsWidget): g.path.setParentItem(g) g.path.setPen(fn.mkPen('g')) g.path.setZValue(100) - + QtCore.QTimer.singleShot(timeout*1000, self.clearLocate) - + def clearLocate(self): if self.locateGroup is None: return From e4681b720108e375486ea8e22d0b9bf41ad7a53a Mon Sep 17 00:00:00 2001 From: Sara Zanzottera Date: Thu, 17 Jan 2019 11:19:16 +0100 Subject: [PATCH 17/92] Fix issue #811 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0982cb37..610746b2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1014,7 +1014,10 @@ class ViewBox(GraphicsWidget): self.updateViewRange() self.update() self.sigStateChanged.emit(self) - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) + if ax: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) + else: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) def invertY(self, b=True): """ From 82d2b757e47ae5effe15cab9d7acd67987c78c22 Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Fri, 18 Jan 2019 10:31:37 +0200 Subject: [PATCH 18/92] speed up AxisItem __init__ --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index a0d0bcbd..3e358870 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -88,6 +88,8 @@ class AxisItem(GraphicsWidget): self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 + self.showLabel(False) + self.setRange(0, 1) if pen is None: @@ -99,8 +101,6 @@ class AxisItem(GraphicsWidget): if linkView is not None: self.linkToView(linkView) - self.showLabel(False) - self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) From e2ca71a65ca3459d145717f3fdf537709a09c6c2 Mon Sep 17 00:00:00 2001 From: ronpandolfi Date: Fri, 1 Feb 2019 19:44:54 -0800 Subject: [PATCH 19/92] Fix for PySide2; QtCore.QPoint.__sub__ no longer works with tuples --- pyqtgraph/widgets/GraphicsView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index e1a7327e..b81eab9d 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -362,7 +362,7 @@ class GraphicsView(QtGui.QGraphicsView): def mouseMoveEvent(self, ev): if self.lastMousePos is None: self.lastMousePos = Point(ev.pos()) - delta = Point(ev.pos() - self.lastMousePos) + delta = Point(ev.pos() - QtCore.QPoint(*self.lastMousePos)) self.lastMousePos = Point(ev.pos()) QtGui.QGraphicsView.mouseMoveEvent(self, ev) From 691da09eb0897ddc1361b72a122f20e4c94b5851 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:36:02 -0500 Subject: [PATCH 20/92] MNT: do not use 'is' with literals, use == py3.8 gives a syntax warning on this --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index b8face5e..ae74c5b6 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -986,8 +986,8 @@ class PlotItem(GraphicsWidget): self._menuEnabled = enableMenu if enableViewBoxMenu is None: return - if enableViewBoxMenu is 'same': - enableViewBoxMenu = enableMenu + if enableViewBoxMenu == 'same': + enableViewBoxMenu = enableMenu self.vb.setMenuEnabled(enableViewBoxMenu) def menuEnabled(self): From 4fe90bb21514a729d76a877c394b970234b066ef Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:39:45 -0500 Subject: [PATCH 21/92] MNT: escape docstrings that have rst escaping in them --- pyqtgraph/graphicsItems/ROI.py | 41 ++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 84a8d0bd..a710f808 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1102,9 +1102,9 @@ class ROI(GraphicsObject): return bounds, tr def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): - """Use the position and orientation of this ROI relative to an imageItem + r"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. - + =================== ==================================================== **Arguments** data The array to slice from. Note that this array does @@ -1524,9 +1524,9 @@ class TestROI(ROI): class RectROI(ROI): - """ + r""" Rectangular ROI subclass with a single scale handle at the top-right corner. - + ============== ============================================================= **Arguments** pos (length-2 sequence) The position of the ROI origin. @@ -1586,11 +1586,13 @@ class LineROI(ROI): + + class MultiRectROI(QtGui.QGraphicsObject): - """ - Chain of rectangular ROIs connected by handles. - - This is generally used to mark a curved path through + r""" + Chain of rectangular ROIs connected by handles. + + This is generally used to mark a curved path through an image similarly to PolyLineROI. It differs in that each segment of the chain is rectangular instead of linear and thus has width. @@ -1724,12 +1726,12 @@ class MultiLineROI(MultiRectROI): MultiRectROI.__init__(self, *args, **kwds) print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") - + class EllipseROI(ROI): - """ + r""" Elliptical ROI subclass with one scale handle and one rotation handle. - - + + ============== ============================================================= **Arguments** pos (length-2 sequence) The position of the ROI's origin. @@ -1810,8 +1812,9 @@ class EllipseROI(ROI): return self.path + class CircleROI(EllipseROI): - """ + r""" Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled proportionally to maintain its aspect ratio. @@ -1878,13 +1881,13 @@ class PolygonROI(ROI): sc['angle'] = self.state['angle'] return sc - + class PolyLineROI(ROI): - """ + r""" Container class for multiple connected LineSegmentROIs. - + This class allows the user to draw paths of multiple line segments. - + ============== ============================================================= **Arguments** positions (list of length-2 sequences) The list of points in the path. @@ -2076,9 +2079,9 @@ class PolyLineROI(ROI): class LineSegmentROI(ROI): - """ + r""" ROI subclass with two freely-moving handles defining a line. - + ============== ============================================================= **Arguments** positions (list of two length-2 sequences) The endpoints of the line From 0649ff8f3cb4605484f1bc1fd13bc55e9207ba0a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:41:06 -0500 Subject: [PATCH 22/92] MNT: do not use 'is not' on literal py38 raises a SyntaxWarning on this --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index f85b64dd..7272aef3 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -186,7 +186,7 @@ class HistogramLUTItem(GraphicsWidget): """Return a lookup table from the color gradient defined by this HistogramLUTItem. """ - if self.levelMode is not 'mono': + if self.levelMode != 'mono': return None if n is None: if img.dtype == np.uint8: From da1bf54ec86de69b2ae884e5bdb896cd224010b8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:41:54 -0500 Subject: [PATCH 23/92] MNT: use raw for regular expression --- pyqtgraph/parametertree/Parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index be77c9ff..df6b1492 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -612,7 +612,7 @@ class Parameter(QtCore.QObject): def incrementName(self, name): ## return an unused name by adding a number to the name given - base, num = re.match('(.*)(\d*)', name).groups() + base, num = re.match(r'(.*)(\d*)', name).groups() numLen = len(num) if numLen == 0: num = 2 From 2817b95c93c3d953c4b7c9cdeec928f21f7f9b8c Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 May 2019 18:45:15 -0700 Subject: [PATCH 24/92] Set path attr in case ErrorBarItem initialized without data --- pyqtgraph/graphicsItems/ErrorBarItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 986c5140..09fa97da 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -59,6 +59,7 @@ class ErrorBarItem(GraphicsObject): x, y = self.opts['x'], self.opts['y'] if x is None or y is None: + self.path = p return beam = self.opts['beam'] @@ -146,4 +147,4 @@ class ErrorBarItem(GraphicsObject): self.drawPath() return self.path.boundingRect() - \ No newline at end of file + From aa50296b9fe44da3e8131ed04c101ac4f32ad380 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:30:40 -0700 Subject: [PATCH 25/92] gc.collect() causes segfault on pyside2, test will pass on pyqt5 bindings (did not test pyqt4 or pyside1) --- pyqtgraph/tests/test_qt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index bfb98631..c86cd500 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -10,7 +10,6 @@ def test_isQObjectAlive(): o2 = pg.QtCore.QObject() o2.setParent(o1) del o1 - gc.collect() assert not pg.Qt.isQObjectAlive(o2) @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' From d873ee6b264bcdc9897ce3206599877637dc78cf Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:31:12 -0700 Subject: [PATCH 26/92] fixes ImportError on importing pysideuic --- pyqtgraph/Qt.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 88c27e27..696d65f5 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -100,10 +100,13 @@ def _loadUiType(uiFile): how to make PyQt4 and pyside look the same... http://stackoverflow.com/a/8717832 """ - import pysideuic + + if QT_LIB == "PYSIDE": + import pysideuic + else: + import pyside2uic as pysideuic import xml.etree.ElementTree as xml - #from io import StringIO - + parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text From afb665ec992264f134607fb02cd81e6c311d67f2 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:35:26 -0700 Subject: [PATCH 27/92] make use of shiboken2 directly for isValid method --- pyqtgraph/Qt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 696d65f5..0941c3c7 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -219,8 +219,12 @@ elif QT_LIB == PYSIDE2: except ImportError as err: QtTest = FailedImport(err) - isQObjectAlive = _isQObjectAlive - + try: + import shiboken2 + isQObjectAlive = shiboken2.isValid + except ImportError: + # use approximate version + isQObjectAlive = _isQObjectAlive import PySide2 VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__ From 42fd5614d0350474093ce616dfec79bbc070ce30 Mon Sep 17 00:00:00 2001 From: dschoni Date: Tue, 21 May 2019 13:38:34 +0200 Subject: [PATCH 28/92] Fix deprecation warning of multi-dimension tuples --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 2b43b940..81463b7a 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -633,7 +633,7 @@ class ImageView(QtGui.QWidget): ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) - data = data[sl] + data = data[tuple(sl)] cax = self.axes['c'] if cax is None: From 7f93e8205f8cbe291847019c06e43e52bc2385b6 Mon Sep 17 00:00:00 2001 From: dschoni Date: Thu, 2 May 2019 17:33:00 +0200 Subject: [PATCH 29/92] Found one more instance of the same warning in functions.py --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index fe3f9910..062986c7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1380,7 +1380,7 @@ def gaussianFilter(data, sigma): # clip off extra data sl = [slice(None)] * data.ndim sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None) - filtered = filtered[sl] + filtered = filtered[tuple(sl)] return filtered + baseline From bac8080b0c130b85cfe7d875cb73c993cd31fef8 Mon Sep 17 00:00:00 2001 From: dschoni Date: Tue, 21 May 2019 14:14:10 +0200 Subject: [PATCH 30/92] Typecast Levels to be float This circumvents cases in which "levels" is a boolean array and therefore the substraction fails due to deprecation. --- pyqtgraph/functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index fe3f9910..1fa10f5c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1057,6 +1057,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): raise Exception('levels argument is required for float input types') if not isinstance(levels, np.ndarray): levels = np.array(levels) + levels = levels.astype(np.float) if levels.ndim == 1: if levels.shape[0] != 2: raise Exception('levels argument must have length 2') From 9cb351feee9524610baaf529a6558af6d754e0b6 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 22 May 2019 15:24:21 -0700 Subject: [PATCH 31/92] Implement azure ci (#865) * [skip-ci] Initial Azure-Pipelines configuration. The following configurations are tested * macOS 10.13 * ubuntu 16.04 * Windows Server 2016 Under each operating system, the following Qt bindings are tested * conda based pyqt4 * conda based pyside * conda based pyside2 (5.6) * conda based PyQt5 (5.9) * pip basedd PyQt5 (5.12) * pip based PySide2 (5.12) For each configuration, it runs `python -m pytest --cov pyqtgraph -sv` The only configuration that actually passes all tests is Ubuntu-pip-PyQt5 --- azure-pipelines.yml | 38 +++++++++ azure-test-template.yml | 183 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 azure-pipelines.yml create mode 100644 azure-test-template.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..b91f515a --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,38 @@ +############################################################################################ +# This config was rectrieved in no small part from https://github.com/slaclab/pydm +############################################################################################ + +trigger: + branches: + include: + - '*' # Build for all branches if they have a azure-pipelines.yml file. + tags: + include: + - 'v*' # Ensure that we are building for tags starting with 'v' (Official Versions) + +# Build only for PRs for master branch +pr: + autoCancel: true + branches: + include: + - master + - develop + +variables: + OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' + +jobs: + - template: azure-test-template.yml + parameters: + name: Linux + vmImage: 'Ubuntu 16.04' + + - template: azure-test-template.yml + parameters: + name: Windows + vmImage: 'vs2017-win2016' + + - template: azure-test-template.yml + parameters: + name: MacOS + vmImage: 'macOS-10.13' diff --git a/azure-test-template.yml b/azure-test-template.yml new file mode 100644 index 00000000..2f1a7ae3 --- /dev/null +++ b/azure-test-template.yml @@ -0,0 +1,183 @@ +# Azure Pipelines CI job template for PyDM Tests +# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/anaconda?view=azure-devops +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + matrix: + Python27-Qt4: + python.version: '2.7' + install.method: "conda" + qt.bindings: "pyqt=4" + Python27-PySide: + python.version: '2.7' + qt.bindings: "pyside" + install.method: "conda" + Python37-PyQt-5.9: + python.version: "3.7" + qt.bindings: "pyqt" + install.method: "conda" + Python37-PySide2-5.6: + python.version: "3.7" + qt.bindings: "pyside2" + install.method: "conda" + Python35-PyQt-5.12: + python.version: '3.5' + qt.bindings: "PyQt5" + install.method: "pip" + Python35-PySide2-5.12: + python.version: "3.5" + qt.bindings: "PySide2" + install.method: "pip" + + steps: + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: 'Windows - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo "##vso[task.prependpath]$CONDA/bin" + sudo chown -R $USER $CONDA + displayName: 'MacOS - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Darwin' )) + + - bash: | + brew update && brew install azure-cli + brew update && brew install python3 && brew upgrade python3 + brew link --overwrite python3 + displayName: "MacOS - Intall Python3" + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Darwin' )) + + - bash: | + echo "##vso[task.prependpath]/usr/share/miniconda/bin" + displayName: 'Linux - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Linux' )) + + - bash: | + # Install & Start Windows Manager for Linux + sudo apt-get install -y xvfb libxkbcommon-x11-0 # herbstluftwm + displayName: 'Linux - Prepare OS' + condition: eq(variables['agent.os'], 'Linux' ) + + - bash: | + source $HOME/miniconda/etc/profile.d/conda.sh + hash -r + conda config --set always_yes yes --set auto_update_conda no + conda config --add channels conda-forge + conda create -n test_env --quiet python=$(python.version) + displayName: 'Conda Setup Test Environment' + condition: eq(variables['install.method'], 'conda' ) + + - script: | + call activate test_env + conda install --quiet $(qt.bindings) + conda install --quiet numpy scipy pyopengl pytest flake8 six coverage + pip install pytest-azurepipelines pytest-xdist pytest-cov + displayName: Conda Install Dependencies - Windows + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + source activate test_env + conda install --quiet $(qt.bindings) + conda install --quiet numpy scipy pyopengl pytest flake8 six coverage + pip install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb + displayName: Conda Install Dependencies - MacOS+Linux + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: | + pip3 install setuptools wheel + pip3 install $(qt.bindings) + pip3 install numpy scipy pyopengl pytest flake8 six coverage + pip3 install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb + displayName: "Pip - Install Dependencies" + condition: eq(variables['install.method'], 'pip' ) + + - bash: | + source activate test_env + echo python location: `which python3` + echo python version: `python3 --version` + echo pytest location: `which pytest` + echo installed packages + conda list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - Conda/MacOS+Linux' + continueOnError: false + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - script: | + call activate test_env + echo python location + where python + echo python version + python --version + echo pytest location + where pytest + echo installed packages + conda list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - Conda/Windows' + continueOnError: false + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo python location: `which python3` + echo python version: `python3 --version` + echo pytest location: `which pytest` + echo installed packages + pip3 list + echo pyqtgraph system info + python3 -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - System/MacOS+Linux' + continueOnError: false + condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo python location: `where python` + echo python version: `python --version` + echo pytest location: `where pytest` + echo installed packages + python -m pip list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - System/Windows' + continueOnError: false + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: python3 -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Pip/MacOS+Linux' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Pip/Windows' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + source activate test_env + pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Conda/MacOS+Linux' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - script: | + call activate test_env + python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Conda/Windows' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) From cf3c2948993afe05ba42a1d5de90d2dbd62a812c Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Thu, 23 May 2019 00:44:54 +0200 Subject: [PATCH 32/92] Fix Travis CI on 'develop' branch (#877) * Removed unused code There is no reason to keep old, unused code in a git repository * Removed system_site_packages from Travis CI system_site_packages are opposed to the used conda installation. They are both unnecessary and lead to Travis build errors. --- .travis.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index acfde8ec..5a8dcf5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,9 @@ sudo: false notifications: email: false -virtualenv: - system_site_packages: true - - 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=pyqt4 TEST=standard # 2.6 support ended + # Note that the python 2.6 support ended. - PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyside TEST=standard - PYTHON=3.5 QT=pyqt5 TEST=standard @@ -68,11 +62,6 @@ install: - pip install pytest-xdist # multi-thread py.test - pip install pytest-cov # add coverage stats - # required for example testing on python 2.6 - - if [ "${PYTHON}" == "2.6" ]; then - pip install importlib; - fi; - # Debugging helpers - uname -a - cat /etc/issue From 309f89d413f05ed0fe457947503dc023f26106c8 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 22 May 2019 22:07:30 -0700 Subject: [PATCH 33/92] Create tox configuration, update README accordingly. --- README.md | 34 +++++++++++++++++++++++----------- tox.ini | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 tox.ini diff --git a/README.md b/README.md index a8742066..123949d5 100644 --- a/README.md +++ b/README.md @@ -19,28 +19,28 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for Requirements ------------ - * PyQt 4.7+, PySide, PyQt5, or PySide2 - * python 2.7, or 3.x - * NumPy - * For 3D graphics: pyopengl and qt-opengl - * Known to run on Windows, Linux, and Mac. +* PyQt 4.7+, PySide, PyQt5, or PySide2 +* python 2.7, or 3.x +* NumPy +* For 3D graphics: pyopengl and qt-opengl +* Known to run on Windows, Linux, and Mac. Support ------- - * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) - * Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) +* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) +* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) Installation Methods -------------------- -* From pypi: - - Last released version: `pip install pyqtgraph` - - Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` +* From PyPI: + * Last released version: `pip install pyqtgraph` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project. + anywhere that is importable from your project. * For installation packages, see the website (pyqtgraph.org) Documentation @@ -50,3 +50,15 @@ The easiest way to learn pyqtgraph is to browse through the examples; run `pytho The official documentation lives at http://pyqtgraph.org/documentation +Testing +------- + +To test the pyqtgraph library, clone the repository, and run `pytest pyqtgraph`. For more thurough testing, you can use `tox`, however the [tox-conda](https://github.com/tox-dev/tox-conda) plugin is required. Running `tox` on its own will run `pytest pyqtgraph -vv` on it's own, however if you want to run a specific test, you can run `tox -- pyqtgraph/exporters/tests/test_svg::test_plotscene` for example. + +Dependencies include: + +* pytest +* pytest-cov +* pytest-xdist +* tox +* tox-conda \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..5a86b387 --- /dev/null +++ b/tox.ini @@ -0,0 +1,46 @@ +[tox] +envlist = + ; qt 5.12.x + py{27,37}-pyside2-pip + ; qt 5.12.x + py{35,37}-pyqt5-pip + ; qt 5.9.7 + py{27,37}-pyqt5-conda + ; qt 5.6.2 + py35-pyqt5-conda + ; qt 5.6.2 + py{27,35,37}-pyside2-conda + ; pyqt 4.11.4 / qt 4.8.7 + py{27,36}-pyqt4-conda + ; pyside 1.2.4 / qt 4.8.7 + py{27,36}-pyside-conda + +[base] +deps = + pytest + numpy + scipy + pyopengl + flake8 + six + coverage + +[testenv] +deps= + {[base]deps} + pytest-cov + pytest-xdist + pyside2-pip: pyside2 + pyqt5-pip: pyqt5 + +conda_deps= + pyside2-conda: pyside2 + pyside-conda: pyside + pyqt5-conda: pyqt + pyqt4-conda: pyqt=4 + +conda_channels= + conda-forge +commands= + python -c "import pyqtgraph as pg; pg.systemInfo()" + python -m pytest {posargs:pyqtgraph -svv} From fd134f77c6fde72df39a14820482f182e899fdc0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 17:53:42 -0700 Subject: [PATCH 34/92] Only append .fc file extension if not added in the file dialog. --- pyqtgraph/flowchart/Flowchart.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 6a486232..f28ebc3b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -525,7 +525,12 @@ class Flowchart(Node): self.restoreState(state, clear=True) self.viewBox.autoRange() self.sigFileLoaded.emit(fileName) - + + def saveFileSelected(self, fileName): + if not fileName.endswith('.fc'): + fileName += '.fc' + self.saveFile(fileName=fileName) + def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): """Save this flowchart to a .fc file """ @@ -537,11 +542,8 @@ class Flowchart(Node): self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.saveFile) + self.fileDialog.fileSelected.connect(self.saveFileSelected) return - fileName = unicode(fileName) - if not fileName.endswith('.fc'): - fileName += '.fc' fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) From ffd1624cb9b980abcced959e9c722e23e7ff04e8 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 19:02:56 -0700 Subject: [PATCH 35/92] Use defaultSuffix for smarter file extension handling. --- pyqtgraph/flowchart/Flowchart.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index f28ebc3b..ae03d4c2 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -526,11 +526,6 @@ class Flowchart(Node): self.viewBox.autoRange() self.sigFileLoaded.emit(fileName) - def saveFileSelected(self, fileName): - if not fileName.endswith('.fc'): - fileName += '.fc' - self.saveFile(fileName=fileName) - def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): """Save this flowchart to a .fc file """ @@ -540,9 +535,10 @@ class Flowchart(Node): if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + self.fileDialog.setDefaultSuffix("fc") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.saveFileSelected) + self.fileDialog.fileSelected.connect(self.saveFile) return fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) From 8420fe984acb7b23e8feb4e5d6a0e4d3da5e4eb1 Mon Sep 17 00:00:00 2001 From: HappyTreeBeard <34220817+HappyTreeBeard@users.noreply.github.com> Date: Thu, 23 May 2019 21:33:23 -0700 Subject: [PATCH 36/92] Fixed bug in unit test where temp file remained open when os.unlink was called (#832) --- pyqtgraph/exporters/tests/test_csv.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_csv.py b/pyqtgraph/exporters/tests/test_csv.py index 15c6626e..d6da033b 100644 --- a/pyqtgraph/exporters/tests/test_csv.py +++ b/pyqtgraph/exporters/tests/test_csv.py @@ -1,5 +1,5 @@ """ -SVG export test +CSV export test """ from __future__ import division, print_function, absolute_import import pyqtgraph as pg @@ -33,8 +33,9 @@ def test_CSVExporter(): ex = pg.exporters.CSVExporter(plt.plotItem) ex.export(fileName=tempfilename) - r = csv.reader(open(tempfilename, 'r')) - lines = [line for line in r] + with open(tempfilename, 'r') as csv_file: + r = csv.reader(csv_file) + lines = [line for line in r] header = lines.pop(0) assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002'] From e2b01ccf749ef8a46e2f5a3185ca9007587501b9 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 24 May 2019 06:35:01 +0200 Subject: [PATCH 37/92] FIX: Correct deletion of matplotlib exporter window object (#868) E.g. when opening the Matplotlib exporter multiple times, and one closes one instance, Python crashes. This is caused by the Matplotlib QMainWindow listening to the closeEvent and deleting the only reference of the window before it is closed properly. --- pyqtgraph/exporters/Matplotlib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 2da979b1..dedc2b87 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow): def closeEvent(self, ev): MatplotlibExporter.windows.remove(self) - - + self.deleteLater() From 95f4b00e1463cbfd72d8d45568977e192499d5fc Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 23:56:53 -0700 Subject: [PATCH 38/92] TreeWidget.topLevelItems Python 3 fix --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b0ec54c1..8c55ae2f 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -201,7 +201,7 @@ class TreeWidget(QtGui.QTreeWidget): return item def topLevelItems(self): - return map(self.topLevelItem, xrange(self.topLevelItemCount())) + return [self.topLevelItem(i) for i in range(self.topLevelItemCount())] def clear(self): items = self.topLevelItems() From 849c7cab55f7956068a64f1333a7fe1745343557 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 24 May 2019 23:28:48 -0700 Subject: [PATCH 39/92] PySide2 is also a Qt5 binding --- pyqtgraph/tests/image_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index a7552631..2f9e98f9 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -253,7 +253,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert im1.dtype == im2.dtype if pxCount == -1: - if QT_LIB == 'PyQt5': + if QT_LIB in {'PyQt5', 'PySide2'}: # Qt5 generates slightly different results; relax the tolerance # until test images are updated. pxCount = int(im1.shape[0] * im1.shape[1] * 0.01) From c69e04db2df7485b823338e482bdbc1cf6323e99 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 25 May 2019 00:21:37 -0700 Subject: [PATCH 40/92] Simpler way of extracting types from QByteArray Simpler way of extracting bytes from QByteArray --- pyqtgraph/exporters/SVGExporter.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index dcd95c2b..b0e9b1c0 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -190,12 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): ## this is taken care of in generateSvg instead. #if hasattr(item, 'setExportMode'): #item.setExportMode(False) - - if QT_LIB in ['PySide', 'PySide2']: - xmlStr = str(arr) - else: - xmlStr = bytes(arr).decode('utf-8') - doc = xml.parseString(xmlStr.encode('utf-8')) + doc = xml.parseString(arr.data()) try: ## Get top-level group for this item From deab37d533975f4eccc318dc0b5ede2dfafd2181 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 25 May 2019 16:25:49 -0700 Subject: [PATCH 41/92] Try to import from collections.abc for Python 3.3+ (#887) * Try to import from collections.abc for Python 3.3+ --- pyqtgraph/graphicsItems/ImageItem.py | 10 +++++++--- pyqtgraph/pgcollections.py | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 2ebce2c7..65e87eec 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -2,13 +2,17 @@ from __future__ import division from ..Qt import QtGui, QtCore import numpy as np -import collections from .. import functions as fn from .. import debug as debug from .GraphicsObject import GraphicsObject from ..Point import Point from .. import getConfigOption +try: + from collections.abc import Callable +except ImportError: + # fallback for python < 3.3 + from collections import Callable __all__ = ['ImageItem'] @@ -357,7 +361,7 @@ class ImageItem(GraphicsObject): # Request a lookup table if this image has only one channel if self.image.ndim == 2 or self.image.shape[2] == 1: - if isinstance(self.lut, collections.Callable): + if isinstance(self.lut, Callable): lut = self.lut(self.image) else: lut = self.lut @@ -624,7 +628,7 @@ class ImageItem(GraphicsObject): mask = self.drawMask src = dk - if isinstance(self.drawMode, collections.Callable): + if isinstance(self.drawMode, Callable): self.drawMode(dk, self.image, mask, ss, ts, ev) else: src = src[ss] diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index ac7f68fe..ef3db258 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -10,15 +10,22 @@ Includes: - ThreadsafeDict, ThreadsafeList - Self-mutexed data structures """ -import threading, sys, copy, collections -#from debug import * +import threading +import sys +import copy try: from collections import OrderedDict except ImportError: # fallback: try to use the ordereddict backport when using python 2.6 from ordereddict import OrderedDict - + +try: + from collections.abc import Sequence +except ImportError: + # fallback for python < 3.3 + from collections import Sequence + class ReverseDict(dict): """extends dict so that reverse lookups are possible by requesting the key as a list of length 1: @@ -326,7 +333,7 @@ class ProtectedDict(dict): -class ProtectedList(collections.Sequence): +class ProtectedList(Sequence): """ A class allowing read-only 'view' of a list or dict. The object can be treated like a normal list, but will never modify the original list it points to. @@ -408,7 +415,7 @@ class ProtectedList(collections.Sequence): raise Exception("This is a list. It does not poop.") -class ProtectedTuple(collections.Sequence): +class ProtectedTuple(Sequence): """ A class allowing read-only 'view' of a tuple. The object can be treated like a normal tuple, but its contents will be returned as protected objects. From 6a4e0a106f83bed0856cd7aa7732b43be099f5c9 Mon Sep 17 00:00:00 2001 From: ksunden Date: Mon, 27 May 2019 16:49:08 -0500 Subject: [PATCH 42/92] Add condition for namespace packages --- pyqtgraph/reload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index 766ec9d0..f6c630b9 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -47,7 +47,7 @@ def reloadAll(prefix=None, debug=False): continue ## Ignore if the file name does not start with prefix - if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: + if not hasattr(mod, '__file__') or mod.__file__ is None or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: continue if prefix is not None and mod.__file__[:len(prefix)] != prefix: continue From a37e8776312946e19de34bf34aed049ffae5eea3 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 28 May 2019 06:07:25 +0200 Subject: [PATCH 43/92] Add PyQt5 and PySide2 to test_example.py (#897) * Add PySide2 to test_example.py Before, example tests are skipped because no PyQt version was found --- examples/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index ae88b087..d8de370f 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -42,7 +42,7 @@ except ImportError: pass files = utils.buildFileList(utils.examples) -frontends = {Qt.PYQT4: False, Qt.PYSIDE: False} +frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): try: From e8854d69bba513a98932efd6e3e74d0d6d1e5ad7 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:41:44 -0700 Subject: [PATCH 44/92] Capture Screenshots --- pyqtgraph/tests/image_testing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2f9e98f9..c1a14c4d 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") except Exception: + if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " @@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) else: if os.getenv('TRAVIS') is not None: + saveFailedTest(image, stdImage, standardFile, upload=True) + elif os.getenv('AZURE') is not None: + standardFile = r"artifacts/" + standardFile saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -281,14 +285,13 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert corr >= minCorr -def saveFailedTest(data, expect, filename): +def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) - host = 'data.pyqtgraph.org' # concatenate data, expect, and diff into a single image ds = data.shape @@ -306,15 +309,25 @@ def saveFailedTest(data, expect, filename): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) + directory, _, _ = filename.rpartition("/") + if not os.path.isdir(directory): + os.makedirs(directory) + with open(filename + ".png", "wb") as png_file: + png_file.write(png) + print("\nImage comparison failed. Test result: %s %s Expected result: " + "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + if upload: + uploadFailedTest(filename) +def uploadFailedTest(filename): + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, 'data': base64.b64encode(png)}) conn.request('POST', '/upload.py', req) response = conn.getresponse().read() conn.close() - print("\nImage comparison failed. Test result: %s %s Expected result: " - "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + print("Uploaded to: \nhttp://%s/data/%s" % (host, filename)) if not response.startswith(b'OK'): print("WARNING: Error uploading data to %s" % host) @@ -495,7 +508,7 @@ def getTestDataRepo(): if not os.path.isdir(parentPath): os.makedirs(parentPath) - if os.getenv('TRAVIS') is not None: + if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None: # Create a shallow clone of the test-data repository (to avoid # downloading more data than is necessary) os.makedirs(dataPath) From f2aeea8964992ddedf0f5b1d92882d9c8629dc0a Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:01 -0700 Subject: [PATCH 45/92] We support pyside2 don't we? --- examples/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/__main__.py b/examples/__main__.py index 9c49bb3b..0251974a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -135,6 +135,8 @@ if __name__ == '__main__': lib = 'PyQt4' elif '--pyqt5' in args: lib = 'PyQt5' + elif '--pyside2' in args: + lib = 'PySide2' else: lib = '' From aa63c07523dbb1f1ad9e38ef80e06a5e8eb3893d Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:25 -0700 Subject: [PATCH 46/92] Show available desktop resolution --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 68f4f497..a42fb5f9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,7 +31,8 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - + desktop = app.desktop().screenGeometry() + print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) app.processEvents() def test_ViewBox(): From 560993e8c5b4bc7091b0b18c69bfe504279cf030 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 29 May 2019 10:58:33 +0200 Subject: [PATCH 47/92] Exclude selected examples from tests (such as HDF5) --- examples/test_examples.py | 2 +- examples/utils.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index d8de370f..c5997348 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -41,7 +41,7 @@ except ImportError: "pypi\n\npip install importlib\n\n") pass -files = utils.buildFileList(utils.examples) +files = utils.buildFileList(utils.tested_examples) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): diff --git a/examples/utils.py b/examples/utils.py index f7786dba..82270f4c 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -4,6 +4,7 @@ import time import os import sys import errno +import copy from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -91,6 +92,11 @@ examples = OrderedDict([ ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), ]) +not_tested = ['HDF5 big data'] + +tested_examples = copy.deepcopy(examples) +all(map(tested_examples.pop, not_tested)) + def buildFileList(examples, files=None): if files == None: From c4e295ceae9e783b23c54fe24b13b00ef45c8125 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 10:53:04 -0700 Subject: [PATCH 48/92] Use correct path seperators, pass png to upload function --- pyqtgraph/tests/image_testing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c1a14c4d..564e6d46 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -289,9 +289,9 @@ def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) - name = filename.split('/') + name = filename.split(os.path.sep) name.insert(-1, commit.strip()) - filename = '/'.join(name) + filename = os.path.sep.join(name) # concatenate data, expect, and diff into a single image ds = data.shape @@ -309,7 +309,7 @@ def saveFailedTest(data, expect, filename, upload=False): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) - directory, _, _ = filename.rpartition("/") + directory = os.path.dirname(filename) if not os.path.isdir(directory): os.makedirs(directory) with open(filename + ".png", "wb") as png_file: @@ -317,9 +317,9 @@ def saveFailedTest(data, expect, filename, upload=False): print("\nImage comparison failed. Test result: %s %s Expected result: " "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) if upload: - uploadFailedTest(filename) + uploadFailedTest(filename, png) -def uploadFailedTest(filename): +def uploadFailedTest(filename, png): host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, From 4b26519feffe03954e6ca45ace108b05c514aafa Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 13:07:08 -0700 Subject: [PATCH 49/92] Move Desktop Resolution info print statement to test.py --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 -- test.py | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index a42fb5f9..bb705c18 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,8 +31,6 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - desktop = app.desktop().screenGeometry() - print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) app.processEvents() def test_ViewBox(): diff --git a/test.py b/test.py index b07fb1cf..63656d68 100644 --- a/test.py +++ b/test.py @@ -15,10 +15,15 @@ elif '--pyqt4' in args: elif '--pyqt5' in args: args.remove('--pyqt5') import PyQt5 +elif '--pyside2' in args: + args.remove('--pyside2') + import PySide2 import pyqtgraph as pg pg.systemInfo() - +qApp = pg.mkQApp() +desktop = qApp.desktop().screenGeometry() +print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) pytest.main(args) \ No newline at end of file From b3c0bf635d2babffa53596e95a269d6ca91f7f55 Mon Sep 17 00:00:00 2001 From: Matt Liberty Date: Wed, 29 May 2019 20:02:00 -0400 Subject: [PATCH 50/92] Fixed ViewBox.updateViewRange so that transformation is updated for sigXRangeChanged and sigYRangeChanged in PySide2. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5002fa35..e85796c9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1496,14 +1496,13 @@ class ViewBox(GraphicsWidget): changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange - # emit range change signals - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - if any(changed): self._matrixNeedsUpdate = True + # emit range change signals + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() From f2426e9dd2155ffe164911bf775e8629df26394d Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:41:44 -0700 Subject: [PATCH 51/92] Capture Screenshots --- pyqtgraph/tests/image_testing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2f9e98f9..c1a14c4d 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") except Exception: + if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " @@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) else: if os.getenv('TRAVIS') is not None: + saveFailedTest(image, stdImage, standardFile, upload=True) + elif os.getenv('AZURE') is not None: + standardFile = r"artifacts/" + standardFile saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -281,14 +285,13 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert corr >= minCorr -def saveFailedTest(data, expect, filename): +def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) - host = 'data.pyqtgraph.org' # concatenate data, expect, and diff into a single image ds = data.shape @@ -306,15 +309,25 @@ def saveFailedTest(data, expect, filename): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) + directory, _, _ = filename.rpartition("/") + if not os.path.isdir(directory): + os.makedirs(directory) + with open(filename + ".png", "wb") as png_file: + png_file.write(png) + print("\nImage comparison failed. Test result: %s %s Expected result: " + "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + if upload: + uploadFailedTest(filename) +def uploadFailedTest(filename): + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, 'data': base64.b64encode(png)}) conn.request('POST', '/upload.py', req) response = conn.getresponse().read() conn.close() - print("\nImage comparison failed. Test result: %s %s Expected result: " - "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + print("Uploaded to: \nhttp://%s/data/%s" % (host, filename)) if not response.startswith(b'OK'): print("WARNING: Error uploading data to %s" % host) @@ -495,7 +508,7 @@ def getTestDataRepo(): if not os.path.isdir(parentPath): os.makedirs(parentPath) - if os.getenv('TRAVIS') is not None: + if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None: # Create a shallow clone of the test-data repository (to avoid # downloading more data than is necessary) os.makedirs(dataPath) From 2df71abfec301b8ffd3026a064e7c95cbac07302 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:01 -0700 Subject: [PATCH 52/92] We support pyside2 don't we? --- azure-test-template.yml | 259 +++++++++--------- examples/__main__.py | 2 + .../ViewBox/tests/test_ViewBox.py | 1 - pyqtgraph/tests/image_testing.py | 21 +- pyqtgraph/tests/test_display.py | 10 + pyqtgraph/util/get_resolution.py | 7 + test.py | 6 +- 7 files changed, 156 insertions(+), 150 deletions(-) create mode 100644 pyqtgraph/tests/test_display.py create mode 100644 pyqtgraph/util/get_resolution.py diff --git a/azure-test-template.yml b/azure-test-template.yml index 2f1a7ae3..f3eaac40 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -12,8 +12,8 @@ jobs: matrix: Python27-Qt4: python.version: '2.7' - install.method: "conda" qt.bindings: "pyqt=4" + install.method: "conda" Python27-PySide: python.version: '2.7' qt.bindings: "pyside" @@ -26,158 +26,145 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python35-PyQt-5.12: - python.version: '3.5' + Python37-PyQt-5.12: + python.version: '3.7' qt.bindings: "PyQt5" install.method: "pip" - Python35-PySide2-5.12: - python.version: "3.5" + Python37-PySide2-5.12: + python.version: "3.7" qt.bindings: "PySide2" install.method: "pip" steps: - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - displayName: 'Windows - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + - task: ScreenResolutionUtility@1 + inputs: + displaySettings: 'specific' + width: '1920' + height: '1080' + condition: eq(variables['agent.os'], 'Windows_NT' ) + + - task: UsePythonVersion@0 + inputs: + versionSpec: $(python.version) + condition: eq(variables['install.method'], 'pip') - bash: | - echo "##vso[task.prependpath]$CONDA/bin" - sudo chown -R $USER $CONDA - displayName: 'MacOS - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Darwin' )) - - - bash: | - brew update && brew install azure-cli - brew update && brew install python3 && brew upgrade python3 - brew link --overwrite python3 - displayName: "MacOS - Intall Python3" - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Darwin' )) - - - bash: | - echo "##vso[task.prependpath]/usr/share/miniconda/bin" - displayName: 'Linux - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Linux' )) - - - bash: | - # Install & Start Windows Manager for Linux - sudo apt-get install -y xvfb libxkbcommon-x11-0 # herbstluftwm - displayName: 'Linux - Prepare OS' - condition: eq(variables['agent.os'], 'Linux' ) - - - bash: | - source $HOME/miniconda/etc/profile.d/conda.sh - hash -r - conda config --set always_yes yes --set auto_update_conda no - conda config --add channels conda-forge - conda create -n test_env --quiet python=$(python.version) - displayName: 'Conda Setup Test Environment' + if [ $(agent.os) == 'Linux' ] + then + echo '##vso[task.prependpath]/usr/share/miniconda/bin' + elif [ $(agent.os) == 'Darwin' ] + then + echo '##vso[task.prependpath]$CONDA/bin' + sudo install -d -m 0777 /usr/local/miniconda/envs + elif [ $(agent.os) == 'Windows_NT' ] + then + echo "##vso[task.prependpath]$env:CONDA\Scripts" + else + echo 'Just what OS are you using?' + fi + displayName: 'Add Conda to $PATH' condition: eq(variables['install.method'], 'conda' ) + + - task: CondaEnvironment@0 + displayName: 'Create Conda Environment' + condition: eq(variables['install.method'], 'conda') + inputs: + environmentName: 'test-environment-$(python.version)' + packageSpecs: 'python=$(python.version)' + + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes + else + pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage + fi + pip install pytest-xdist pytest-cov pytest-faulthandler + displayName: "Install Dependencies" - - script: | - call activate test_env - conda install --quiet $(qt.bindings) - conda install --quiet numpy scipy pyopengl pytest flake8 six coverage - pip install pytest-azurepipelines pytest-xdist pytest-cov - displayName: Conda Install Dependencies - Windows - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + + pip install setuptools wheel + python setup.py bdist_wheel + pip install dist/*.whl + displayName: 'Build Wheel and Install' + + - task: CopyFiles@2 + inputs: + contents: 'dist/**' + targetFolder: $(Build.ArtifactStagingDirectory) + cleanTargetFolder: true # Optional + displayName: "Copy Distributions To Artifacts" + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Distributions' + condition: always() + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)/dist + artifactName: Distributions - bash: | - source activate test_env - conda install --quiet $(qt.bindings) - conda install --quiet numpy scipy pyopengl pytest flake8 six coverage - pip install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb - displayName: Conda Install Dependencies - MacOS+Linux - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - + sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + pip install pytest-xvfb + displayName: "Linux Virtual Display Setup" + condition: eq(variables['agent.os'], 'Linux' ) + - bash: | - pip3 install setuptools wheel - pip3 install $(qt.bindings) - pip3 install numpy scipy pyopengl pytest flake8 six coverage - pip3 install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb - displayName: "Pip - Install Dependencies" - condition: eq(variables['install.method'], 'pip' ) - - - bash: | - source activate test_env - echo python location: `which python3` - echo python version: `python3 --version` - echo pytest location: `which pytest` - echo installed packages - conda list - echo pyqtgraph system info - python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - Conda/MacOS+Linux' - continueOnError: false - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - - - script: | - call activate test_env - echo python location - where python - echo python version - python --version - echo pytest location - where pytest - echo installed packages - conda list - echo pyqtgraph system info - python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - Conda/Windows' - continueOnError: false - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) - - - bash: | - echo python location: `which python3` - echo python version: `python3 --version` - echo pytest location: `which pytest` - echo installed packages - pip3 list - echo pyqtgraph system info - python3 -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - System/MacOS+Linux' - continueOnError: false - condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) - - - bash: | - echo python location: `where python` + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + echo python location: `which python` echo python version: `python --version` - echo pytest location: `where pytest` + echo pytest location: `which pytest` echo installed packages - python -m pip list + pip list echo pyqtgraph system info python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - System/Windows' + displayName: 'Debug Info' continueOnError: false - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) - - bash: python3 -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Pip/MacOS+Linux' - continueOnError: false + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + mkdir -p "$SCREENSHOT_DIR" + # echo "If Screenshots are generated, they may be downloaded from:" + # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" + python -m pytest -sv \ + --junitxml=junit/test-results.xml \ + --cov pyqtgraph --cov-report=xml --cov-report=html + displayName: 'Unit tests' env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) - - - bash: python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Pip/Windows' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) - - - bash: | - source activate test_env - pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Conda/MacOS+Linux' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - - - script: | - call activate test_env - python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Conda/Windows' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + AZURE: 1 + SCREENSHOT_DIR: $(Build.ArtifactStagingDirectory)/screenshots + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Screenshots' + condition: failed() + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)/screenshots + artifactName: Screenshots + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFiles: '**/test-*.xml' + testRunTitle: 'Test Results for $(agent.os) - $(python.version) - $(qt.bindings) - $(install.method)' + publishRunAttachments: true + + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' \ No newline at end of file diff --git a/examples/__main__.py b/examples/__main__.py index 9c49bb3b..0251974a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -135,6 +135,8 @@ if __name__ == '__main__': lib = 'PyQt4' elif '--pyqt5' in args: lib = 'PyQt5' + elif '--pyside2' in args: + lib = 'PySide2' else: lib = '' diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 68f4f497..bb705c18 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,7 +31,6 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - app.processEvents() def test_ViewBox(): diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c1a14c4d..cfb62bb9 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -213,7 +213,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('TRAVIS') is not None: saveFailedTest(image, stdImage, standardFile, upload=True) elif os.getenv('AZURE') is not None: - standardFile = r"artifacts/" + standardFile + standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile) saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -288,11 +288,6 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ - commit = runSubprocess(['git', 'rev-parse', 'HEAD']) - name = filename.split('/') - name.insert(-1, commit.strip()) - filename = '/'.join(name) - # concatenate data, expect, and diff into a single image ds = data.shape es = expect.shape @@ -309,7 +304,7 @@ def saveFailedTest(data, expect, filename, upload=False): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) - directory, _, _ = filename.rpartition("/") + directory = os.path.dirname(filename) if not os.path.isdir(directory): os.makedirs(directory) with open(filename + ".png", "wb") as png_file: @@ -317,9 +312,15 @@ def saveFailedTest(data, expect, filename, upload=False): print("\nImage comparison failed. Test result: %s %s Expected result: " "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) if upload: - uploadFailedTest(filename) - -def uploadFailedTest(filename): + uploadFailedTest(filename, png) + + +def uploadFailedTest(filename, png): + commit = runSubprocess(['git', 'rev-parse', 'HEAD']) + name = filename.split(os.path.sep) + name.insert(-1, commit.strip()) + filename = os.path.sep.join(name) + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, diff --git a/pyqtgraph/tests/test_display.py b/pyqtgraph/tests/test_display.py new file mode 100644 index 00000000..951a10f9 --- /dev/null +++ b/pyqtgraph/tests/test_display.py @@ -0,0 +1,10 @@ +from .. import mkQApp + +qApp = mkQApp() + + +def test_displayResolution(): + desktop = qApp.desktop().screenGeometry() + width, height = desktop.width(), desktop.height() + print("\n\nDisplay Resolution Logged as {}x{}\n\n".format(width, height)) + assert height > 0 and width > 0 diff --git a/pyqtgraph/util/get_resolution.py b/pyqtgraph/util/get_resolution.py new file mode 100644 index 00000000..3558a81c --- /dev/null +++ b/pyqtgraph/util/get_resolution.py @@ -0,0 +1,7 @@ +from .. import mkQApp + + +def getResolution(): + qApp = mkQApp() + desktop = qApp.desktop().screenGeometry() + return (desktop.width(), desktop.height()) diff --git a/test.py b/test.py index b07fb1cf..d2aeff5c 100644 --- a/test.py +++ b/test.py @@ -15,10 +15,10 @@ elif '--pyqt4' in args: elif '--pyqt5' in args: args.remove('--pyqt5') import PyQt5 +elif '--pyside2' in args: + args.remove('--pyside2') + import PySide2 import pyqtgraph as pg pg.systemInfo() - pytest.main(args) - - \ No newline at end of file From 1616e99b3a5fc2686eadaf9f1b9ad16527555dd2 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 16:20:32 -0700 Subject: [PATCH 53/92] Fix docstring warning --- pyqtgraph/graphicsItems/ROI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index a710f808..fa2bcf5f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1553,7 +1553,7 @@ class RectROI(ROI): self.addScaleHandle([0.5, 1], [0.5, center[1]]) class LineROI(ROI): - """ + r""" Rectangular ROI subclass with scale-rotate handles on either side. This allows the ROI to be positioned as if moving the ends of a line segment. A third handle controls the width of the ROI orthogonal to its "line" axis. From 153d78711bbbaa662f460687f19338bb232954ea Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 30 May 2019 14:13:58 -0700 Subject: [PATCH 54/92] Implement Fault Handler when test takes 60 seconds --- azure-test-template.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index f3eaac40..09ba4757 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -143,7 +143,8 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" python -m pytest -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html + --cov pyqtgraph --cov-report=xml --cov-report=html \ + --faulthandler-timeout=60 displayName: 'Unit tests' env: AZURE: 1 From 191ce16e8d9e9b8b0d9d5076b9a09557e4315ba0 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 31 May 2019 17:11:22 -0700 Subject: [PATCH 55/92] Add pytest config file specifying colordepth when using pytest-xvfb --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..1f133c35 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] + +xvfb_colordepth = 24 From c52382c3b982205588cd903f72c2fddd45658701 Mon Sep 17 00:00:00 2001 From: Matt Liberty Date: Sat, 1 Jun 2019 16:28:23 -0400 Subject: [PATCH 56/92] Moved emits after all method state updates since PySide2 immediately executes signals. Pull request #907 addressed a specific case where a signal was emitted before a state update. If an application's slot then calls back into the instance, the instance was in an inconsistent state. This commit audits and fixes similar issues throughout the pyqtgraph library. This commit fixes several latent issues: * SignalProxy: flush -> sigDelayed -> signalReceived would have incorrectly resulted in timer.stop(). * ViewBox: resizeEvent -> sigStateChange -> background state * ViewBox: setRange -> sigStateChange -> autoranging not updated correctly * ViewBox: updateMatrix -> sigTransformChanged -> any _matrixNeedsUpdate = True -> ignored * Parameter: Child may have missed state tree messages on insert or received extra on remove * GraphicsView: updateMatrix -> sigDeviceRangeChanged/sigDeviceTransformChange -> before propagated to locked viewports. --- pyqtgraph/SignalProxy.py | 6 ++-- pyqtgraph/dockarea/Dock.py | 2 +- pyqtgraph/flowchart/Flowchart.py | 2 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 37 +++++++++++---------- pyqtgraph/parametertree/Parameter.py | 4 +-- pyqtgraph/widgets/ColorButton.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 7 ++-- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index d36282fa..7463dfc3 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject): """If there is a signal queued up, send it now.""" if self.args is None or self.block: return False - #self.emit(self.signal, *self.args) - self.sigDelayed.emit(self.args) - self.args = None + args, self.args = self.args, None self.timer.stop() self.lastFlushTime = time() + #self.emit(self.signal, *self.args) + self.sigDelayed.emit(args) return True def disconnect(self): diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 1d946062..ddeb0c4a 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -346,9 +346,9 @@ class DockLabel(VerticalLabel): ev.accept() def mouseReleaseEvent(self, ev): + ev.accept() if not self.startedDrag: self.sigClicked.emit(self, ev) - ev.accept() def mouseDoubleClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index ae03d4c2..5aeeac38 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -503,8 +503,8 @@ class Flowchart(Node): finally: self.blockSignals(False) - self.sigChartLoaded.emit() self.outputChanged() + self.sigChartLoaded.emit() self.sigStateChanged.emit() def loadFile(self, fileName=None, startDir=None): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 7272aef3..687c2e3f 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -205,8 +205,8 @@ class HistogramLUTItem(GraphicsWidget): def regionChanging(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) - self.sigLevelsChanged.emit(self) self.update() + self.sigLevelsChanged.emit(self) def imageChanged(self, autoLevel=False, autoRange=False): if self.imageItem() is None: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index fa2bcf5f..48f30880 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -711,10 +711,10 @@ class ROI(GraphicsObject): if hover: self.setMouseHover(True) - self.sigHoverEvent.emit(self) ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion. ev.acceptClicks(QtCore.Qt.RightButton) ev.acceptClicks(QtCore.Qt.MidButton) + self.sigHoverEvent.emit(self) else: self.setMouseHover(False) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 89bb5b98..67fafd83 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -834,8 +834,8 @@ class ScatterPlotItem(GraphicsObject): pts = self.pointsAt(ev.pos()) if len(pts) > 0: self.ptsClicked = pts - self.sigClicked.emit(self, self.ptsClicked) ev.accept() + self.sigClicked.emit(self, self.ptsClicked) else: #print "no spots" ev.ignore() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e85796c9..b874a3c4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -427,8 +427,8 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() self.updateViewRange() self._matrixNeedsUpdate = True - self.sigStateChanged.emit(self) self.background.setRect(self.rect()) + self.sigStateChanged.emit(self) self.sigResized.emit(self) def viewRange(self): @@ -561,18 +561,18 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): - self.sigStateChanged.emit(self) - # Update target rect for debugging if self.target.isVisible(): self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - # If ortho axes have auto-visible-only, update them now - # Note that aspect ratio constraints and auto-visible probably do not work together.. - if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): - self._autoRangeNeedsUpdate = True - elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): - self._autoRangeNeedsUpdate = True + # If ortho axes have auto-visible-only, update them now + # Note that aspect ratio constraints and auto-visible probably do not work together.. + if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): + self._autoRangeNeedsUpdate = True + elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): + self._autoRangeNeedsUpdate = True + + self.sigStateChanged.emit(self) def setYRange(self, min, max, padding=None, update=True): """ @@ -1156,8 +1156,8 @@ class ViewBox(GraphicsWidget): self._resetTarget() self.scaleBy(s, center) - self.sigRangeChangedManually.emit(mask) ev.accept() + self.sigRangeChangedManually.emit(mask) def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): @@ -1498,14 +1498,8 @@ class ViewBox(GraphicsWidget): if any(changed): self._matrixNeedsUpdate = True - # emit range change signals - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: @@ -1514,6 +1508,13 @@ class ViewBox(GraphicsWidget): if link is not None: link.linkedViewChanged(self, ax) + # emit range change signals + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + self.sigRangeChanged.emit(self, self.state['viewRange']) + def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return @@ -1541,9 +1542,9 @@ class ViewBox(GraphicsWidget): m.translate(-st[0], -st[1]) self.childGroup.setTransform(m) + self._matrixNeedsUpdate = False self.sigTransformChanged.emit(self) ## segfaults here: 1 - self._matrixNeedsUpdate = False def paint(self, p, opt, widget): self.checkSceneChange() diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index df6b1492..654a33db 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -559,8 +559,8 @@ class Parameter(QtCore.QObject): self.childs.insert(pos, child) child.parentChanged(self) - self.sigChildAdded.emit(self, child, pos) child.sigTreeStateChanged.connect(self.treeStateChanged) + self.sigChildAdded.emit(self, child, pos) return child def removeChild(self, child): @@ -571,11 +571,11 @@ class Parameter(QtCore.QObject): del self.names[name] self.childs.pop(self.childs.index(child)) child.parentChanged(None) - self.sigChildRemoved.emit(self, child) try: child.sigTreeStateChanged.disconnect(self.treeStateChanged) except (TypeError, RuntimeError): ## already disconnected pass + self.sigChildRemoved.emit(self, child) def clearChildren(self): """Remove all child parameters.""" diff --git a/pyqtgraph/widgets/ColorButton.py b/pyqtgraph/widgets/ColorButton.py index a0bb0c8e..43dd16f6 100644 --- a/pyqtgraph/widgets/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -50,11 +50,11 @@ class ColorButton(QtGui.QPushButton): def setColor(self, color, finished=True): """Sets the button's color and emits both sigColorChanged and sigColorChanging.""" self._color = functions.mkColor(color) + self.update() if finished: self.sigColorChanged.emit(self) else: self.sigColorChanging.emit(self) - self.update() def selectColor(self): self.origColor = self.color() diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index b81eab9d..7b8c5986 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -227,12 +227,12 @@ class GraphicsView(QtGui.QGraphicsView): else: self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) - self.sigDeviceRangeChanged.emit(self, self.range) - self.sigDeviceTransformChanged.emit(self) - if propagate: for v in self.lockedViewports: v.setXRange(self.range, padding=0) + + self.sigDeviceRangeChanged.emit(self, self.range) + self.sigDeviceTransformChanged.emit(self) def viewRect(self): """Return the boundaries of the view in scene coordinates""" @@ -262,7 +262,6 @@ class GraphicsView(QtGui.QGraphicsView): h = self.range.height() / scale[1] self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) - self.updateMatrix() self.sigScaleChanged.emit(self) From 5ff409ba4b2dacc041ea7f9a7a0a006f52e25ee4 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 1 Jun 2019 22:18:39 -0700 Subject: [PATCH 57/92] Move example test code such that pytest is required --- examples/__main__.py | 29 +--------- examples/test_examples.py | 119 ++++++++++++++++++++++++++++++++------ examples/utils.py | 81 -------------------------- 3 files changed, 104 insertions(+), 125 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 0251974a..ffc38ff7 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -9,8 +9,8 @@ import subprocess from pyqtgraph.python2_3 import basestring from pyqtgraph.Qt import QtGui, QT_LIB +from .utils import buildFileList, path, examples -from .utils import buildFileList, testFile, path, examples if QT_LIB == 'PySide': from .exampleLoaderTemplate_pyside import Ui_Form @@ -117,32 +117,7 @@ class ExampleLoader(QtGui.QMainWindow): def run(): app = QtGui.QApplication([]) loader = ExampleLoader() - app.exec_() if __name__ == '__main__': - - args = sys.argv[1:] - - if '--test' in args: - # get rid of orphaned cache files first - pg.renamePyc(path) - - files = buildFileList(examples) - if '--pyside' in args: - lib = 'PySide' - elif '--pyqt' in args or '--pyqt4' in args: - lib = 'PyQt4' - elif '--pyqt5' in args: - lib = 'PyQt5' - elif '--pyside2' in args: - lib = 'PySide2' - else: - lib = '' - - exe = sys.executable - print("Running tests:", lib, sys.executable) - for f in files: - testFile(f[0], f[1], exe, lib) - else: - run() + run() diff --git a/examples/test_examples.py b/examples/test_examples.py index c5997348..81de8235 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,9 +1,87 @@ from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils +import errno +import importlib import itertools +import pkgutil import pytest import os, sys +import subprocess +import time + + +path = os.path.abspath(os.path.dirname(__file__)) + + +def runExampleFile(name, f, exe, lib, graphicsSystem=None): + global path + fn = os.path.join(path,f) + os.chdir(path) + sys.stdout.write("{} ".format(name)) + sys.stdout.flush() + import1 = "import %s" % lib if lib != '' else '' + import2 = os.path.splitext(os.path.split(fn)[1])[0] + graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + code = """ +try: + %s + import initExample + import pyqtgraph as pg + %s + import %s + import sys + print("test complete") + sys.stdout.flush() + import time + while True: ## run a little event loop + pg.QtGui.QApplication.processEvents() + time.sleep(0.01) +except: + print("test failed") + raise + +""" % (import1, graphicsSystem, import2) + if sys.platform.startswith('win'): + process = subprocess.Popen([exe], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + else: + process = subprocess.Popen(['exec %s -i' % (exe)], + shell=True, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + process.stdin.close() ##? + output = '' + fail = False + while True: + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise + output += c + + if output.endswith('test complete'): + break + if output.endswith('test failed'): + fail = True + break + time.sleep(1) + process.kill() + #res = process.communicate() + res = (process.stdout.read(), process.stderr.read()) + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + print(res[0].decode()) + print(res[1].decode()) + return False + return True # printing on travis ci frequently leads to "interrupted system call" errors. @@ -32,16 +110,7 @@ if os.getenv('TRAVIS') is not None: print("Installed wrapper for flaky print.") -# apparently importlib does not exist in python 2.6... -try: - import importlib -except ImportError: - # we are on python 2.6 - print("If you want to test the examples, please install importlib from " - "pypi\n\npip install importlib\n\n") - pass - -files = utils.buildFileList(utils.tested_examples) +files = utils.buildFileList(utils.examples) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): @@ -50,16 +119,32 @@ for frontend in frontends.keys(): frontends[frontend] = True except ImportError: pass + except ModuleNotFoundError: + pass + +installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) + +# keep a dictionary of example files and their non-standard dependencies +specialExamples = { + "hdf5.py": ["h5py"] +} @pytest.mark.parametrize( - "frontend, f", itertools.product(sorted(list(frontends.keys())), files)) -def test_examples(frontend, f): - # Test the examples with all available front-ends - print('frontend = %s. f = %s' % (frontend, f)) - if not frontends[frontend]: - pytest.skip('%s is not installed. Skipping tests' % frontend) - utils.testFile(f[0], f[1], utils.sys.executable, frontend) + "frontend, f", + [ + pytest.param( + frontend, + f, + marks=pytest.mark.skipif(any(pkgutil.find_loader(pkg) is None for pkg in specialExamples[f[1]]), + reason="Skipping Example for Missing Dependencies") if f[1] in specialExamples.keys() else (), + ) + for frontend, f, in itertools.product(installed, files) + ], + ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] +) +def testExamples(frontend, f): + assert runExampleFile(f[0], f[1], sys.executable, frontend) if __name__ == "__main__": pytest.cmdline.main() diff --git a/examples/utils.py b/examples/utils.py index 82270f4c..494b686b 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -1,10 +1,5 @@ from __future__ import division, print_function, absolute_import -import subprocess -import time import os -import sys -import errno -import copy from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -87,16 +82,10 @@ examples = OrderedDict([ #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', 'JoystickButton.py'), ])), - ('Flowcharts', 'Flowchart.py'), ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), ]) -not_tested = ['HDF5 big data'] - -tested_examples = copy.deepcopy(examples) -all(map(tested_examples.pop, not_tested)) - def buildFileList(examples, files=None): if files == None: @@ -109,73 +98,3 @@ def buildFileList(examples, files=None): else: buildFileList(val, files) return files - -def testFile(name, f, exe, lib, graphicsSystem=None): - global path - fn = os.path.join(path,f) - #print "starting process: ", fn - os.chdir(path) - sys.stdout.write(name) - sys.stdout.flush() - - import1 = "import %s" % lib if lib != '' else '' - import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem - code = """ -try: - %s - import initExample - import pyqtgraph as pg - %s - import %s - import sys - print("test complete") - sys.stdout.flush() - import time - while True: ## run a little event loop - pg.QtGui.QApplication.processEvents() - time.sleep(0.01) -except: - print("test failed") - raise - -""" % (import1, graphicsSystem, import2) - - if sys.platform.startswith('win'): - process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() - else: - process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? - output = '' - fail = False - while True: - try: - c = process.stdout.read(1).decode() - except IOError as err: - if err.errno == errno.EINTR: - # Interrupted system call; just try again. - c = '' - else: - raise - output += c - #sys.stdout.write(c) - #sys.stdout.flush() - if output.endswith('test complete'): - break - if output.endswith('test failed'): - fail = True - break - time.sleep(1) - process.kill() - #res = process.communicate() - res = (process.stdout.read(), process.stderr.read()) - - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): - print('.' * (50-len(name)) + 'FAILED') - print(res[0].decode()) - print(res[1].decode()) - else: - print('.' * (50-len(name)) + 'passed') From d2331bde7f5e7080cee8dc3edb2f8d3a5f7f916f Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 1 Jun 2019 22:36:29 -0700 Subject: [PATCH 58/92] Removing duplicate entries --- examples/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/utils.py b/examples/utils.py index 494b686b..cf147fb5 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -15,9 +15,7 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Image Analysis', 'imageAnalysis.py'), - ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), - ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), ('Symbols', 'Symbols.py'), From 9f66b7dc6ec285885a492e18e7890cc2e2edcdb7 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 2 Jun 2019 22:06:07 -0700 Subject: [PATCH 59/92] Much better error reporting/tracepacks on examples --- examples/test_examples.py | 155 +++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 81de8235..979bbfb5 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils +from collections import namedtuple import errno import importlib import itertools @@ -13,77 +14,6 @@ import time path = os.path.abspath(os.path.dirname(__file__)) - -def runExampleFile(name, f, exe, lib, graphicsSystem=None): - global path - fn = os.path.join(path,f) - os.chdir(path) - sys.stdout.write("{} ".format(name)) - sys.stdout.flush() - import1 = "import %s" % lib if lib != '' else '' - import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem - code = """ -try: - %s - import initExample - import pyqtgraph as pg - %s - import %s - import sys - print("test complete") - sys.stdout.flush() - import time - while True: ## run a little event loop - pg.QtGui.QApplication.processEvents() - time.sleep(0.01) -except: - print("test failed") - raise - -""" % (import1, graphicsSystem, import2) - if sys.platform.startswith('win'): - process = subprocess.Popen([exe], - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - else: - process = subprocess.Popen(['exec %s -i' % (exe)], - shell=True, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? - output = '' - fail = False - while True: - try: - c = process.stdout.read(1).decode() - except IOError as err: - if err.errno == errno.EINTR: - # Interrupted system call; just try again. - c = '' - else: - raise - output += c - - if output.endswith('test complete'): - break - if output.endswith('test failed'): - fail = True - break - time.sleep(1) - process.kill() - #res = process.communicate() - res = (process.stdout.read(), process.stderr.read()) - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): - print(res[0].decode()) - print(res[1].decode()) - return False - return True - - # printing on travis ci frequently leads to "interrupted system call" errors. # as a workaround, we overwrite the built-in print function (bleh) if os.getenv('TRAVIS') is not None: @@ -124,9 +54,9 @@ for frontend in frontends.keys(): installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) -# keep a dictionary of example files and their non-standard dependencies -specialExamples = { - "hdf5.py": ["h5py"] +exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) +conditionalExampleTests = { + "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing") } @@ -136,15 +66,84 @@ specialExamples = { pytest.param( frontend, f, - marks=pytest.mark.skipif(any(pkgutil.find_loader(pkg) is None for pkg in specialExamples[f[1]]), - reason="Skipping Example for Missing Dependencies") if f[1] in specialExamples.keys() else (), + marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, + reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), ) for frontend, f, in itertools.product(installed, files) ], ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] ) -def testExamples(frontend, f): - assert runExampleFile(f[0], f[1], sys.executable, frontend) +def testExamples(frontend, f, graphicsSystem=None): + # runExampleFile(f[0], f[1], sys.executable, frontend) + + name, file = f + global path + fn = os.path.join(path,file) + os.chdir(path) + sys.stdout.write("{} ".format(name)) + sys.stdout.flush() + import1 = "import %s" % frontend if frontend != '' else '' + import2 = os.path.splitext(os.path.split(fn)[1])[0] + graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + code = """ +try: + %s + import initExample + import pyqtgraph as pg + %s + import %s + import sys + print("test complete") + sys.stdout.flush() + import time + while True: ## run a little event loop + pg.QtGui.QApplication.processEvents() + time.sleep(0.01) +except: + print("test failed") + raise + +""" % (import1, graphicsSystem, import2) + if sys.platform.startswith('win'): + process = subprocess.Popen([sys.executable], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + else: + process = subprocess.Popen(['exec %s -i' % (sys.executable)], + shell=True, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + process.stdin.close() ##? + output = '' + fail = False + while True: + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise + output += c + + if output.endswith('test complete'): + break + if output.endswith('test failed'): + fail = True + break + time.sleep(1) + process.kill() + #res = process.communicate() + res = (process.stdout.read(), process.stderr.read()) + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + print(res[0].decode()) + print(res[1].decode()) + pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) + assert True if __name__ == "__main__": pytest.cmdline.main() From be0e95ace7e05430e6b454de7c6ef035bacd15b4 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 3 Jun 2019 20:49:31 -0700 Subject: [PATCH 60/92] Incorporating requested changes --- examples/test_examples.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 979bbfb5..61d60d88 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -49,8 +49,6 @@ for frontend in frontends.keys(): frontends[frontend] = True except ImportError: pass - except ModuleNotFoundError: - pass installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) @@ -143,7 +141,6 @@ except: print(res[0].decode()) print(res[1].decode()) pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) - assert True if __name__ == "__main__": pytest.cmdline.main() From 501ad4f08238b53e999c5f8e321fbd77680edcec Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 6 Jun 2019 23:45:28 -0700 Subject: [PATCH 61/92] Only set visible when ErrorBarItem has something to draw. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 09fa97da..4dc93a56 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -23,6 +23,7 @@ class ErrorBarItem(GraphicsObject): beam=None, pen=None ) + self.setVisible(False) self.setData(**opts) def setData(self, **opts): @@ -44,6 +45,8 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ + if 'x' in opts and 'y' in opts: + self.setVisible(True) self.opts.update(opts) self.path = None self.update() From 654b76e6a360dd62c21082be8820c2020a90c049 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 6 Jun 2019 23:57:34 -0700 Subject: [PATCH 62/92] Handle setting/clearing data a little more robustly. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 4dc93a56..5e399e34 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -45,9 +45,11 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ - if 'x' in opts and 'y' in opts: - self.setVisible(True) self.opts.update(opts) + if self.opts['x'] is not None and self.opts['y'] is not None: + self.setVisible(True) + else: + self.setVisible(False) self.path = None self.update() self.prepareGeometryChange() From a2fb00633aa1ae64952eb61e97df2763fdbb966b Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 7 Jun 2019 00:00:30 -0700 Subject: [PATCH 63/92] DeMorgans the logic for better readability. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 5e399e34..5d57e3db 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -46,10 +46,10 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) - if self.opts['x'] is not None and self.opts['y'] is not None: - self.setVisible(True) - else: + if self.opts['x'] is None or self.opts['y'] is None: self.setVisible(False) + else: + self.setVisible(True) self.path = None self.update() self.prepareGeometryChange() From 1839c5ef59fd7d2f1a3671cda7c4b7e478d6a5a0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 7 Jun 2019 13:32:25 -0700 Subject: [PATCH 64/92] More concise visibility setting logic --- pyqtgraph/graphicsItems/ErrorBarItem.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 5d57e3db..b79da6f7 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -46,10 +46,7 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) - if self.opts['x'] is None or self.opts['y'] is None: - self.setVisible(False) - else: - self.setVisible(True) + self.setVisible(all(self.opts[ax] is not None for ax in ['x', 'y'])) self.path = None self.update() self.prepareGeometryChange() From 9f7a4423af2bfc74c9cfcb79f4646d89d9f484a1 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 7 Jun 2019 15:06:59 -0700 Subject: [PATCH 65/92] Fix attribute lookup reference --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b874a3c4..27fb8268 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -277,7 +277,7 @@ class ViewBox(GraphicsWidget): scene = self.scene() if scene == self._lastScene: return - if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'): + if self._lastScene is not None and hasattr(self._lastScene, 'sigPrepareForPaint'): self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint) if scene is not None and hasattr(scene, 'sigPrepareForPaint'): scene.sigPrepareForPaint.connect(self.prepareForPaint) From 0c8423461274eb5567171ebf16cec93f9c617706 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 8 Jun 2019 19:57:53 -0700 Subject: [PATCH 66/92] Add a test for ErrorBarItem --- .../graphicsItems/tests/test_ErrorBarItem.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py new file mode 100644 index 00000000..8fa38153 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -0,0 +1,39 @@ +import pytest +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +app = pg.mkQApp() + + +def test_errorbaritem_defer_data(): + plot = pg.PlotWidget() + plot.show() + + # plot some data away from the origin to set the view rect + x = np.arange(5) + 10 + curve = pg.PlotCurveItem(x=x, y=x) + plot.addItem(curve) + app.processEvents() + r_no_ebi = plot.viewRect() + + # ErrorBarItem with no data shouldn't affect the view rect + err = pg.ErrorBarItem() + plot.addItem(err) + app.processEvents() + r_empty_ebi = plot.viewRect() + + assert r_no_ebi == r_empty_ebi + + err.setData(x=x, y=x, bottom=x, top=x) + app.processEvents() + r_ebi = plot.viewRect() + + assert r_empty_ebi != r_ebi + + # unset data, ErrorBarItem disappears and view rect goes back to original + err.setData(x=None, y=None) + app.processEvents() + r_clear_ebi = plot.viewRect() + + assert r_clear_ebi == r_no_ebi From 24621959914e08c9d9dff0baffa00daa9b8bddbb Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 18:42:37 -0700 Subject: [PATCH 67/92] Call pytest directly, ignore specific warnings, fix azure template labeling --- azure-test-template.yml | 13 ++++++------- pytest.ini | 10 +++++++++- tox.ini | 17 +++++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 09ba4757..cfdb98dc 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -10,7 +10,7 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-Qt4: + Python27-PyQt4: python.version: '2.7' qt.bindings: "pyqt=4" install.method: "conda" @@ -19,11 +19,11 @@ jobs: qt.bindings: "pyside" install.method: "conda" Python37-PyQt-5.9: - python.version: "3.7" + python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python37-PySide2-5.6: - python.version: "3.7" + Python37-PySide2-5.9: + python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" Python37-PyQt-5.12: @@ -141,10 +141,9 @@ jobs: mkdir -p "$SCREENSHOT_DIR" # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" - python -m pytest -sv \ + pytest . -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html \ - --faulthandler-timeout=60 + --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 diff --git a/pytest.ini b/pytest.ini index 1f133c35..c2f39a6f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,11 @@ [pytest] - +# use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 +addopts = --faulthandler-timeout=15 +filterwarnings = + # comfortable skipping these warnings runtime warnings + # https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility + ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning + # Warnings generated from PyQt5.9 + ignore:*U.*mode is deprecated:DeprecationWarning + ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning \ No newline at end of file diff --git a/tox.ini b/tox.ini index 5a86b387..6bbb5566 100644 --- a/tox.ini +++ b/tox.ini @@ -2,19 +2,22 @@ envlist = ; qt 5.12.x py{27,37}-pyside2-pip - ; qt 5.12.x py{35,37}-pyqt5-pip + ; qt 5.9.7 py{27,37}-pyqt5-conda + py{27,37}-pyside2-conda + ; qt 5.6.2 py35-pyqt5-conda - ; qt 5.6.2 - py{27,35,37}-pyside2-conda - ; pyqt 4.11.4 / qt 4.8.7 + ; consider dropping support... + ; py35-pyside2-conda + + ; qt 4.8.7 py{27,36}-pyqt4-conda - ; pyside 1.2.4 / qt 4.8.7 py{27,36}-pyside-conda + [base] deps = pytest @@ -26,10 +29,12 @@ deps = coverage [testenv] +passenv = DISPLAY XAUTHORITY deps= {[base]deps} pytest-cov pytest-xdist + pytest-faulthandler pyside2-pip: pyside2 pyqt5-pip: pyqt5 @@ -43,4 +48,4 @@ conda_channels= conda-forge commands= python -c "import pyqtgraph as pg; pg.systemInfo()" - python -m pytest {posargs:pyqtgraph -svv} + pytest {posargs:.} From c5126dc26f786e0249b757901bc9fdea7d5de2e0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 9 Jun 2019 09:12:01 -0700 Subject: [PATCH 68/92] Update test name. Cleanup unused imports. --- pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py index 8fa38153..4ee25e45 100644 --- a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -1,12 +1,10 @@ -import pytest -from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np app = pg.mkQApp() -def test_errorbaritem_defer_data(): +def test_ErrorBarItem_defer_data(): plot = pg.PlotWidget() plot.show() From 5c44d51d6c2b0e4ae2872af2ea35deb493880754 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 21:55:32 -0700 Subject: [PATCH 69/92] remove resolution test, have display information printed during debug step --- azure-test-template.yml | 25 ++++++++++++++++--------- pyqtgraph/tests/test_display.py | 10 ---------- pyqtgraph/util/get_resolution.py | 16 ++++++++++++---- pytest.ini | 7 +++++-- 4 files changed, 33 insertions(+), 25 deletions(-) delete mode 100644 pyqtgraph/tests/test_display.py diff --git a/azure-test-template.yml b/azure-test-template.yml index cfdb98dc..6a237e99 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -10,19 +10,19 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-PyQt4: + Python27-PyQt4-4.8: python.version: '2.7' qt.bindings: "pyqt=4" install.method: "conda" - Python27-PySide: + Python27-PySide-4.8: python.version: '2.7' qt.bindings: "pyside" install.method: "conda" - Python37-PyQt-5.9: + Python36-PyQt-5.9: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python37-PySide2-5.9: + Python36-PySide2-5.9: python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" @@ -88,7 +88,6 @@ jobs: then source activate test-environment-$(python.version) fi - pip install setuptools wheel python setup.py bdist_wheel pip install dist/*.whl @@ -98,11 +97,11 @@ jobs: inputs: contents: 'dist/**' targetFolder: $(Build.ArtifactStagingDirectory) - cleanTargetFolder: true # Optional - displayName: "Copy Distributions To Artifacts" + cleanTargetFolder: true + displayName: "Copy Binary Wheel Distribution To Artifacts" - task: PublishBuildArtifacts@1 - displayName: 'Publish Distributions' + displayName: 'Publish Binary Wheel' condition: always() inputs: pathtoPublish: $(Build.ArtifactStagingDirectory)/dist @@ -130,10 +129,18 @@ jobs: pip list echo pyqtgraph system info python -c "import pyqtgraph as pg; pg.systemInfo()" + echo display information + if [ $(agent.os) == 'Linux' ] + then + export DISPLAY=:99.0 + Xvfb :99 -screen 0 1920x1080x24 & + sleep 3 + fi + python -m pyqtgraph.util.get_resolution displayName: 'Debug Info' continueOnError: false - - bash: | + - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) diff --git a/pyqtgraph/tests/test_display.py b/pyqtgraph/tests/test_display.py deleted file mode 100644 index 951a10f9..00000000 --- a/pyqtgraph/tests/test_display.py +++ /dev/null @@ -1,10 +0,0 @@ -from .. import mkQApp - -qApp = mkQApp() - - -def test_displayResolution(): - desktop = qApp.desktop().screenGeometry() - width, height = desktop.width(), desktop.height() - print("\n\nDisplay Resolution Logged as {}x{}\n\n".format(width, height)) - assert height > 0 and width > 0 diff --git a/pyqtgraph/util/get_resolution.py b/pyqtgraph/util/get_resolution.py index 3558a81c..79e17170 100644 --- a/pyqtgraph/util/get_resolution.py +++ b/pyqtgraph/util/get_resolution.py @@ -1,7 +1,15 @@ from .. import mkQApp - -def getResolution(): +def test_screenInformation(): qApp = mkQApp() - desktop = qApp.desktop().screenGeometry() - return (desktop.width(), desktop.height()) + desktop = qApp.desktop() + resolution = desktop.screenGeometry() + availableResolution = desktop.availableGeometry() + print("Screen resolution: {}x{}".format(resolution.width(), resolution.height())) + print("Available geometry: {}x{}".format(availableResolution.width(), availableResolution.height())) + print("Number of Screens: {}".format(desktop.screenCount())) + return None + + +if __name__ == "__main__": + test_screenInformation() \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index c2f39a6f..7d27b7a2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,11 +1,14 @@ [pytest] +xvfb_width = 1920 +xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 addopts = --faulthandler-timeout=15 + filterwarnings = # comfortable skipping these warnings runtime warnings # https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 - ignore:*U.*mode is deprecated:DeprecationWarning - ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning \ No newline at end of file + ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning + ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning \ No newline at end of file From f05ff6fbf9331cfabb29d2048143c4ff16045dcb Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 08:07:47 -0700 Subject: [PATCH 70/92] Restore duplicate entries in examples app, but test_examples does not duplicate tests --- examples/test_examples.py | 8 ++++---- examples/utils.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 61d60d88..97809653 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -40,7 +40,7 @@ if os.getenv('TRAVIS') is not None: print("Installed wrapper for flaky print.") -files = utils.buildFileList(utils.examples) +files = sorted(set(utils.buildFileList(utils.examples))) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): @@ -50,7 +50,7 @@ for frontend in frontends.keys(): except ImportError: pass -installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) +installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { @@ -67,9 +67,9 @@ conditionalExampleTests = { marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), ) - for frontend, f, in itertools.product(installed, files) + for frontend, f, in itertools.product(installedFrontends, files) ], - ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] + ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)] ) def testExamples(frontend, f, graphicsSystem=None): # runExampleFile(f[0], f[1], sys.executable, frontend) diff --git a/examples/utils.py b/examples/utils.py index cf147fb5..494b686b 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -15,7 +15,9 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Image Analysis', 'imageAnalysis.py'), + ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), + ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), ('Symbols', 'Symbols.py'), From f359449715bba89bf39c9877bc916cb4c528b8fe Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 10 Jun 2019 22:24:53 -0700 Subject: [PATCH 71/92] README and CONTRIBUTING update --- CONTRIBUTING.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.txt | 58 ---------------------------------------- README.md | 53 +++++++++++++++++++++---------------- 3 files changed, 99 insertions(+), 81 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3ca5e0bf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to PyQtGraph + +Contributions to pyqtgraph are welcome! + +Please use the following guidelines when preparing changes: + +## Submitting Code Changes + +* The preferred method for submitting changes is by github pull request against the "develop" branch. +* Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected. +* For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort. + * Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy. + +## Documentation + +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Documentation is generated with sphinx; please check that docstring changes compile correctly + +## Style guidelines + +* PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. +* Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8. +* Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt +* Exception 2: Function docstrings use ReStructuredText tables for describing arguments: + + ```text + ============== ======================================================== + **Arguments:** + argName1 (type) Description of argument + argName2 (type) Description of argument. Longer descriptions must + be wrapped within the column guidelines defined by the + "====" header and footer. + ============== ======================================================== + ``` + + QObject subclasses that implement new signals should also describe + these in a similar table. + +## Testing Setting up a test environment + +### Dependencies + +* tox +* tox-conda +* pytest +* pytest-cov +* pytest-xdist +* pytest-faulthandler +* Optional: pytest-xvfb + +### Tox + +As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin. + +* Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%. +* To measure the test coverage, un `pytest --cov -n 4` to run the test suite with coverage on 4 cores. + +### Continous Integration + +For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations + +* Python2.7 with PyQt4 +* Python2.7 with PySide +* Python3.6 with PyQt5-5.9 +* Python3.6 with PySide2-5.9 +* Python3.7 with PyQt5-5.12 +* Python3.7 with PySide2-5.12 + +More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt deleted file mode 100644 index 5df9703f..00000000 --- a/CONTRIBUTING.txt +++ /dev/null @@ -1,58 +0,0 @@ -Contributions to pyqtgraph are welcome! - -Please use the following guidelines when preparing changes: - -* The preferred method for submitting changes is by github pull request - against the "develop" branch. - -* Pull requests should include only a focused and related set of changes. - Mixed features and unrelated changes may be rejected. - -* For major changes, it is recommended to discuss your plans on the mailing - list or in a github issue before putting in too much effort. - - * Along these lines, please note that pyqtgraph.opengl will be deprecated - soon and replaced with VisPy. - -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph - uses nose / py.test style testing, so tests should usually be included in a - tests/ directory adjacent to the relevant code. - -* Documentation is generated with sphinx; please check that docstring changes - compile correctly. - -* Style guidelines: - - * PyQtGraph prefers PEP8 for most style issues, but this is not enforced - rigorously as long as the code is clean and readable. - - * Use `python setup.py style` to see whether your code follows - the mandatory style guidelines checked by flake8. - - * Exception 1: All variable names should use camelCase rather than - underscore_separation. This is done for consistency with Qt - - * Exception 2: Function docstrings use ReStructuredText tables for - describing arguments: - - ``` - ============== ======================================================== - **Arguments:** - argName1 (type) Description of argument - argName2 (type) Description of argument. Longer descriptions must - be wrapped within the column guidelines defined by the - "====" header and footer. - ============== ======================================================== - ``` - - QObject subclasses that implement new signals should also describe - these in a similar table. - -* Setting up a test environment. - - Tests for a module should ideally cover all code in that module, - i.e., statement coverage should be at 100%. - - To measure the test coverage, install py.test, pytest-cov and pytest-xdist. - Then run 'py.test --cov -n 4' to run the test suite with coverage on 4 cores. - diff --git a/README.md b/README.md index 123949d5..e5b3a9c7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -[![Build Status](https://travis-ci.org/pyqtgraph/pyqtgraph.svg?branch=develop)](https://travis-ci.org/pyqtgraph/pyqtgraph) -[![codecov.io](http://codecov.io/github/pyqtgraph/pyqtgraph/coverage.svg?branch=develop)](http://codecov.io/github/pyqtgraph/pyqtgraph?branch=develop) + +[![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) + PyQtGraph ========= -A pure-Python graphics library for PyQt/PySide +A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2 -Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill @@ -15,15 +16,32 @@ Despite being written entirely in python, the library is fast due to its heavy leverage of numpy for number crunching, Qt's GraphicsView framework for 2D display, and OpenGL for 3D display. - Requirements ------------ -* PyQt 4.7+, PySide, PyQt5, or PySide2 +* PyQt 4.8+, PySide, PyQt5, or PySide2 * python 2.7, or 3.x -* NumPy -* For 3D graphics: pyopengl and qt-opengl -* Known to run on Windows, Linux, and Mac. +* Required + * `numpy`, `scipy` +* Optional + * `pyopengl` for 3D graphics + * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` + * `hdf5` for large hdf5 binary format support +* Known to run on Windows, Linux, and macOS. + +Qt Bindings Test Matrix +----------------------- + +Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings. + +| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 | +| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | +| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | +| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | +| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | + +* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible. Support ------- @@ -36,7 +54,9 @@ Installation Methods * From PyPI: * Last released version: `pip install pyqtgraph` - * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop` +* From conda + * Last released version: `conda install pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory @@ -49,16 +69,3 @@ Documentation The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. The official documentation lives at http://pyqtgraph.org/documentation - -Testing -------- - -To test the pyqtgraph library, clone the repository, and run `pytest pyqtgraph`. For more thurough testing, you can use `tox`, however the [tox-conda](https://github.com/tox-dev/tox-conda) plugin is required. Running `tox` on its own will run `pytest pyqtgraph -vv` on it's own, however if you want to run a specific test, you can run `tox -- pyqtgraph/exporters/tests/test_svg::test_plotscene` for example. - -Dependencies include: - -* pytest -* pytest-cov -* pytest-xdist -* tox -* tox-conda \ No newline at end of file From ed3a039d236cd0893cfb7365d32c09f22f0c7a1b Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 11 Jun 2019 23:01:24 -0700 Subject: [PATCH 72/92] Testing segfault potential fix --- azure-test-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 6a237e99..8a68317b 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -150,7 +150,7 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 From 4a592ef10e9cad8fe469d3d5b8a29d9b0ad88a37 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 17 Jun 2019 19:10:32 +0200 Subject: [PATCH 73/92] Prevent element-wise string comparison Issue #835 shows that comparing `bins`, which may be a numpy array, with a string `'auto'` leads to element-wise comparison, because the `==` operator for numpy arrays is used. With this commit, potential array and string are switched, so the `==` operator for strings is used, which does no element-wise comparison. --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 65e87eec..1758bb4d 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -488,7 +488,7 @@ class ImageItem(GraphicsObject): step = (step, step) stepData = self.image[::step[0], ::step[1]] - if bins == 'auto': + if 'auto' == bins: mn = np.nanmin(stepData) mx = np.nanmax(stepData) if mx == mn: From fa2a03b8ecb9216cd0833d853e4cf64e803c3f83 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 18 Jun 2019 20:14:51 +0200 Subject: [PATCH 74/92] Write Python representation of path to Python file Before, if the path contained escaped sequences, they would be parsed before being written to `reload_test_mod.py`, therefore when the file was parsed by the Python interpreter, the escape signs would be missing. With this commit, the Python representation is written to the file, so escaped sequences stay escaped. --- pyqtgraph/tests/test_reload.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 6adbeeb6..007e90d2 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -4,6 +4,7 @@ import pyqtgraph.reload pgpath = os.path.join(os.path.dirname(pg.__file__), '..') +pgpath_repr = repr(pgpath) # make temporary directory to write module code path = None @@ -22,7 +23,7 @@ def teardown_module(): code = """ import sys -sys.path.append('{path}') +sys.path.append({path_repr}) import pyqtgraph as pg @@ -47,7 +48,7 @@ def test_reload(): # write a module mod = os.path.join(path, 'reload_test_mod.py') print("\nRELOAD FILE:", mod) - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version1")) # import the new module import reload_test_mod @@ -63,7 +64,7 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: @@ -87,7 +88,7 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: From 04baa6eef7571ef1565674bb226603b87725763b Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Thu, 20 Jun 2019 04:37:09 +0200 Subject: [PATCH 75/92] addLine now accepts 'pos' and 'angle' parameters The issue and this solution are discussed in issue https://github.com/pyqtgraph/pyqtgraph/issues/70. --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index ae74c5b6..9703f286 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -545,9 +545,9 @@ class PlotItem(GraphicsWidget): :func:`InfiniteLine.__init__() `. Returns the item created. """ - pos = kwds.get('pos', x if x is not None else y) - angle = kwds.get('angle', 0 if x is None else 90) - line = InfiniteLine(pos, angle, **kwds) + kwds['pos'] = kwds.get('pos', x if x is not None else y) + kwds['angle'] = kwds.get('angle', 0 if x is None else 90) + line = InfiniteLine(**kwds) self.addItem(line) if z is not None: line.setZValue(z) From 9b8ef188a573f4c1f248c2040df2973e56cbce0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janez=20Dem=C5=A1ar?= Date: Thu, 20 Jun 2019 07:07:57 +0200 Subject: [PATCH 76/92] Fix incorrect clipping of horizontal axis when stopAxisAtTick is set (#932) Horizontal axis are clipeed incorrectly because the code always takes the vertical coordinate of the span even if the axis is horizontal. --- pyqtgraph/graphicsItems/AxisItem.py | 8 +++-- .../graphicsItems/tests/test_AxisItem.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_AxisItem.py diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3e358870..cc94f318 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -900,16 +900,20 @@ class AxisItem(GraphicsWidget): if self.style['stopAxisAtTick'][0] is True: - stop = max(span[0].y(), min(map(min, tickPositions))) + minTickPosition = min(map(min, tickPositions)) if axis == 0: + stop = max(span[0].y(), minTickPosition) span[0].setY(stop) else: + stop = max(span[0].x(), minTickPosition) span[0].setX(stop) if self.style['stopAxisAtTick'][1] is True: - stop = min(span[1].y(), max(map(max, tickPositions))) + maxTickPosition = max(map(max, tickPositions)) if axis == 0: + stop = min(span[1].y(), maxTickPosition) span[1].setY(stop) else: + stop = min(span[1].x(), maxTickPosition) span[1].setX(stop) axisSpec = (self.pen(), span[0], span[1]) diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py new file mode 100644 index 00000000..f076890d --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -0,0 +1,30 @@ +import pyqtgraph as pg + +app = pg.mkQApp() + +def test_AxisItem_stopAxisAtTick(monkeypatch): + def test_bottom(p, axisSpec, tickSpecs, textSpecs): + assert view.mapToView(axisSpec[1]).x() == 0.25 + assert view.mapToView(axisSpec[2]).x() == 0.75 + + def test_left(p, axisSpec, tickSpecs, textSpecs): + assert view.mapToView(axisSpec[1]).y() == 0.875 + assert view.mapToView(axisSpec[2]).y() == 0.125 + + plot = pg.PlotWidget() + view = plot.plotItem.getViewBox() + bottom = plot.getAxis("bottom") + bottom.setRange(0, 1) + bticks = [(0.25, "a"), (0.6, "b"), (0.75, "c")] + bottom.setTicks([bticks, bticks]) + bottom.setStyle(stopAxisAtTick=(True, True)) + monkeypatch.setattr(bottom, "drawPicture", test_bottom) + + left = plot.getAxis("left") + lticks = [(0.125, "a"), (0.55, "b"), (0.875, "c")] + left.setTicks([lticks, lticks]) + left.setRange(0, 1) + left.setStyle(stopAxisAtTick=(True, True)) + monkeypatch.setattr(left, "drawPicture", test_left) + + plot.show() From 2f4ac51a118a532b0650645259380e7166f53cd0 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 19 Jun 2019 22:08:54 -0700 Subject: [PATCH 77/92] Check if items having events sent to are still in the scene (#919) Check if items having events sent to are still in the scene --- pyqtgraph/GraphicsScene/GraphicsScene.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 0fca2684..01b6b808 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -263,7 +263,8 @@ class GraphicsScene(QtGui.QGraphicsScene): for item in prevItems: event.currentItem = item try: - item.hoverEvent(event) + if item.scene() is self: + item.hoverEvent(event) except: debug.printExc("Error sending hover exit event:") finally: @@ -288,7 +289,7 @@ class GraphicsScene(QtGui.QGraphicsScene): else: acceptedItem = None - if acceptedItem is not None: + if acceptedItem is not None and acceptedItem.scene() is self: #print "Drag -> pre-selected item:", acceptedItem self.dragItem = acceptedItem event.currentItem = self.dragItem @@ -435,6 +436,8 @@ class GraphicsScene(QtGui.QGraphicsScene): for item in items: if hoverable and not hasattr(item, 'hoverEvent'): continue + if item.scene() is not self: + continue shape = item.shape() # Note: default shape() returns boundingRect() if shape is None: continue From 0cc4900d7aedd853c2b1b10fe5bdf8d886751a61 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 21 Jun 2019 08:36:42 -0700 Subject: [PATCH 78/92] Skip some test examples (#937) * Skip RemoteSpeedTest.py during testing * Skip `optics_demos.py` test on PySide 1. 2.4 due to documented pyside bug --- examples/test_examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 97809653..0856b4ff 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -54,7 +54,9 @@ installedFrontends = sorted([frontend for frontend, isPresent in frontends.items exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { - "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing") + "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), + "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), + "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671") } From 5238c097d59fd57f7e819484ce0ba80af4fc7972 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 21 Jun 2019 21:54:04 +0200 Subject: [PATCH 79/92] Update Travis according to new xvfb syntax (#944) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5a8dcf5f..4ce5f228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ env: # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda #- PYTHON=3.2 QT=pyqt5 TEST=standard +services: + - xvfb before_install: - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi @@ -74,7 +76,6 @@ install: before_script: # We need to create a (fake) display on Travis, let's use a funny resolution - export DISPLAY=:99.0 - - "sh -e /etc/init.d/xvfb start" - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render # Make sure everyone uses the correct python (this is handled by conda) From 0264dd40cd286c5350138769ac0b88481b7588bb Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 01:52:11 +0200 Subject: [PATCH 80/92] Added pytest-faulthandler to Travis (#945) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4ce5f228..0da455d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,6 +63,7 @@ install: fi; - pip install pytest-xdist # multi-thread py.test - pip install pytest-cov # add coverage stats + - pip install pytest-faulthandler # activate faulthandler # Debugging helpers - uname -a From 781e129725ab66dd155162896d6967a3af52dcf0 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 06:18:12 +0200 Subject: [PATCH 81/92] Fix deprecation warning of multi-dimensional tuples (#947) --- pyqtgraph/graphicsItems/ROI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 48f30880..9ce62bd9 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1667,7 +1667,7 @@ class MultiRectROI(QtGui.QGraphicsObject): ms = min([r.shape[axes[1]] for r in rgns]) sl = [slice(None)] * rgns[0].ndim sl[axes[1]] = slice(0,ms) - rgns = [r[sl] for r in rgns] + rgns = [r[tuple(sl)] for r in rgns] #print [r.shape for r in rgns], axes return np.concatenate(rgns, axis=axes[0]) From edf2942010e8b9651ae0aab2ce351f674e02e737 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 06:19:02 +0200 Subject: [PATCH 82/92] Replaced usage of deprecated ROI classes in example (#946) --- examples/ROItypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 1a064d33..4352f888 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None): rois = [] rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) -rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) +rois.append(pg.MultiRectROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9))) rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9))) -rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) +rois.append(pg.PolyLineROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) #rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) ## Add each ROI to the scene and link its data to a plot curve with the same color From 9500f4db0194a37521f94fde50af9fcd39e1e980 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 23 Jun 2019 07:17:14 +0200 Subject: [PATCH 83/92] Allow multiline parameters in configparser (#949) * FIX: Exception.message does not exist in Python3 * FIX: Allow multiline configfile parameters * Added configparser tests * Reasonable file ending for test files --- pyqtgraph/configfile.py | 6 ++--- pyqtgraph/tests/test_configparser.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 pyqtgraph/tests/test_configparser.py diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index e7056599..275a4fdb 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -33,9 +33,8 @@ class ParseError(Exception): msg = "Error parsing string at line %d:\n" % self.lineNum else: msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum) - msg += "%s\n%s" % (self.line, self.message) + msg += "%s\n%s" % (self.line, Exception.__str__(self)) return msg - #raise Exception() def writeConfigFile(data, fname): @@ -93,13 +92,14 @@ def genString(data, indent=''): s += indent + sk + ':\n' s += genString(data[k], indent + ' ') else: - s += indent + sk + ': ' + repr(data[k]) + '\n' + s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n' return s def parseString(lines, start=0): data = OrderedDict() if isinstance(lines, basestring): + lines = lines.replace("\\\n", "") lines = lines.split('\n') lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines diff --git a/pyqtgraph/tests/test_configparser.py b/pyqtgraph/tests/test_configparser.py new file mode 100644 index 00000000..27af9ec7 --- /dev/null +++ b/pyqtgraph/tests/test_configparser.py @@ -0,0 +1,36 @@ +from pyqtgraph import configfile +import numpy as np +import tempfile, os + +def test_longArrays(): + """ + Test config saving and loading of long arrays. + """ + tmp = tempfile.mktemp(".cfg") + + arr = np.arange(20) + configfile.writeConfigFile({'arr':arr}, tmp) + config = configfile.readConfigFile(tmp) + + assert all(config['arr'] == arr) + + os.remove(tmp) + +def test_multipleParameters(): + """ + Test config saving and loading of multiple parameters. + """ + tmp = tempfile.mktemp(".cfg") + + par1 = [1,2,3] + par2 = "Test" + par3 = {'a':3,'b':'c'} + + configfile.writeConfigFile({'par1':par1, 'par2':par2, 'par3':par3}, tmp) + config = configfile.readConfigFile(tmp) + + assert config['par1'] == par1 + assert config['par2'] == par2 + assert config['par3'] == par3 + + os.remove(tmp) From 96532540943faf1d53c36a5921b8b1622853b3cb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 23 Jun 2019 09:38:48 -0700 Subject: [PATCH 84/92] Fix infinite scale in makeARGB (#955) --- pyqtgraph/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a08c995e..6f67cfff 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1094,7 +1094,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): for i in range(data.shape[-1]): minVal, maxVal = levels[i] if minVal == maxVal: - maxVal += 1e-16 + maxVal = np.nextafter(maxVal, 2*maxVal) rng = maxVal-minVal rng = 1 if rng == 0 else rng newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype) @@ -1104,7 +1104,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels if minVal != 0 or maxVal != scale: if minVal == maxVal: - maxVal += 1e-16 + maxVal = np.nextafter(maxVal, 2*maxVal) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) From e510971d71be5d8aeec5860df6ecd2f0e719b09f Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 24 Jun 2019 02:01:32 +0200 Subject: [PATCH 85/92] RotateFree handle now rotates freely (Code by alguryanow) (#952) --- pyqtgraph/graphicsItems/ROI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9ce62bd9..bb0523cf 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -928,6 +928,7 @@ class ROI(GraphicsObject): if h['type'] == 'rf': h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle + h['pos'] = self.mapFromParent(p1) elif h['type'] == 'sr': if h['center'][0] == h['pos'][0]: From dea8a86dfd7e5df4f94f088201c968632564470f Mon Sep 17 00:00:00 2001 From: Ben Mathews Date: Sun, 23 Jun 2019 18:05:11 -0600 Subject: [PATCH 86/92] Fixes https://github.com/pyqtgraph/pyqtgraph/issues/950 (#951) Moving a scale handle on a ROI object does not fire a sigRegionChangeStarted signal. This patch adds the signal emit to handleMoveStarted(). --- pyqtgraph/graphicsItems/ROI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bb0523cf..fafb5592 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -428,6 +428,7 @@ class ROI(GraphicsObject): def handleMoveStarted(self): self.preMoveState = self.getState() + self.sigRegionChangeStarted.emit(self) def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): """ From 1b6537b241ba54eed286417dcf7cea703f19051f Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 01:07:55 +0100 Subject: [PATCH 87/92] Curve fill: draw line around patch (#922) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 9b4e95ef..b864c61b 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -502,7 +502,10 @@ class PlotCurveItem(GraphicsObject): p.setPen(sp) p.drawPath(path) p.setPen(cp) - p.drawPath(path) + if self.fillPath is not None: + p.drawPath(self.fillPath) + else: + p.drawPath(path) profiler('drawPath') #print "Render hints:", int(p.renderHints()) From 3e7cace746cd11ce02144028c84921b52045ad4d Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 01:27:16 +0100 Subject: [PATCH 88/92] tickSpacing bug fix (#836) Fixed a bug where `tickSpacing()` would return `None` if `style['maxTickLevel'] < 2`, resulting in the axis not being drawn. --- pyqtgraph/graphicsItems/AxisItem.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index cc94f318..b34052ae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -643,10 +643,9 @@ class AxisItem(GraphicsWidget): maxTickCount = size / minSpacing if dif / intervals[minorIndex] <= maxTickCount: levels.append((intervals[minorIndex], 0)) - return levels - - - + + return levels + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection ### Determine major/minor tick spacings which flank the optimal spacing. #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit From 297e1d95a56ad84dd44dd9f02a7a575bffa04b31 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Mon, 24 Jun 2019 03:30:40 +0300 Subject: [PATCH 89/92] avoid double call to mkPen when creating PlotCurveItem objects (#817) * avoid double call to mkPen when creating PlotCurveItem objects * avoid unnecessary calls to mkPen in paint --- pyqtgraph/graphicsItems/PlotCurveItem.py | 205 +++++++++++------------ 1 file changed, 102 insertions(+), 103 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b864c61b..673d8334 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -4,7 +4,7 @@ try: HAVE_OPENGL = True except: HAVE_OPENGL = False - + import numpy as np from .GraphicsObject import GraphicsObject from .. import functions as fn @@ -15,51 +15,50 @@ from .. import debug __all__ = ['PlotCurveItem'] class PlotCurveItem(GraphicsObject): - - + + """ Class representing a single plot curve. Instances of this class are created automatically as part of PlotDataItem; these rarely need to be instantiated directly. - + Features: - + - Fast data update - Fill under curve - Mouse interaction - + ==================== =============================================== **Signals:** sigPlotChanged(self) Emitted when the data being plotted has changed sigClicked(self) Emitted when the curve is clicked ==================== =============================================== """ - + sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) - + def __init__(self, *args, **kargs): """ Forwards all arguments to :func:`setData `. - + Some extra arguments are accepted as well: - + ============== ======================================================= **Arguments:** parent The parent GraphicsObject (optional) - clickable If True, the item will emit sigClicked when it is + clickable If True, the item will emit sigClicked when it is clicked on. Defaults to False. ============== ======================================================= """ GraphicsObject.__init__(self, kargs.get('parent', None)) self.clear() - + ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - + self.metaData = {} self.opts = { - 'pen': fn.mkPen('w'), 'shadowPen': None, 'fillLevel': None, 'brush': None, @@ -70,21 +69,23 @@ class PlotCurveItem(GraphicsObject): 'mouseWidth': 8, # width of shape responding to mouse click 'compositionMode': None, } + if 'pen' not in kargs: + self.opts['pen'] = fn.mkPen('w') self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) - + def implements(self, interface=None): ints = ['plotData'] if interface is None: return ints return interface in ints - + def name(self): return self.opts.get('name', None) - + def setClickable(self, s, width=None): """Sets whether the item responds to mouse clicks. - + The *width* argument specifies the width in pixels orthogonal to the curve that will respond to a mouse click. """ @@ -92,41 +93,41 @@ class PlotCurveItem(GraphicsObject): if width is not None: self.opts['mouseWidth'] = width self._mouseShape = None - self._boundingRect = None - + self._boundingRect = None + def setCompositionMode(self, mode): """Change the composition mode of the item (see QPainter::CompositionMode in the Qt documentation). This is useful when overlaying multiple items. - + ============================================ ============================================================ **Most common arguments:** QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it is opaque. Otherwise, it uses the alpha channel to blend the image with the background. - QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to reflect the lightness or darkness of the background. - QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels are added together. QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. ============================================ ============================================================ """ self.opts['compositionMode'] = mode self.update() - + def getData(self): return self.xData, self.yData - + def dataBounds(self, ax, frac=1.0, orthoRange=None): ## Need this to run as fast as possible. ## check cache first: cache = self._boundsCache[ax] if cache is not None and cache[0] == (frac, orthoRange): return cache[1] - + (x, y) = self.getData() if x is None or len(x) == 0: return (None, None) - + if ax == 0: d = x d2 = y @@ -139,7 +140,7 @@ class PlotCurveItem(GraphicsObject): mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] #d2 = d2[mask] - + if len(d) == 0: return (None, None) @@ -154,7 +155,7 @@ class PlotCurveItem(GraphicsObject): if len(d) == 0: return (None, None) b = (d.min(), d.max()) - + elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: @@ -166,7 +167,7 @@ class PlotCurveItem(GraphicsObject): ## adjust for fill level if ax == 1 and self.opts['fillLevel'] is not None: b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) - + ## Add pen width only if it is non-cosmetic. pen = self.opts['pen'] spen = self.opts['shadowPen'] @@ -174,10 +175,10 @@ class PlotCurveItem(GraphicsObject): b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072) if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen: b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072) - + self._boundsCache[ax] = [(frac, orthoRange), b] return b - + def pixelPadding(self): pen = self.opts['pen'] spen = self.opts['shadowPen'] @@ -196,11 +197,11 @@ class PlotCurveItem(GraphicsObject): (ymn, ymx) = self.dataBounds(ax=1) if xmn is None or ymn is None: return QtCore.QRectF() - + px = py = 0.0 pxPad = self.pixelPadding() if pxPad > 0: - # determine length of pixel in local x, y directions + # determine length of pixel in local x, y directions px, py = self.pixelVectors() try: px = 0 if px is None else px.length() @@ -210,68 +211,68 @@ class PlotCurveItem(GraphicsObject): py = 0 if py is None else py.length() except OverflowError: py = 0 - + # return bounds expanded by pixel size px *= pxPad py *= pxPad #px += self._maxSpotWidth * 0.5 #py += self._maxSpotWidth * 0.5 self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) - + return self._boundingRect - + def viewTransformChanged(self): self.invalidateBounds() self.prepareGeometryChange() - + #def boundingRect(self): #if self._boundingRect is None: #(x, y) = self.getData() #if x is None or y is None or len(x) == 0 or len(y) == 0: #return QtCore.QRectF() - - + + #if self.opts['shadowPen'] is not None: #lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) #else: #lineWidth = (self.opts['pen'].width()+1) - - + + #pixels = self.pixelVectors() #if pixels == (None, None): #pixels = [Point(0,0), Point(0,0)] - + #xmin = x.min() #xmax = x.max() #ymin = y.min() #ymax = y.max() - + #if self.opts['fillLevel'] is not None: #ymin = min(ymin, self.opts['fillLevel']) #ymax = max(ymax, self.opts['fillLevel']) - + #xmin -= pixels[0].x() * lineWidth #xmax += pixels[0].x() * lineWidth #ymin -= abs(pixels[1].y()) * lineWidth #ymax += abs(pixels[1].y()) * lineWidth - + #self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) #return self._boundingRect - + def invalidateBounds(self): self._boundingRect = None self._boundsCache = [None, None] - + def setPen(self, *args, **kargs): """Set the pen used to draw the curve.""" self.opts['pen'] = fn.mkPen(*args, **kargs) self.invalidateBounds() self.update() - + def setShadowPen(self, *args, **kargs): """Set the shadow pen used to draw behind tyhe primary pen. - This pen must have a larger width than the primary + This pen must have a larger width than the primary pen to be visible. """ self.opts['shadowPen'] = fn.mkPen(*args, **kargs) @@ -283,7 +284,7 @@ class PlotCurveItem(GraphicsObject): self.opts['brush'] = fn.mkBrush(*args, **kargs) self.invalidateBounds() self.update() - + def setFillLevel(self, level): """Set the level filled to when filling under the curve""" self.opts['fillLevel'] = level @@ -295,11 +296,11 @@ class PlotCurveItem(GraphicsObject): """ =============== ======================================================== **Arguments:** - x, y (numpy arrays) Data to show + x, y (numpy arrays) Data to show pen Pen to use when drawing. Any single argument accepted by :func:`mkPen ` is allowed. shadowPen Pen for drawing behind the primary pen. Usually this - is used to emphasize the curve by providing a + is used to emphasize the curve by providing a high-contrast border. Any single argument accepted by :func:`mkPen ` is allowed. fillLevel (float or None) Fill the area 'under' the curve to @@ -317,18 +318,18 @@ class PlotCurveItem(GraphicsObject): to be drawn. "finite" causes segments to be omitted if they are attached to nan or inf values. For any other connectivity, specify an array of boolean values. - compositionMode See :func:`setCompositionMode + compositionMode See :func:`setCompositionMode `. =============== ======================================================== - + If non-keyword arguments are used, they will be interpreted as setData(y) for a single argument and setData(x, y) for two arguments. - - + + """ self.updateData(*args, **kargs) - + def updateData(self, *args, **kargs): profiler = debug.Profiler() @@ -340,12 +341,12 @@ class PlotCurveItem(GraphicsObject): elif len(args) == 2: kargs['x'] = args[0] kargs['y'] = args[1] - + if 'y' not in kargs or kargs['y'] is None: kargs['y'] = np.array([]) if 'x' not in kargs or kargs['x'] is None: kargs['x'] = np.arange(len(kargs['y'])) - + for k in ['x', 'y']: data = kargs[k] if isinstance(data, list): @@ -355,9 +356,9 @@ class PlotCurveItem(GraphicsObject): raise Exception("Plot data must be 1D ndarray.") if 'complex' in str(data.dtype): raise Exception("Can not plot complex data types.") - + profiler("data checks") - + #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot self.invalidateBounds() @@ -365,24 +366,24 @@ class PlotCurveItem(GraphicsObject): self.informViewBoundsChanged() self.yData = kargs['y'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray) - + profiler('copy') - + if 'stepMode' in kargs: self.opts['stepMode'] = kargs['stepMode'] - + if self.opts['stepMode'] is True: if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape)) else: if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape)) - + self.path = None self.fillPath = None self._mouseShape = None #self.xDisp = self.yDisp = None - + if 'name' in kargs: self.opts['name'] = kargs['name'] if 'connect' in kargs: @@ -397,14 +398,14 @@ class PlotCurveItem(GraphicsObject): self.setBrush(kargs['brush']) if 'antialias' in kargs: self.opts['antialias'] = kargs['antialias'] - - + + profiler('set') self.update() profiler('update') self.sigPlotChanged.emit(self) profiler('emit') - + def generatePath(self, x, y): if self.opts['stepMode']: ## each value in the x/y arrays generates 2 points. @@ -423,9 +424,9 @@ class PlotCurveItem(GraphicsObject): y = y2.reshape(y2.size)[1:-1] y[0] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel'] - + path = fn.arrayToQPath(x, y, connect=self.opts['connect']) - + return path @@ -438,7 +439,7 @@ class PlotCurveItem(GraphicsObject): self.path = self.generatePath(*self.getData()) self.fillPath = None self._mouseShape = None - + return self.path @debug.warnOnException ## raising an exception here causes crash @@ -446,27 +447,27 @@ class PlotCurveItem(GraphicsObject): profiler = debug.Profiler() if self.xData is None or len(self.xData) == 0: return - + if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): self.paintGL(p, opt, widget) return - + x = None y = None path = self.getPath() profiler('generate path') - + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) else: aa = self.opts['antialias'] - + p.setRenderHint(p.Antialiasing, aa) - + cmode = self.opts['compositionMode'] if cmode is not None: p.setCompositionMode(cmode) - + if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: if x is None: @@ -477,14 +478,14 @@ class PlotCurveItem(GraphicsObject): p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 - + profiler('generate fill path') p.fillPath(self.fillPath, self.opts['brush']) profiler('draw fill path') - - sp = fn.mkPen(self.opts['shadowPen']) - cp = fn.mkPen(self.opts['pen']) - + + sp = self.opts['shadowPen'] + cp = self.opts['pen'] + ## Copy pens and apply alpha adjustment #sp = QtGui.QPen(self.opts['shadowPen']) #cp = QtGui.QPen(self.opts['pen']) @@ -495,9 +496,7 @@ class PlotCurveItem(GraphicsObject): #c.setAlpha(c.alpha() * self.opts['alphaHint']) #pen.setColor(c) ##pen.setCosmetic(True) - - - + if sp is not None and sp.style() != QtCore.Qt.NoPen: p.setPen(sp) p.drawPath(path) @@ -507,29 +506,29 @@ class PlotCurveItem(GraphicsObject): else: p.drawPath(path) profiler('drawPath') - + #print "Render hints:", int(p.renderHints()) #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) #p.drawRect(self.boundingRect()) - + def paintGL(self, p, opt, widget): p.beginNativePainting() import OpenGL.GL as gl - + ## set clipping viewport view = self.getViewBox() if view is not None: rect = view.mapRectToItem(self, view.boundingRect()) #gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height())) - + #gl.glTranslate(-rect.x(), -rect.y(), 0) - + gl.glEnable(gl.GL_STENCIL_TEST) gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer - gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF) - gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) - + gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF) + gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) + ## draw stencil pattern gl.glStencilMask(0xFF) gl.glClear(gl.GL_STENCIL_BUFFER_BIT) @@ -541,12 +540,12 @@ class PlotCurveItem(GraphicsObject): gl.glVertex2f(rect.x()+rect.width(), rect.y()) gl.glVertex2f(rect.x(), rect.y()+rect.height()) gl.glEnd() - + gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) gl.glDepthMask(gl.GL_TRUE) gl.glStencilMask(0x00) gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF) - + try: x, y = self.getData() pos = np.empty((len(x), 2)) @@ -571,7 +570,7 @@ class PlotCurveItem(GraphicsObject): gl.glDisableClientState(gl.GL_VERTEX_ARRAY) finally: p.endNativePainting() - + def clear(self): self.xData = None ## raw values self.yData = None @@ -587,7 +586,7 @@ class PlotCurveItem(GraphicsObject): def mouseShape(self): """ Return a QPainterPath representing the clickable shape of the curve - + """ if self._mouseShape is None: view = self.getViewBox() @@ -600,14 +599,14 @@ class PlotCurveItem(GraphicsObject): mousePath = stroker.createStroke(path) self._mouseShape = self.mapFromItem(view, mousePath) return self._mouseShape - + def mouseClickEvent(self, ev): if not self.clickable or ev.button() != QtCore.Qt.LeftButton: return if self.mouseShape().contains(ev.pos()): ev.accept() self.sigClicked.emit(self) - + class ROIPlotItem(PlotCurveItem): @@ -622,7 +621,7 @@ class ROIPlotItem(PlotCurveItem): #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) roi.sigRegionChanged.connect(self.roiChangedEvent) #self.roiChangedEvent() - + def getRoiData(self): d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) if d is None: @@ -630,7 +629,7 @@ class ROIPlotItem(PlotCurveItem): while d.ndim > 1: d = d.mean(axis=1) return d - + def roiChangedEvent(self): d = self.getRoiData() self.updateData(d, self.xVals) From 0ba07300e125d3ead890aad3528c3244397fcca6 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 02:10:35 +0100 Subject: [PATCH 90/92] `_updateMaxTextSize` to reduce text size when no longer needed (#838) Currently `_updateMaxTextSize ` will increase the current space required for axis labels, if necessary, but not decrease it when the extra space is no longer needed. The proposed change will release no longer needed space again. --- pyqtgraph/graphicsItems/AxisItem.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b34052ae..4bd77a65 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -301,18 +301,16 @@ class AxisItem(GraphicsWidget): def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized - ## to accomodate. + ## to accommodate. if self.orientation in ['left', 'right']: - mx = max(self.textWidth, x) - if mx > self.textWidth or mx < self.textWidth-10: - self.textWidth = mx + if x > self.textWidth or x < self.textWidth-10: + self.textWidth = x if self.style['autoExpandTextSpace'] is True: self._updateWidth() #return True ## size has changed else: - mx = max(self.textHeight, x) - if mx > self.textHeight or mx < self.textHeight-10: - self.textHeight = mx + if x > self.textHeight or x < self.textHeight-10: + self.textHeight = x if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed From 053fca6e831de8afc4d69b516719738b80b82a35 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 23 Jun 2019 21:41:20 -0700 Subject: [PATCH 91/92] Revert "`_updateMaxTextSize` to reduce text size when no longer needed (#838)" (#957) This reverts commit 0ba07300e125d3ead890aad3528c3244397fcca6. --- pyqtgraph/graphicsItems/AxisItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 4bd77a65..b34052ae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -301,16 +301,18 @@ class AxisItem(GraphicsWidget): def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized - ## to accommodate. + ## to accomodate. if self.orientation in ['left', 'right']: - if x > self.textWidth or x < self.textWidth-10: - self.textWidth = x + mx = max(self.textWidth, x) + if mx > self.textWidth or mx < self.textWidth-10: + self.textWidth = mx if self.style['autoExpandTextSpace'] is True: self._updateWidth() #return True ## size has changed else: - if x > self.textHeight or x < self.textHeight-10: - self.textHeight = x + mx = max(self.textHeight, x) + if mx > self.textHeight or mx < self.textHeight-10: + self.textHeight = mx if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed From 7506ee3d3f8156f6c77184cceffd774dea2d216f Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 23 Jun 2019 23:03:51 -0700 Subject: [PATCH 92/92] Add mesa drivers to windows CI images and show openGL info during debug stage (#954) Add mesa drivers to Windows CI Image --- azure-test-template.yml | 27 +++++++++++++++++++++++---- pyqtgraph/opengl/glInfo.py | 4 ++-- pytest.ini | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 8a68317b..496ec10b 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -43,6 +43,23 @@ jobs: height: '1080' condition: eq(variables['agent.os'], 'Windows_NT' ) + - script: | + curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe + 7z x mesa3d-19.1.0-release-msvc.exe + cd x64 + xcopy opengl32.dll C:\windows\system32\mesadrv.dll* + xcopy opengl32.dll C:\windows\syswow64\mesadrv.dll* + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f + displayName: "Install Windows-Mesa OpenGL DLL" + condition: eq(variables['agent.os'], 'Windows_NT') + - task: UsePythonVersion@0 inputs: versionSpec: $(python.version) @@ -76,7 +93,7 @@ jobs: if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage fi @@ -114,9 +131,9 @@ jobs: source activate test-environment-$(python.version) fi pip install pytest-xvfb - displayName: "Linux Virtual Display Setup" + displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) - + - bash: | if [ $(install.method) == "conda" ] then @@ -133,10 +150,12 @@ jobs: if [ $(agent.os) == 'Linux' ] then export DISPLAY=:99.0 - Xvfb :99 -screen 0 1920x1080x24 & + Xvfb :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset & sleep 3 fi python -m pyqtgraph.util.get_resolution + echo openGL information + python -c "from pyqtgraph.opengl.glInfo import GLTest" displayName: 'Debug Info' continueOnError: false diff --git a/pyqtgraph/opengl/glInfo.py b/pyqtgraph/opengl/glInfo.py index 84346d81..0c3e758a 100644 --- a/pyqtgraph/opengl/glInfo.py +++ b/pyqtgraph/opengl/glInfo.py @@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget): def __init__(self): QtOpenGL.QGLWidget.__init__(self) self.makeCurrent() - print("GL version:" + glGetString(GL_VERSION)) + print("GL version:" + glGetString(GL_VERSION).decode("utf-8")) print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE)) print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE)) - print("Extensions: " + glGetString(GL_EXTENSIONS)) + print("Extensions: " + glGetString(GL_EXTENSIONS).decode("utf-8").replace(" ", "\n")) GLTest() diff --git a/pytest.ini b/pytest.ini index 7d27b7a2..fa664793 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,7 @@ xvfb_width = 1920 xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 +xvfb_args=-ac +extension GLX +render addopts = --faulthandler-timeout=15 filterwarnings =