Merge branch 'master' into test-polyline

This commit is contained in:
Ogi Moore 2021-06-04 21:06:52 -07:00
commit 145ab2e9e3
147 changed files with 384 additions and 727 deletions

View File

@ -40,11 +40,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout test-data
uses: actions/checkout@v2
with:
repository: pyqtgraph/test-data
path: .pyqtgraph/test-data
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
@ -69,10 +64,10 @@ jobs:
shell: cmd
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
pip install .
pip install pytest
python -m pip install --upgrade pip setuptools wheel
python -m pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
python -m pip install --use-feature=in-tree-build .
python -m pip install pytest
- name: "Install Linux VirtualDisplay"
if: runner.os == 'Linux'
run: |
@ -80,14 +75,14 @@ jobs:
sudo apt-get install -y libxkbcommon-x11-0 x11-utils
sudo apt-get install --no-install-recommends -y libyaml-dev libegl1-mesa libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
sudo apt-get install -y libopengl0
pip install pytest-xvfb
python -m pip install pytest-xvfb
- name: 'Debug Info'
run: |
echo python location: `which python`
echo python version: `python --version`
echo pytest location: `which pytest`
echo installed packages
pip list
python -m pip list
echo pyqtgraph system info
python -c "import pyqtgraph as pg; pg.systemInfo()"
shell: bash
@ -106,7 +101,7 @@ jobs:
- name: Run Tests
run: |
mkdir $SCREENSHOT_DIR
pytest pyqtgraph examples -v \
pytest tests examples -v \
--junitxml pytest.xml \
shell: bash
- name: Upload Screenshots

23
examples/RunExampleApp.py Normal file
View File

@ -0,0 +1,23 @@
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtTest
from examples.ExampleApp import ExampleLoader
"""
This file is used by test_examples.py for ensuring the Example App works.
It is not named test_ExampleApp.py as that way the Example application is
not run twice.
"""
pg.mkQApp()
def test_ExampleLoader():
loader = ExampleLoader()
QtTest.QTest.qWaitForWindowExposed(loader)
QtTest.QTest.qWait(200)
loader.close()
if __name__ == "__main__":
test_ExampleLoader()
pg.exec()

View File

@ -1,5 +1,5 @@
import sys
from PyQt4 import QtGui
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
from pyqtgraph.graphicsItems import TextItem
# For packages that require scipy, these may be needed:

View File

@ -1,5 +1,6 @@
# Build with `python setup.py build_exe`
from cx_Freeze import setup, Executable
from pathlib import Path
import shutil
from glob import glob
@ -8,12 +9,24 @@ shutil.rmtree("build", ignore_errors=True)
shutil.rmtree("dist", ignore_errors=True)
import sys
includes = ['PyQt4.QtCore', 'PyQt4.QtGui', 'sip', 'pyqtgraph.graphicsItems',
includes = ['pyqtgraph.graphicsItems',
'numpy', 'atexit']
excludes = ['cvxopt','_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger',
'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl','tables',
'Tkconstants', 'Tkinter', 'zmq','PySide','pysideuic','scipy','matplotlib']
# Workaround for making sure the templates are included in the frozen app package
include_files = []
import pyqtgraph
pg_folder = Path(pyqtgraph.__file__).parent
templates = pg_folder.rglob('*template*.py')
sources = [str(w) for w in templates]
destinations = ['lib' + w.replace(str(pg_folder.parent), '') for w in sources]
for a in zip(sources, destinations):
include_files.append(a)
print(include_files)
if sys.version[0] == '2':
# causes syntax error on py2
excludes.append('PyQt4.uic.port_v3')
@ -24,11 +37,10 @@ if sys.platform == "win32":
build_exe_options = {'excludes': excludes,
'includes':includes, 'include_msvcr':True,
'compressed':True, 'copy_dependent_files':True, 'create_shared_zip':True,
'include_in_shared_zip':True, 'optimize':2}
'optimize':1, "include_files": include_files,}
setup(name = "cx_freeze plot test",
version = "0.1",
version = "0.2",
description = "cx_freeze plot test",
options = {"build_exe": build_exe_options},
executables = [Executable("plotTest.py", base=base)])

View File

@ -56,7 +56,7 @@ def update():
p2 = pts[i+1]
v2 = p2 - p1
t = p1 - pts[0]
r = v1.angle(v2)
r = v2.angle(v1)
s = v2.length() / l1
trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r}))

View File

@ -82,7 +82,7 @@ params = [
{'name': 'Units + SI prefix', 'type': 'float', 'value': 1.2e-6, 'step': 1e-6, 'siPrefix': True, 'suffix': 'V'},
{'name': 'Limits (min=7;max=15)', 'type': 'int', 'value': 11, 'limits': (7, 15), 'default': -6},
{'name': 'Int suffix', 'type': 'int', 'value': 9, 'suffix': 'V'},
{'name': 'DEC stepping', 'type': 'float', 'value': 1.2e6, 'dec': True, 'step': 1, 'siPrefix': True, 'suffix': 'Hz'},
{'name': 'DEC stepping', 'type': 'float', 'value': 1.2e6, 'dec': True, 'step': 1, 'minStep': 1.0e-12, 'siPrefix': True, 'suffix': 'Hz'},
]},
{'name': 'Save/Restore functionality', 'type': 'group', 'children': [

View File

@ -1,11 +0,0 @@
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from examples.ExampleApp import ExampleLoader
loader = ExampleLoader()
if __name__ == '__main__':
pg.exec()

View File

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
from collections import namedtuple
from pyqtgraph import Qt
import errno
import time
import importlib
import itertools
import pytest
import os, sys
import platform
import subprocess
import time
from argparse import Namespace
if __name__ == "__main__" and (__package__ is None or __package__==''):
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -34,7 +31,7 @@ def buildFileList(examples, files=None):
path = os.path.abspath(os.path.dirname(__file__))
files = [("Example App", "test_ExampleApp.py")]
files = [("Example App", "RunExampleApp.py")]
for ex in [utils.examples, utils.others]:
files = buildFileList(ex, files)
files = sorted(set(files))
@ -143,13 +140,11 @@ conditionalExamples = {
]
)
def testExamples(frontend, f):
# 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.write(f"{name}")
sys.stdout.flush()
import1 = "import %s" % frontend if frontend != '' else ''
import2 = os.path.splitext(os.path.split(fn)[1])[0]
@ -172,24 +167,19 @@ except:
raise
""".format(import1, 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 = subprocess.Popen([sys.executable],
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True)
process.stdin.write(code)
process.stdin.close()
output = ''
fail = False
while True:
try:
c = process.stdout.read(1).decode()
c = process.stdout.read(1)
except IOError as err:
if err.errno == errno.EINTR:
# Interrupted system call; just try again.
@ -197,7 +187,6 @@ except:
else:
raise
output += c
if output.endswith('test complete'):
break
if output.endswith('test failed'):
@ -210,16 +199,25 @@ except:
if time.time() - start > 2.0 and not killed:
process.kill()
killed = True
#res = process.communicate()
res = (process.stdout.read(), process.stderr.read())
stdout, stderr = (process.stdout.read(), process.stderr.read())
process.stdout.close()
process.stderr.close()
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)
'exception' in stderr.lower() or
'error' in stderr.lower()):
if (not fail
and name == "RemoteGraphicsView"
and "pyqtgraph.multiprocess.remoteproxy.ClosedError" in stderr):
# This test can intermittently fail when the subprocess is killed
return None
print(stdout)
print(stderr)
pytest.fail(
f"{stdout}\n{stderr}\nFailed {name} Example Test Located in {file}",
pytrace=False
)
if __name__ == "__main__":
pytest.cmdline.main()

View File

@ -107,7 +107,7 @@ class Point(QtCore.QPointF):
def angle(self, a, units="degrees"):
"""
Returns the angle in degrees between this vector and the vector a.
Returns the angle in degrees from the vector a to self.
Parameters
----------
@ -120,9 +120,9 @@ class Point(QtCore.QPointF):
Returns
-------
float
The angle between the two points
The angle between two vectors
"""
rads = atan2(a.y(), a.x()) - atan2(self.y(), self.x())
rads = atan2(self.y(), self.x()) - atan2(a.y(), a.x())
if units == "radians":
return rads
return degrees(rads)

View File

@ -66,7 +66,7 @@ class SRTTransform(QtGui.QTransform):
dp3 = Point(p3-p1)
## detect flipped axes
if dp3.angle(dp2, units="radians") > 0:
if dp2.angle(dp3, units="radians") > 0:
da = 0
sy = -1.0
else:

View File

@ -12,6 +12,7 @@ as it can be converted to/from a string using repr and eval.
import re, os, sys, datetime
import numpy
from collections import OrderedDict
import tempfile
from . import units
from .python2_3 import asUnicode, basestring
from .Qt import QtCore
@ -187,11 +188,8 @@ def measureIndent(s):
while n < len(s) and s[n] == ' ':
n += 1
return n
if __name__ == '__main__':
import tempfile
cf = """
key: 'value'
key2: ##comment
@ -201,16 +199,13 @@ key2: ##comment
key22: [1,2,3]
key23: 234 #comment
"""
fn = tempfile.mktemp()
with open(fn, 'w') as tf:
tf.write(cf)
print("=== Test:===")
num = 1
for line in cf.split('\n'):
print("%02d %s" % (num, line))
num += 1
print(cf)
print("============")
data = readConfigFile(fn)
with tempfile.NamedTemporaryFile(encoding="utf-8") as tf:
tf.write(cf.encode("utf-8"))
print("=== Test:===")
for num, line in enumerate(cf.split('\n'), start=1):
print("%02d %s" % (num, line))
print(cf)
print("============")
data = readConfigFile(tf.name)
print(data)
os.remove(fn)

View File

@ -113,11 +113,10 @@ def getExc(indent=4, prefix='| ', skip=1):
def printExc(msg='', indent=4, prefix='|'):
"""Print an error message followed by an indented exception backtrace
(This function is intended to be called within except: blocks)"""
exc = getExc(indent, prefix + ' ', skip=2)
print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
print(" "*indent + prefix + '='*30 + '>>')
print(exc)
print(" "*indent + prefix + '='*30 + '<<')
exc = getExc(indent=0, prefix="", skip=2)
# print(" "*indent + prefix + '='*30 + '>>')
warnings.warn("\n".join([msg, exc]), RuntimeWarning, stacklevel=2)
# print(" "*indent + prefix + '='*30 + '<<')
def printTrace(msg='', indent=4, prefix='|'):

View File

@ -1423,10 +1423,6 @@ def ndarray_to_qimage(arr, fmt):
h, w = arr.shape[:2]
bytesPerLine = arr.strides[0]
qimg = QtGui.QImage(img_ptr, w, h, bytesPerLine, fmt)
# Note that the bindings that support ndarray directly already hold a reference
# to it. The manual reference below is only needed for those bindings that take
# in a raw pointer.
qimg.data = arr
return qimg
@ -1511,17 +1507,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
return ndarray_to_qimage(imgData, imgFormat)
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
fmt = img.format()
img_ptr = img.bits()
def qimage_to_ndarray(qimg):
img_ptr = qimg.bits()
if QT_LIB.startswith('PyQt'):
if hasattr(img_ptr, 'setsize'): # PyQt sip.voidptr
# sizeInBytes() was introduced in Qt 5.10
# however PyQt5 5.12 will fail with:
# "TypeError: QImage.sizeInBytes() is a private method"
@ -1529,14 +1518,37 @@ def imageToArray(img, copy=False, transpose=True):
# PyQt5 5.15, PySide2 5.12, PySide2 5.15
try:
# 64-bits size
nbytes = img.sizeInBytes()
nbytes = qimg.sizeInBytes()
except (TypeError, AttributeError):
# 32-bits size
nbytes = img.byteCount()
nbytes = qimg.byteCount()
img_ptr.setsize(nbytes)
arr = np.frombuffer(img_ptr, dtype=np.ubyte)
arr = arr.reshape(img.height(), img.width(), 4)
depth = qimg.depth()
if depth in (8, 24, 32):
dtype = np.uint8
nchan = depth // 8
elif depth in (16, 64):
dtype = np.uint16
nchan = depth // 16
else:
raise ValueError("Unsupported Image Type")
shape = qimg.height(), qimg.width()
if nchan != 1:
shape = shape + (nchan,)
return np.frombuffer(img_ptr, dtype=dtype).reshape(shape)
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
arr = qimage_to_ndarray(img)
fmt = img.format()
if fmt == img.Format_RGB32:
arr[...,3] = 255

View File

@ -235,7 +235,7 @@ class ImageItem(GraphicsObject):
self._processingBuffer = self._xp.empty(shape[:2] + (4,), dtype=self._xp.ubyte)
else:
self._processingBuffer = self._displayBuffer
self.qimage = fn.makeQImage(self._displayBuffer, transpose=False, copy=False)
self.qimage = None
def setImage(self, image=None, autoLevels=None, **kargs):
"""
@ -411,7 +411,7 @@ class ImageItem(GraphicsObject):
if self.image.ndim == 2 or self.image.shape[2] == 1:
self.lut = self._ensure_proper_substrate(self.lut, self._xp)
if isinstance(self.lut, Callable):
lut = self.lut(self.image)
lut = self._ensure_proper_substrate(self.lut(self.image), self._xp)
else:
lut = self.lut
else:
@ -471,6 +471,7 @@ class ImageItem(GraphicsObject):
fn.makeARGB(image, lut=lut, levels=levels, output=self._processingBuffer)
if self._xp == getCupy():
self._processingBuffer.get(out=self._displayBuffer)
self.qimage = fn.ndarray_to_qimage(self._displayBuffer, QtGui.QImage.Format.Format_ARGB32)
self._renderRequired = False
self._unrenderable = False

View File

@ -373,8 +373,8 @@ class PlotItem(GraphicsWidget):
if y is not None:
self.ctrl.yGridCheck.setChecked(y)
if alpha is not None:
v = fn.clip_scalar(alpha, 0., 1.)*self.ctrl.gridAlphaSlider.maximum()
self.ctrl.gridAlphaSlider.setValue(v)
v = fn.clip_scalar(alpha, 0, 1) * self.ctrl.gridAlphaSlider.maximum() # slider range 0 to 255
self.ctrl.gridAlphaSlider.setValue( int(v) )
def close(self):
## Most of this crap is needed to avoid PySide trouble.

View File

@ -795,17 +795,28 @@ class ROI(GraphicsObject):
self.mouseDragHandler.mouseDragEvent(ev)
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
ev.accept()
self.cancelMove()
if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled():
self.raiseContextMenu(ev)
ev.accept()
elif ev.button() in self.acceptedMouseButtons():
ev.accept()
self.sigClicked.emit(self, ev)
else:
ev.ignore()
with warnings.catch_warnings():
# warning present on pyqt5 5.12 + python 3.8
warnings.filterwarnings(
"ignore",
message=(
".*Implicit conversion to integers using __int__ is "
"deprecated, and may be removed in a future version of "
"Python."
),
category=DeprecationWarning
)
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
ev.accept()
self.cancelMove()
if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled():
self.raiseContextMenu(ev)
ev.accept()
elif ev.button() & self.acceptedMouseButtons():
ev.accept()
self.sigClicked.emit(self, ev)
else:
ev.ignore()
def _moveStarted(self):
self.isMoving = True
@ -936,7 +947,7 @@ class ROI(GraphicsObject):
return
## determine new rotation angle, constrained if necessary
ang = newState['angle'] - lp1.angle(lp0)
ang = newState['angle'] - lp0.angle(lp1)
if ang is None: ## this should never happen..
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -972,7 +983,7 @@ class ROI(GraphicsObject):
except OverflowError:
return
ang = newState['angle'] - lp1.angle(lp0)
ang = newState['angle'] - lp0.angle(lp1)
if ang is None:
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -1400,18 +1411,29 @@ class Handle(UIGraphicsItem):
self.update()
def mouseClickEvent(self, ev):
## right-click cancels drag
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
self.isMoving = False ## prevents any further motion
self.movePoint(self.startPos, finish=True)
ev.accept()
elif ev.button() & self.acceptedMouseButtons():
ev.accept()
if ev.button() == QtCore.Qt.RightButton and self.deletable:
self.raiseContextMenu(ev)
self.sigClicked.emit(self, ev)
else:
ev.ignore()
with warnings.catch_warnings():
# warning present on pyqt5 5.12 + python 3.8
warnings.filterwarnings(
"ignore",
message=(
".*Implicit conversion to integers using __int__ is "
"deprecated, and may be removed in a future version of "
"Python."
),
category=DeprecationWarning
)
## right-click cancels drag
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
self.isMoving = False ## prevents any further motion
self.movePoint(self.startPos, finish=True)
ev.accept()
elif ev.button() & self.acceptedMouseButtons():
ev.accept()
if ev.button() == QtCore.Qt.RightButton and self.deletable:
self.raiseContextMenu(ev)
self.sigClicked.emit(self, ev)
else:
ev.ignore()
def buildMenu(self):
menu = QtGui.QMenu()
@ -1663,7 +1685,7 @@ class LineROI(ROI):
pos2 = Point(pos2)
d = pos2-pos1
l = d.length()
ra = d.angle(Point(1, 0), units="radians")
ra = Point(1, 0).angle(d, units="radians")
c = Point(-width/2. * sin(ra), -width/2. * cos(ra))
pos1 = pos1 + c
@ -2358,7 +2380,7 @@ class RulerROI(LineSegmentROI):
vec = Point(h2) - Point(h1)
length = vec.length()
angle = Point(1, 0).angle(vec)
angle = vec.angle(Point(1, 0))
pvec = p2 - p1
pvecT = Point(pvec.y(), -pvec.x())

View File

@ -1330,8 +1330,6 @@ if __name__ == '__main__':
#### File I/O tests
print("\n================ File I/O Tests ===================\n")
import tempfile
tf = tempfile.mktemp()
tf = 'test.ma'
# write whole array

View File

@ -314,7 +314,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget):
center = self.opts['center']
dist = self.opts['distance']
if self.opts['rotationMethod'] == "quaternion":
pos = center - self.opts['rotation'].rotatedVector( Vector(0,0,dist) )
pos = Vector(center - self.opts['rotation'].rotatedVector(Vector(0,0,dist) ))
else:
# using 'euler' rotation method
elev = radians(self.opts['elevation'])

View File

@ -196,7 +196,7 @@ class GLMeshItem(GLGraphicsItem):
if faces is None:
glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1]))
else:
faces = faces.astype(np.uint).flatten()
faces = faces.astype(np.uint32).flatten()
glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces)
finally:
glDisableClientState(GL_NORMAL_ARRAY)

View File

@ -441,13 +441,13 @@ class Parameter(QtCore.QObject):
def valueIsDefault(self):
"""Returns True if this parameter's value is equal to the default value."""
return self.value() == self.defaultValue()
return fn.eq(self.value(), self.defaultValue())
def setLimits(self, limits):
"""Set limits on the acceptable values for this parameter.
The format of limits depends on the type of the parameter and
some parameters do not make use of limits at all."""
if 'limits' in self.opts and self.opts['limits'] == limits:
if 'limits' in self.opts and fn.eq(self.opts['limits'], limits):
return
self.opts['limits'] = limits
self.sigLimitsChanged.emit(self, limits)

View File

@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
import tempfile
import pyqtgraph as pg
import pytest
import textwrap
import time
code = """
import sys
sys.path.insert(0, '{path}')
import pyqtgraph as pg
app = pg.mkQApp()
w = pg.{classname}({args})
"""
skipmessage = ('unclear why this test is failing. skipping until someone has'
' time to fix it')
def call_with_timeout(*args, **kwargs):
"""Mimic subprocess.call with timeout for python < 3.3"""
wait_per_poll = 0.1
try:
timeout = kwargs.pop('timeout')
except KeyError:
timeout = 10
rc = None
p = subprocess.Popen(*args, **kwargs)
for i in range(int(timeout/wait_per_poll)):
rc = p.poll()
if rc is not None:
break
time.sleep(wait_per_poll)
return rc
@pytest.mark.skipif(True, reason=skipmessage)
def test_exit_crash():
# For each Widget subclass, run a simple python script that creates an
# instance and then shuts down. The intent is to check for segmentation
# faults when each script exits.
tmp = tempfile.mktemp(".py")
path = os.path.dirname(pg.__file__)
initArgs = {
'CheckTable': "[]",
'ProgressDialog': '"msg"',
'VerticalLabel': '"msg"',
}
for name in dir(pg):
obj = getattr(pg, name)
if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget):
continue
print(name)
argstr = initArgs.get(name, "")
with open(tmp, 'w') as f:
f.write(code.format(path=path, classname=name, args=argstr))
proc = subprocess.Popen([sys.executable, tmp])
assert proc.wait() == 0
os.remove(tmp)
@pytest.mark.skipif(pg.Qt.QtVersion.startswith("5.9"), reason="Functionality not well supported, failing only on this config")
def test_pg_exit():
# test the pg.exit() function
code = textwrap.dedent("""
import pyqtgraph as pg
app = pg.mkQApp()
pg.plot()
pg.exit()
""")
rc = call_with_timeout([sys.executable, '-c', code], timeout=5, shell=False)
assert rc == 0

View File

@ -223,6 +223,10 @@ class SpinBox(QtGui.QAbstractSpinBox):
if 'format' not in opts:
self.opts['format'] = asUnicode("{value:d}{suffixGap}{suffix}")
if self.opts['dec']:
if self.opts.get('minStep') is None:
self.opts['minStep'] = self.opts['step']
if 'delay' in opts:
self.proxy.setDelay(opts['delay'])

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
import warnings
__all__ = ['VerticalLabel']
#class VerticalLabel(QtGui.QLabel):
@ -46,8 +47,9 @@ class VerticalLabel(QtGui.QLabel):
rgn = self.contentsRect()
align = self.alignment()
#align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter
self.hint = p.drawText(rgn, align, self.text())
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.hint = p.drawText(rgn, align, self.text())
p.end()
if self.orientation == 'vertical':

View File

@ -4,9 +4,10 @@ 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
faulthandler_timeout = 30
faulthandler_timeout = 60
filterwarnings =
error
# re-enable standard library warnings
once::DeprecationWarning
once::PendingDeprecationWarning
@ -19,3 +20,5 @@ filterwarnings =
ignore:Visible window deleted. To prevent this, store a reference to the window object.
# xvfb warnings on non-linux systems
ignore:Unknown config option:pytest.PytestConfigWarning
# pyreadline windows warning
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working:DeprecationWarning:pyreadline:8

View File

@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
#try:
# from PyQt5 import sip
#except ImportError:
# import sip
# sip.setapi('QString', 1)
import pyqtgraph as pg
pg.mkQApp()

View File

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
import pytest
import pyqtgraph as pg
from collections import OrderedDict
import pyqtgraph.dockarea as da
pg.mkQApp()
import pyqtgraph.dockarea as da
def test_dockarea():
a = da.DockArea()
@ -176,14 +174,14 @@ def test_dockarea():
# a superfluous vertical splitter in state2 has been removed
state4 = a4.saveState()
state4['main'][1][0] = state4['main'][1][0][1][0]
assert clean_state(state4['main']) == clean_state(state2['main'])
with pytest.raises(AssertionError):
# this test doesn't work, likely due to clean_state not working as intended
assert clean_state(state4['main']) == clean_state(state2['main'])
def clean_state(state):
# return state dict with sizes removed
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
state = (state[0], ch, {})
if __name__ == '__main__':
test_dockarea()
return state

View File

@ -4,7 +4,6 @@ CSV export test
from __future__ import division, print_function, absolute_import
import pyqtgraph as pg
import csv
import os
import tempfile
app = pg.mkQApp()
@ -15,44 +14,33 @@ def approxeq(a, b):
def test_CSVExporter():
tempfilename = tempfile.NamedTemporaryFile(suffix='.csv').name
print("using %s as a temporary file" % tempfilename)
plt = pg.plot()
y1 = [1,3,2,3,1,6,9,8,4,2]
plt.plot(y=y1, name='myPlot')
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
x2 = pg.np.linspace(0, 1.0, len(y2))
plt.plot(x=x2, y=y2)
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
plt.plot(x=x3, y=y3, stepMode="center")
ex = pg.exporters.CSVExporter(plt.plotItem)
ex.export(fileName=tempfilename)
with open(tempfilename, 'r') as csv_file:
r = csv.reader(csv_file)
lines = [line for line in r]
ex = pg.exporters.CSVExporter(plt.plotItem)
with tempfile.NamedTemporaryFile(mode="w+t", suffix='.csv', encoding="utf-8", delete=False) as tf:
print("using %s as a temporary file" % tf.name)
ex.export(fileName=tf.name)
lines = [line for line in csv.reader(tf)]
header = lines.pop(0)
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
i = 0
for vals in lines:
for i, vals in enumerate(lines):
vals = list(map(str.strip, vals))
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
i += 1
os.unlink(tempfilename)
if __name__ == '__main__':
test_CSVExporter()

View File

@ -5,7 +5,6 @@ from pyqtgraph.exporters import HDF5Exporter
import numpy as np
from numpy.testing import assert_equal
import h5py
import os
@pytest.fixture

View File

@ -1,18 +1,10 @@
"""
SVG export test
"""
from __future__ import division, print_function, absolute_import
import pyqtgraph as pg
import tempfile
import os
app = pg.mkQApp()
def test_plotscene():
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
print("using %s as a temporary file" % tempfilename)
def test_plotscene(tmpdir):
pg.setConfigOption('foreground', (0,0,0))
w = pg.GraphicsLayoutWidget()
w.show()
@ -25,15 +17,13 @@ def test_plotscene():
app.processEvents()
ex = pg.exporters.SVGExporter(w.scene())
ex.export(fileName=tempfilename)
tf = tmpdir.join("expot.svg")
ex.export(fileName=tf)
# clean up after the test is done
os.unlink(tempfilename)
w.close()
def test_simple():
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
print("using %s as a temporary file" % tempfilename)
def test_simple(tmpdir):
view = pg.GraphicsView()
view.show()
@ -78,5 +68,5 @@ def test_simple():
grp2.addItem(rect3)
ex = pg.exporters.SVGExporter(scene)
ex.export(fileName=tempfilename)
os.unlink(tempfilename)
tf = tmpdir.join("expot.svg")
ex.export(fileName=tf)

View File

@ -15,7 +15,7 @@ def test_PlotItem_shared_axis_items(orientation):
layout = pg.GraphicsLayoutWidget()
pi1 = layout.addPlot(axisItems={orientation: ax1})
_ = layout.addPlot(axisItems={orientation: ax1})
pi2 = layout.addPlot()
# left or bottom replaces, right or top adds new

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import pyqtgraph as pg
import pytest
app = pg.mkQApp()

View File

@ -1,16 +1,19 @@
import pyqtgraph as pg
from math import isclose
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
viewPixelSize = view.viewPixelSize()
assert isclose(view.mapToView(axisSpec[1]).x(), 0.25, abs_tol=viewPixelSize[0])
assert isclose(view.mapToView(axisSpec[2]).x(), 0.75, abs_tol=viewPixelSize[0])
def test_left(p, axisSpec, tickSpecs, textSpecs):
assert view.mapToView(axisSpec[1]).y() == 0.875
assert view.mapToView(axisSpec[2]).y() == 0.125
viewPixelSize = view.viewPixelSize()
assert isclose(view.mapToView(axisSpec[1]).y(), 0.875, abs_tol=viewPixelSize[1])
assert isclose(view.mapToView(axisSpec[2]).y(), 0.125, abs_tol=viewPixelSize[1])
plot = pg.PlotWidget()
view = plot.plotItem.getViewBox()

View File

@ -1,11 +1,8 @@
import weakref
try:
import faulthandler
faulthandler.enable()
except ImportError:
pass
import pyqtgraph as pg
import faulthandler
faulthandler.enable()
pg.mkQApp()
def test_getViewWidget():

View File

@ -2,10 +2,10 @@
import time
import pytest
from pyqtgraph.Qt import QtCore, QtGui, QtTest
from pyqtgraph.Qt import QtGui, QtTest
import numpy as np
import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved, TransposedImageItem
from tests.image_testing import assertImageApproved, TransposedImageItem
try:
import cupy
except ImportError:
@ -188,7 +188,6 @@ def test_ImageItem_axisorder():
def test_dividebyzero():
import pyqtgraph as pg
im = pg.image(pg.np.random.normal(size=(100,100)))
im.imageItem.setAutoDownsample(True)
im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25])

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtTest
from pyqtgraph.tests import mouseDrag, mouseMove
from tests.ui_testing import mouseDrag, mouseMove
pg.mkQApp()
@ -48,7 +48,7 @@ def test_InfiniteLine():
px = pg.Point(-0.5, -1.0 / 3**0.5)
assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill)
assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill)
plt.close()
def test_mouseInteraction():
# disable delay of mouse move events because events is called immediately in test
@ -96,7 +96,4 @@ def test_mouseInteraction():
assert hline2.mouseHovering == False
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
assert hline2.value() == -1
if __name__ == '__main__':
test_mouseInteraction()
plt.close()

View File

@ -2,7 +2,7 @@ import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtTest
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
from pyqtgraph.tests import assertImageApproved
from tests.image_testing import assertImageApproved
from pyqtgraph.colormap import ColorMap
import pyqtgraph.functions as fn
import pytest
@ -93,7 +93,7 @@ def test_NonUniformImage_colormap():
image = NonUniformImage(x, y, Z, border=fn.mkPen('g'))
cmap = ColorMap(pos=[0.0, 1.0], color=[(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)])
cmap = ColorMap(pos=[0.0, 1.0], color=[(0, 0, 0), (255, 255, 255)])
image.setColorMap(cmap)
viewbox.addItem(image)

View File

@ -1,6 +1,6 @@
import numpy as np
import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved
from tests.image_testing import assertImageApproved
def test_PlotCurveItem():
@ -30,7 +30,3 @@ def test_PlotCurveItem():
assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.")
p.close()
if __name__ == '__main__':
test_PlotCurveItem()

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
import sys
import numpy as np
import pytest
import pyqtgraph as pg
import platform
from pyqtgraph.Qt import QtCore, QtGui, QtTest
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow
import pytest
from tests.image_testing import assertImageApproved
from tests.ui_testing import mouseMove, mouseDrag, mouseClick, resizeWindow
app = pg.mkQApp()
pg.setConfigOption("mouseRateLimit", 0)
@ -39,17 +39,20 @@ def test_getArrayRegion_axisorder():
def check_getArrayRegion(roi, name, testResize=True, transpose=False):
# on windows, edges corner pixels seem to be slightly different from other platforms
# giving a pxCount=2 for a fudge factor
if isinstance(roi, (pg.ROI, pg.RectROI)) and platform.system() == "Windows":
pxCount = 2
else:
pxCount=-1
initState = roi.getState()
#win = pg.GraphicsLayoutWidget()
win = pg.GraphicsView()
win.show()
resizeWindow(win, 200, 400)
# Don't use Qt's layouts for testing--these generate unpredictable results.
#vb1 = win.addViewBox()
#win.nextRow()
#vb2 = win.addViewBox()
# Don't use Qt's layouts for testing--these generate unpredictable results.
# Instead, place the viewboxes manually
vb1 = pg.ViewBox()
win.scene().addItem(vb1)
@ -97,7 +100,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
vb2.enableAutoRange(True, True)
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.')
assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.', pxCount=pxCount)
with pytest.raises(TypeError):
roi.setPos(0, False)
@ -106,38 +109,33 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.')
assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.', pxCount=pxCount)
roi.setAngle(45)
roi.setPos([3, 0])
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.')
assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.', pxCount=pxCount)
if testResize:
roi.setSize([60, 60])
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.')
assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.', pxCount=pxCount)
img1.setPos(0, img1.height())
img1.setTransform(QtGui.QTransform().scale(1, -1).rotate(20), True)
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.')
assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.', pxCount=pxCount)
vb1.invertY()
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
# on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10
if pg.Qt.QT_LIB in {'PyQt4', 'PySide'}:
pxCount = 10
else:
pxCount=-1
assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount)
roi.setState(initState)
@ -146,13 +144,31 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0])
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.')
assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.', pxCount=pxCount)
# allow the roi to be re-used
roi.scene().removeItem(roi)
win.hide()
def test_mouseClickEvent():
plt = pg.GraphicsView()
plt.show()
resizeWindow(plt, 200, 200)
vb = pg.ViewBox()
plt.scene().addItem(vb)
vb.resize(200, 200)
QtTest.QTest.qWaitForWindowExposed(plt)
QtTest.QTest.qWait(100)
roi = pg.RectROI((0, 0), (10, 20), removable=True)
vb.addItem(roi)
app.processEvents()
mouseClick(plt, roi.mapToScene(pg.Point(2, 2)), QtCore.Qt.LeftButton)
def test_PolyLineROI():
rois = [
(pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'),

View File

@ -5,7 +5,6 @@ import numpy as np
def test_scatterplotitem():
app = pg.mkQApp()
app.processEvents()
plot = pg.PlotWidget()
# set view range equal to its bounding rect.
@ -99,7 +98,4 @@ def test_init_spots():
assert spots[1].pen() == pg.mkPen(None)
assert spots[1].brush() == pg.mkBrush(None)
assert spots[1].data() == 'zzz'
if __name__ == '__main__':
test_scatterplotitem()
plot.close()

View File

@ -1,4 +1,3 @@
import pytest
import pyqtgraph as pg
app = pg.mkQApp()

View File

@ -3,68 +3,32 @@
"""
Procedure for unit-testing with images:
1. Run unit tests at least once; this initializes a git clone of
pyqtgraph/test-data in ~/.pyqtgraph.
2. Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set:
Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set:
$ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
Any failing tests will display the test results, standard image, and the
differences between the two. If the test result is bad, then press (f)ail.
If the test result is good, then press (p)ass and the new image will be
saved to the test-data directory.
To check all test results regardless of whether the test failed, set the
environment variable PYQTGRAPH_AUDIT_ALL=1.
3. After adding or changing test images, create a new commit:
$ cd ~/.pyqtgraph/test-data
$ git add ...
$ git commit -a
4. Look up the most recent tag name from the `testDataTag` global variable
below. Increment the tag name by 1 and create a new tag in the test-data
repository:
$ git tag test-data-NNN
$ git push --tags origin master
This tag is used to ensure that each pyqtgraph commit is linked to a specific
commit in the test-data repository. This makes it possible to push new
commits to the test-data repository without interfering with existing
tests, and also allows unit tests to continue working on older pyqtgraph
versions.
Any failing tests will display the test results, standard image, and the
differences between the two. If the test result is bad, then press (f)ail.
If the test result is good, then press (p)ass and the new image will be
saved to the test-data directory.
To check all test results regardless of whether the test failed, set the
environment variable PYQTGRAPH_AUDIT_ALL=1.
"""
# This is the name of a tag in the test-data repository that this version of
# pyqtgraph should be tested against. When adding or changing test images,
# create and push a new tag and update this variable. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository.
testDataTag = 'test-data-8'
import time
import os
import sys
import inspect
import base64
import subprocess as sp
import warnings
import numpy as np
if sys.version[0] >= '3':
import http.client as httplib
import urllib.parse as urllib
else:
import httplib
import urllib
from ..Qt import QtGui, QtCore, QtTest, QT_LIB
from .. import functions as fn
from .. import GraphicsLayoutWidget
from .. import ImageItem, TextItem
from pathlib import Path
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import functions as fn
from pyqtgraph import GraphicsLayoutWidget
from pyqtgraph import ImageItem, TextItem
tester = None
@ -101,6 +65,21 @@ def getTester():
return tester
def getImageFromWidget(widget):
# just to be sure the widget size is correct (new window may be resized):
QtGui.QApplication.processEvents()
qimg = QtGui.QImage(widget.size(), QtGui.QImage.Format.Format_ARGB32)
qimg.fill(QtCore.Qt.GlobalColor.transparent)
painter = QtGui.QPainter(qimg)
widget.render(painter)
painter.end()
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
return fn.qimage_to_ndarray(qimg).copy()
def assertImageApproved(image, standardFile, message=None, **kwargs):
"""Check that an image test result matches a pre-approved standard.
@ -108,10 +87,6 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
to compare the images and decide whether to fail the test or save the new
image as the standard.
This function will automatically clone the test-data repository into
~/.pyqtgraph/test-data. However, it is up to the user to ensure this repository
is kept up to date and to commit/push new images after they are saved.
Run the test with the environment variable PYQTGRAPH_AUDIT=1 to bring up
the auditing GUI.
@ -131,43 +106,28 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
comparison (see ``assertImageMatch()``).
"""
if isinstance(image, QtGui.QWidget):
w = image
# just to be sure the widget size is correct (new window may be resized):
# just to be sure the widget size is correct (new window may be resized):
QtGui.QApplication.processEvents()
graphstate = scenegraphState(w, standardFile)
qimg = QtGui.QImage(w.size(), QtGui.QImage.Format.Format_ARGB32)
qimg.fill(QtCore.Qt.GlobalColor.transparent)
painter = QtGui.QPainter(qimg)
w.render(painter)
painter.end()
image = fn.imageToArray(qimg, copy=False, transpose=False)
# the standard images seem to have their Red and Blue swapped
if sys.byteorder == 'little':
# transpose B,G,R,A to R,G,B,A
image = image[..., [2, 1, 0, 3]]
else:
# transpose A,R,G,B to A,B,G,R
image = image[..., [0, 3, 2, 1]]
graphstate = scenegraphState(image, standardFile)
image = getImageFromWidget(image)
if message is None:
code = inspect.currentframe().f_back.f_code
message = "%s::%s" % (code.co_filename, code.co_name)
# Make sure we have a test data repo available, possibly invoking git
dataPath = getTestDataRepo()
# Make sure we have a test data repo available
dataPath = getTestDataDirectory()
# Read the standard image if it exists
stdFileName = os.path.join(dataPath, standardFile + '.png')
if not os.path.isfile(stdFileName):
stdImage = None
else:
pxm = QtGui.QPixmap()
pxm.load(stdFileName)
stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False)
qimg = QtGui.QImage(stdFileName)
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
stdImage = fn.qimage_to_ndarray(qimg).copy()
del qimg
# If the test image does not match, then we go to audit if requested.
try:
@ -191,18 +151,13 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype)
assertImageMatch(image, stdImage, **kwargs)
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
print(graphstate)
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 "
"%s`\n" % (stdFileName, dataPath, standardFile))
if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
sys.excepthook(*sys.exc_info())
getTester().test(image, stdImage, message)
@ -210,20 +165,18 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
print('Saving new standard image to "%s"' % stdFileName)
if not os.path.isdir(stdPath):
os.makedirs(stdPath)
img = fn.makeQImage(image, alpha=True, transpose=False)
img.save(stdFileName)
qimg = fn.ndarray_to_qimage(image, QtGui.QImage.Format.Format_RGBA8888)
qimg.save(stdFileName)
del qimg
else:
if stdImage is None:
raise Exception("Test standard %s does not exist. Set "
"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('CI') is not None:
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
saveFailedTest(image, stdImage, standardFile)
print(graphstate)
raise
if os.getenv('CI') is not None:
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
saveFailedTest(image, stdImage, standardFile)
print(graphstate)
raise
def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
@ -249,8 +202,8 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
pxThreshold : float
Minimum value difference at which two pixels are considered different
pxCount : int or None
Maximum number of pixels that may differ. Default is 0 for Qt4 and
1% of image size for Qt5.
Maximum number of pixels that may differ. Default is 0, on Windows some
tests have a value of 2.
maxPxDiff : float or None
Maximum allowed difference between pixels
avgPxDiff : float or None
@ -264,12 +217,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert im1.dtype == im2.dtype
if pxCount == -1:
if QT_LIB in {'PyQt5', 'PySide2', 'PySide6', 'PyQt6'}:
# Qt5 generates slightly different results; relax the tolerance
# until test images are updated.
pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
else:
pxCount = 0
pxCount = 0
diff = im1.astype(float) - im2.astype(float)
if imgDiff is not None:
@ -292,9 +240,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert corr >= minCorr
def saveFailedTest(data, expect, filename, upload=False):
"""Upload failed test images to web server to allow CI test debugging.
"""
def saveFailedTest(data, expect, filename):
# concatenate data, expect, and diff into a single image
ds = data.shape
es = expect.shape
@ -310,7 +256,7 @@ def saveFailedTest(data, expect, filename, upload=False):
diff = makeDiffImage(data, expect)
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
png = makePng(img)
png = makePng(data) # change `img` to `data` to save just the failed image
directory = os.path.dirname(filename)
if not os.path.isdir(directory):
os.makedirs(directory)
@ -318,38 +264,15 @@ def saveFailedTest(data, expect, filename, upload=False):
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, 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,
'data': base64.b64encode(png)})
conn.request('POST', '/upload.py', req)
response = conn.getresponse().read()
conn.close()
print("Uploaded to: \nhttp://%s/data/%s" % (host, filename))
if not response.startswith(b'OK'):
print("WARNING: Error uploading data to %s" % host)
print(response)
def makePng(img):
"""Given an array like (H, W, 4), return a PNG-encoded byte string.
"""
io = QtCore.QBuffer()
qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False)
qim = fn.ndarray_to_qimage(img, QtGui.QImage.Format.Format_RGBX8888)
qim.save(io, 'PNG')
png = bytes(io.data().data())
return png
return bytes(io.data().data())
def makeDiffImage(im1, im2):
@ -467,155 +390,18 @@ class ImageTester(QtGui.QWidget):
def getTestDataRepo():
"""Return the path to a git repository with the required commit checked
out.
If the repository does not exist, then it is cloned from
https://github.com/pyqtgraph/test-data. If the repository already exists
then the required commit is checked out.
"""
global testDataTag
if os.getenv("CI"):
dataPath = os.path.join(os.environ["GITHUB_WORKSPACE"], '.pyqtgraph', 'test-data')
else:
dataPath = os.path.join(os.path.expanduser('~'), '.pyqtgraph', 'test-data')
gitPath = 'https://github.com/pyqtgraph/test-data'
gitbase = gitCmdBase(dataPath)
if os.path.isdir(dataPath):
# Already have a test-data repository to work with.
# Get the commit ID of testDataTag. Do a fetch if necessary.
try:
tagCommit = gitCommitId(dataPath, testDataTag)
except NameError:
cmd = gitbase + ['fetch', '--tags', 'origin']
print(' '.join(cmd))
sp.check_call(cmd)
try:
tagCommit = gitCommitId(dataPath, testDataTag)
except NameError:
raise Exception("Could not find tag '%s' in test-data repo at"
" %s" % (testDataTag, dataPath))
except Exception:
if not os.path.exists(os.path.join(dataPath, '.git')):
raise Exception("Directory '%s' does not appear to be a git "
"repository. Please remove this directory." %
dataPath)
else:
raise
# If HEAD is not the correct commit, then do a checkout
if gitCommitId(dataPath, 'HEAD') != tagCommit:
print("Checking out test-data tag '%s'" % testDataTag)
sp.check_call(gitbase + ['checkout', testDataTag])
else:
print("Attempting to create git clone of test data repo in %s.." %
dataPath)
parentPath = os.path.split(dataPath)[0]
if not os.path.isdir(parentPath):
os.makedirs(parentPath)
if os.getenv('TRAVIS') is not None or os.getenv('CI') is not None:
# Create a shallow clone of the test-data repository (to avoid
# downloading more data than is necessary)
os.makedirs(dataPath)
cmds = [
gitbase + ['init'],
gitbase + ['remote', 'add', 'origin', gitPath],
gitbase + ['fetch', '--tags', 'origin', testDataTag,
'--depth=1'],
gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'],
]
else:
# Create a full clone
cmds = [['git', 'clone', gitPath, dataPath]]
for cmd in cmds:
print(' '.join(cmd))
rval = sp.check_call(cmd)
if rval == 0:
continue
raise RuntimeError("Test data path '%s' does not exist and could "
"not be created with git. Please create a git "
"clone of %s at this path." %
(dataPath, gitPath))
return dataPath
warnings.warn(
"Test data data repo has been merged with the main repo"
"use getTestDataDirectory() instead, this method will be removed"
"in a future version of pyqtgraph",
DeprecationWarning, stacklevel=2
)
return getTestDataDirectory()
def gitCmdBase(path):
return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path]
def gitStatus(path):
"""Return a string listing all changes to the working tree in a git
repository.
"""
cmd = gitCmdBase(path) + ['status', '--porcelain']
return runSubprocess(cmd, stderr=None, universal_newlines=True)
def gitCommitId(path, ref):
"""Return the commit id of *ref* in the git repository at *path*.
"""
cmd = gitCmdBase(path) + ['show', ref]
try:
output = runSubprocess(cmd, stderr=None, universal_newlines=True)
except sp.CalledProcessError:
print(cmd)
raise NameError("Unknown git reference '%s'" % ref)
commit = output.split('\n')[0]
assert commit[:7] == 'commit '
return commit[7:]
def runSubprocess(command, return_code=False, **kwargs):
"""Run command using subprocess.Popen
Similar to subprocess.check_output(), which is not available in 2.6.
Run command and wait for command to complete. If the return code was zero
then return, otherwise raise CalledProcessError.
By default, this will also add stdout= and stderr=subproces.PIPE
to the call to Popen to suppress printing to the terminal.
Parameters
----------
command : list of str
Command to run as subprocess (see subprocess.Popen documentation).
**kwargs : dict
Additional kwargs to pass to ``subprocess.Popen``.
Returns
-------
stdout : str
Stdout returned by the process.
"""
# code adapted with permission from mne-python
use_kwargs = dict(stderr=None, stdout=sp.PIPE)
use_kwargs.update(kwargs)
p = sp.Popen(command, **use_kwargs)
output = p.communicate()[0]
# communicate() may return bytes, str, or None depending on the kwargs
# passed to Popen(). Convert all to unicode str:
output = '' if output is None else output
output = output.decode('utf-8') if isinstance(output, bytes) else output
if p.returncode != 0:
print(output)
err_fun = sp.CalledProcessError.__init__
if 'output' in inspect.getfullargspec(err_fun).args:
raise sp.CalledProcessError(p.returncode, command, output)
else:
raise sp.CalledProcessError(p.returncode, command)
return output
def getTestDataDirectory():
dataPath = Path(__file__).absolute().parent / "images"
return dataPath.as_posix()
def scenegraphState(view, name):
@ -632,7 +418,7 @@ def scenegraphState(view, name):
def itemState(root):
state = str(root) + '\n'
from .. import ViewBox
from pyqtgraph import ViewBox
state += 'bounding rect: ' + str(root.boundingRect()) + '\n'
if isinstance(root, ViewBox):
state += "view range: " + str(root.viewRange()) + '\n'
@ -647,7 +433,7 @@ def transformStr(t):
def indent(s, pfx):
return '\n'.join([pfx+line for line in s.split('\n')])
return '\n'.join(pfx+line for line in s.split('\n'))
class TransposedImageItem(ImageItem):

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More