Merge branch 'develop' into update_changelog

This commit is contained in:
Kyle Sunden 2019-12-15 20:30:39 -06:00
commit bedb34bf8d
63 changed files with 1280 additions and 610 deletions

49
.flake8 Normal file
View File

@ -0,0 +1,49 @@
[flake8]
exclude = .git,.tox,__pycache__,doc,old,build,dist
show_source = True
statistics = True
verbose = 2
select =
E101,
E112,
E122,
E125,
E133,
E223,
E224,
E242,
E273,
E274,
E901,
E902,
W191,
W601,
W602,
W603,
W604,
E124,
E231,
E211,
E261,
E271,
E272,
E304,
F401,
F402,
F403,
F404,
E501,
E502,
E702,
E703,
E711,
E712,
E721,
F811,
F812,
F821,
F822,
F823,
F831,
F841,
W292

11
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
sha: master
hooks:
- id: check-added-large-files
args: ['--maxkb=100']
- id: check-case-conflict
- id: end-of-file-fixer
- id: fix-encoding-pragma
- id: mixed-line-ending
args: [--fix=lf]

View File

@ -1,6 +1,6 @@
# Contributing to PyQtGraph
Contributions to pyqtgraph are welcome!
Contributions to pyqtgraph are welcome!
Please use the following guidelines when preparing changes:
@ -13,11 +13,13 @@ Please use the following guidelines when preparing changes:
## 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.
* 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
### Rules
* 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
@ -33,9 +35,15 @@ Please use the following guidelines when preparing changes:
============== ========================================================
```
QObject subclasses that implement new signals should also describe
QObject subclasses that implement new signals should also describe
these in a similar table.
### Pre-Commit
PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to ensure it conforms to various standards, such as `flake8`, utf-8 encoding pragma, line-ending fixers, and so on. If any of the checks fail, the commit will be rejected, and you will have the opportunity to make the necessary fixes before adding and committing a file again. This ensures that every commit made conforms to (most) of the styling standards that the library enforces; and you will most likely pass the code style checks by the CI.
To make use of `pre-commit`, have it available in your `$PATH` and run `pre-commit install` from the root directory of PyQtGraph.
## Testing Setting up a test environment
### Dependencies

View File

@ -22,8 +22,9 @@ Requirements
* PyQt 4.8+, PySide, PyQt5, or PySide2
* python 2.7, or 3.x
* Required
* `numpy`, `scipy`
* `numpy`
* Optional
* `scipy` for image processing
* `pyopengl` for 3D graphics
* macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics
* `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy`

View File

@ -1,7 +1,3 @@
############################################################################################
# This config was rectrieved in no small part from https://github.com/slaclab/pydm
############################################################################################
trigger:
branches:
include:
@ -20,19 +16,83 @@ pr:
variables:
OFFICIAL_REPO: 'pyqtgraph/pyqtgraph'
DEFAULT_MERGE_BRANCH: 'develop'
jobs:
- template: azure-test-template.yml
parameters:
name: Linux
stages:
- stage: "pre_test"
jobs:
- job: check_diff_size
pool:
vmImage: 'Ubuntu 16.04'
steps:
- bash: |
git config --global advice.detachedHead false
mkdir ~/repo-clone && cd ~/repo-clone
git init
git remote add -t $(Build.SourceBranchName) origin $(Build.Repository.Uri)
git remote add -t ${DEFAULT_MERGE_BRANCH} upstream https://github.com/${OFFICIAL_REPO}.git
git fetch origin $(Build.SourceBranchName)
git fetch upstream ${DEFAULT_MERGE_BRANCH}
git checkout $(Build.SourceBranchName)
MERGE_SIZE=`du -s . | sed -e "s/\t.*//"`
echo -e "Merge Size ${MERGE_SIZE}"
git checkout ${DEFAULT_MERGE_BRANCH}
TARGET_SIZE=`du -s . | sed -e "s/\t.*//"`
echo -e "Target Size ${TARGET_SIZE}"
if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
else
SIZE_DIFF=0;
fi;
echo -e "Estimated content size difference = ${SIZE_DIFF} kB" &&
test ${SIZE_DIFF} -lt 100;
displayName: 'Diff Size Check'
continueOnError: true
- job: "style_check"
pool:
vmImage: "Ubuntu 16.04"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.7
- bash: |
pip install flake8
python setup.py style
displayName: 'flake8 check'
continueOnError: true
- job: "build_wheel"
pool:
vmImage: 'Ubuntu 16.04'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.7
- script: |
python -m pip install setuptools wheel
python setup.py bdist_wheel --universal
displayName: "Build Python Wheel"
continueOnError: false
- publish: dist
artifact: wheel
- stage: "test"
jobs:
- template: azure-test-template.yml
parameters:
name: Windows
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
name: macOS
vmImage: 'macOS-10.13'

View File

@ -26,16 +26,22 @@ jobs:
python.version: "3.6"
qt.bindings: "pyside2"
install.method: "conda"
Python37-PyQt-5.12:
Python37-PyQt-5.13:
python.version: '3.7'
qt.bindings: "PyQt5"
install.method: "pip"
Python37-PySide2-5.12:
Python37-PySide2-5.13:
python.version: "3.7"
qt.bindings: "PySide2"
install.method: "pip"
steps:
- task: DownloadPipelineArtifact@2
inputs:
source: 'current'
artifact: wheel
path: 'dist'
- task: ScreenResolutionUtility@1
inputs:
displaySettings: 'specific'
@ -43,6 +49,11 @@ jobs:
height: '1080'
condition: eq(variables['agent.os'], 'Windows_NT' )
- task: UsePythonVersion@0
inputs:
versionSpec: $(python.version)
condition: eq(variables['install.method'], 'pip')
- 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
@ -60,75 +71,71 @@ jobs:
displayName: "Install Windows-Mesa OpenGL DLL"
condition: eq(variables['agent.os'], 'Windows_NT')
- task: UsePythonVersion@0
inputs:
versionSpec: $(python.version)
condition: eq(variables['install.method'], 'pip')
- bash: |
if [ $(agent.os) == 'Linux' ]
then
echo '##vso[task.prependpath]/usr/share/miniconda/bin'
echo "##vso[task.prependpath]$CONDA/bin"
if [ $(python.version) == '2.7' ]
then
echo "Grabbing Older Miniconda"
wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-Linux-x86_64.sh -O Miniconda.sh
bash Miniconda.sh -b -p $CONDA -f
fi
elif [ $(agent.os) == 'Darwin' ]
then
echo '##vso[task.prependpath]$CONDA/bin'
sudo install -d -m 0777 /usr/local/miniconda/envs
sudo chown -R $USER $CONDA
echo "##vso[task.prependpath]$CONDA/bin"
if [ $(python.version) == '2.7' ]
then
echo "Grabbing Older Miniconda"
wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-MacOSX-x86_64.sh -O Miniconda.sh
bash Miniconda.sh -b -p $CONDA -f
fi
elif [ $(agent.os) == 'Windows_NT' ]
then
echo "##vso[task.prependpath]$env:CONDA\Scripts"
echo "##vso[task.prependpath]$CONDA/Scripts"
else
echo 'Just what OS are you using?'
fi
displayName: 'Add Conda to $PATH'
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)'
updateConda: false
- bash: |
if [ $(install.method) == "conda" ]
then
conda create --name test-environment-$(python.version) python=$(python.version) --yes
echo "Conda Info:"
conda info
echo "Installing qt-bindings"
source activate test-environment-$(python.version)
conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest six coverage --yes --quiet
if [ $(agent.os) == "Linux" ] && [ $(python.version) == "2.7" ]
then
conda install $(qt.bindings) --yes
else
conda install -c conda-forge $(qt.bindings) --yes
fi
echo "Installing remainder of dependencies"
conda install -c conda-forge numpy scipy six pyopengl h5py --yes
else
pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage
pip install $(qt.bindings) numpy scipy pyopengl six h5py
fi
pip install pytest-xdist pytest-cov
echo ""
pip install pytest pytest-xdist pytest-cov coverage
if [ $(python.version) == "2.7" ]
then
pip install pytest-faulthandler
pip install pytest-faulthandler==1.6.0
export PYTEST_ADDOPTS="--faulthandler-timeout=15"
fi
displayName: "Install Dependencies"
- 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
displayName: "Copy Binary Wheel Distribution To Artifacts"
- task: PublishBuildArtifacts@1
displayName: 'Publish Binary Wheel'
condition: always()
inputs:
pathtoPublish: $(Build.ArtifactStagingDirectory)/dist
artifactName: Distributions
python -m pip install --no-index --find-links=dist pyqtgraph
displayName: 'Install Wheel'
- bash: |
sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm

View File

@ -30,8 +30,13 @@ Export Formats
for export.
* Printer - Exports to the operating system's printing service. This exporter is provided for completeness,
but is not well supported due to problems with Qt's printing system.
* HDF5 - Exports data from a :class:`~pyqtgraph.PlotItem` to a HDF5 file if
h5py_ is installed. This exporter supports :class:`~pyqtgraph.PlotItem`
objects containing multiple curves, stacking the data into a single HDF5
dataset based on the ``columnMode`` parameter. If data items aren't the same
size, each one is given its own dataset.
.. _h5py: https://www.h5py.org/
Exporting from the API
----------------------

View File

@ -3,8 +3,7 @@
## Add path to library (just for examples; you do not need this)
import initExample
from scipy import random
import numpy as np
from numpy import linspace
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
@ -22,7 +21,7 @@ pw = MultiPlotWidget()
mw.setCentralWidget(pw)
mw.show()
data = random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]])
data = np.random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]])
ma = MetaArray(data, info=[
{'name': 'Signal', 'cols': [
{'name': 'Col1', 'units': 'V'},

View File

@ -103,6 +103,7 @@ def mkData():
dt = np.float
loc = 1.0
scale = 0.1
mx = 1.0
if ui.rgbCheck.isChecked():
data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
from pyqtgraph import Qt
from . import utils
@ -5,7 +6,6 @@ from collections import namedtuple
import errno
import importlib
import itertools
import pkgutil
import pytest
import os, sys
import subprocess
@ -41,7 +41,12 @@ if os.getenv('TRAVIS') is not None:
files = sorted(set(utils.buildFileList(utils.examples)))
frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: 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:
@ -50,48 +55,136 @@ for frontend in frontends.keys():
except ImportError:
pass
installedFrontends = 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 = {
"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"),
'GLVolumeItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLIsosurface.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLSurfacePlot.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLScatterPlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLshaders.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLLinePlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLMeshItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"),
'GLImageItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939")
conditionalExamples = {
"hdf5.py": exceptionCondition(
False,
reason="Example requires user interaction"
),
"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"
)
),
'GLVolumeItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLIsosurface.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLSurfacePlot.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLScatterPlotItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLshaders.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLLinePlotItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLMeshItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLImageItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
)
}
@pytest.mark.parametrize(
"frontend, f",
[
pytest.param(
frontend,
"frontend, f",
[
pytest.param(
frontend,
f,
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(installedFrontends, files)
],
ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)]
marks=pytest.mark.skipif(
conditionalExamples[f[1]].condition is False,
reason=conditionalExamples[f[1]].reason
) if f[1] in conditionalExamples.keys() else (),
)
for frontend, f, in itertools.product(installedFrontends, 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)
name, file = f
global path
fn = os.path.join(path,file)
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
graphicsSystem = (
'' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
)
code = """
try:
%s
@ -123,7 +216,7 @@ except:
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
process.stdin.write(code.encode('UTF-8'))
process.stdin.close() ##?
process.stdin.close()
output = ''
fail = False
while True:
@ -146,10 +239,14 @@ except:
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():
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)
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} "
.format(res[0].decode(), res[1].decode(), name, file),
pytrace=False)
if __name__ == "__main__":
pytest.cmdline.main()

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import weakref
from ..Qt import QtCore, QtGui
from ..python2_3 import sortList, cmp
from ..Point import Point
from .. import functions as fn
from .. import ptime as ptime
@ -183,12 +183,14 @@ class GraphicsScene(QtGui.QGraphicsScene):
if int(ev.buttons() & btn) == 0:
continue
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
dist = Point(ev.scenePos() - cev.scenePos()).length()
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
continue
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
self.dragButtons.append(int(btn))
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)]
if cev:
cev = cev[0]
dist = Point(ev.scenePos() - cev.scenePos()).length()
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
continue
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
self.dragButtons.append(int(btn))
## If we have dragged buttons, deliver a drag event
if len(self.dragButtons) > 0:
@ -208,10 +210,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.dragButtons.remove(ev.button())
else:
cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())]
if self.sendClickEvent(cev[0]):
#print "sent click event"
ev.accept()
self.clickEvents.remove(cev[0])
if cev:
if self.sendClickEvent(cev[0]):
#print "sent click event"
ev.accept()
self.clickEvents.remove(cev[0])
if int(ev.buttons()) == 0:
self.dragItem = None
@ -451,7 +454,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
return 0
return item.zValue() + absZValue(item.parentItem())
sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a)))
items2.sort(key=absZValue, reverse=True)
return items2
@ -560,6 +563,3 @@ class GraphicsScene(QtGui.QGraphicsScene):
@staticmethod
def translateGraphicsItems(items):
return list(map(GraphicsScene.translateGraphicsItem, items))

View File

@ -23,6 +23,8 @@ class ExportDialog(QtGui.QWidget):
self.currentExporter = None
self.scene = scene
self.exporterParameters = {}
self.selectBox = QtGui.QGraphicsRectItem()
self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine))
self.selectBox.hide()
@ -121,7 +123,18 @@ class ExportDialog(QtGui.QWidget):
return
expClass = self.exporterClasses[str(item.text())]
exp = expClass(item=self.ui.itemTree.currentItem().gitem)
params = exp.parameters()
if prev:
oldtext = str(prev.text())
self.exporterParameters[oldtext] = self.currentExporter.parameters()
newtext = str(item.text())
if newtext in self.exporterParameters.keys():
params = self.exporterParameters[newtext]
exp.params = params
else:
params = exp.parameters()
self.exporterParameters[newtext] = params
if params is None:
self.ui.paramTree.clear()
else:

View File

@ -29,9 +29,6 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1]
## helpers for 2/3 compatibility
from . import python2_3
## install workarounds for numpy bugs
from . import numpy_fix
## in general openGL is poorly supported with Qt+GraphicsView.
## we only enable it where the performance benefit is critical.
## Note this only applies to 2D graphics; 3D graphics always use OpenGL.
@ -67,7 +64,6 @@ CONFIG_OPTIONS = {
def setConfigOption(opt, value):
global CONFIG_OPTIONS
if opt not in CONFIG_OPTIONS:
raise KeyError('Unknown configuration option "%s"' % opt)
if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'):
@ -99,7 +95,8 @@ def systemInfo():
if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file
lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision')
if os.path.exists(lastRevFile):
rev = open(lastRevFile, 'r').read().strip()
with open(lastRevFile, 'r') as fd:
rev = fd.read().strip()
print("pyqtgraph: %s; %s" % (__version__, rev))
print("config:")
@ -264,6 +261,7 @@ from .widgets.LayoutWidget import *
from .widgets.TableWidget import *
from .widgets.ProgressDialog import *
from .widgets.GroupBox import GroupBox
from .widgets.RemoteGraphicsView import RemoteGraphicsView
from .imageview import *
from .WidgetGroup import *

View File

@ -1,8 +1,4 @@
# -*- coding: utf-8 -*-
if __name__ == '__main__':
import sys, os
md = os.path.dirname(os.path.abspath(__file__))
sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path
from ..Qt import QtGui, QtCore, QT_LIB
from ..graphicsItems.ROI import ROI

View File

@ -39,10 +39,10 @@ class ParseError(Exception):
def writeConfigFile(data, fname):
s = genString(data)
fd = open(fname, 'w')
fd.write(s)
fd.close()
with open(fname, 'w') as fd:
fd.write(s)
def readConfigFile(fname):
#cwd = os.getcwd()
global GLOBAL_PATH
@ -55,9 +55,8 @@ def readConfigFile(fname):
try:
#os.chdir(newDir) ## bad.
fd = open(fname)
s = asUnicode(fd.read())
fd.close()
with open(fname) as fd:
s = asUnicode(fd.read())
s = s.replace("\r\n", "\n")
s = s.replace("\r", "\n")
data = parseString(s)[1]
@ -73,9 +72,8 @@ def readConfigFile(fname):
def appendConfigFile(data, fname):
s = genString(data)
fd = open(fname, 'a')
fd.write(s)
fd.close()
with open(fname, 'a') as fd:
fd.write(s)
def genString(data, indent=''):
@ -194,8 +192,6 @@ def measureIndent(s):
if __name__ == '__main__':
import tempfile
fn = tempfile.mktemp()
tf = open(fn, 'w')
cf = """
key: 'value'
key2: ##comment
@ -205,8 +201,9 @@ key2: ##comment
key22: [1,2,3]
key23: 234 #comment
"""
tf.write(cf)
tf.close()
fn = tempfile.mktemp()
with open(fn, 'w') as tf:
tf.write(cf)
print("=== Test:===")
num = 1
for line in cf.split('\n'):

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import sys, re, os, time, traceback, subprocess
import pickle
@ -98,12 +99,14 @@ class ConsoleWidget(QtGui.QWidget):
def loadHistory(self):
"""Return the list of previously-invoked command strings (or None)."""
if self.historyFile is not None:
return pickle.load(open(self.historyFile, 'rb'))
with open(self.historyFile, 'rb') as pf:
return pickle.load(pf)
def saveHistory(self, history):
"""Store the list of previously-invoked command strings."""
if self.historyFile is not None:
pickle.dump(open(self.historyFile, 'wb'), history)
with open(self.historyFile, 'wb') as pf:
pickle.dump(pf, history)
def runCmd(self, cmd):
self.stdout = sys.stdout

View File

@ -9,9 +9,9 @@ from ..python2_3 import basestring
class DockArea(Container, QtGui.QWidget, DockDrop):
def __init__(self, temporary=False, home=None):
def __init__(self, parent=None, temporary=False, home=None):
Container.__init__(self, self)
QtGui.QWidget.__init__(self)
QtGui.QWidget.__init__(self, parent=parent)
DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom'])
self.layout = QtGui.QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
from .Exporter import Exporter
from ..parametertree import Parameter
@ -29,7 +30,6 @@ class CSVExporter(Exporter):
self.fileSaveDialog(filter=["*.csv", "*.tsv"])
return
fd = open(fileName, 'w')
data = []
header = []
@ -55,28 +55,29 @@ class CSVExporter(Exporter):
sep = ','
else:
sep = '\t'
fd.write(sep.join(header) + '\n')
i = 0
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for j, d in enumerate(data):
# write x value if this is the first column, or if we want x
# for all rows
if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
with open(fileName, 'w') as fd:
fd.write(sep.join(header) + '\n')
i = 0
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for j, d in enumerate(data):
# write x value if this is the first column, or if we want
# x for all rows
if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
else:
fd.write(' %s' % sep)
# write y value
if d is not None and i < len(d[1]):
fd.write(numFormat % d[1][i] + sep)
else:
fd.write(' %s' % sep)
# write y value
if d is not None and i < len(d[1]):
fd.write(numFormat % d[1][i] + sep)
else:
fd.write(' %s' % sep)
fd.write('\n')
fd.close()
fd.write('\n')
CSVExporter.register()

View File

@ -44,20 +44,27 @@ class HDF5Exporter(Exporter):
data = []
appendAllX = self.params['columnMode'] == '(x,y) per plot'
#print dir(self.item.curves[0])
tlen = 0
for i, c in enumerate(self.item.curves):
d = c.getData()
if i > 0 and len(d[0]) != tlen:
raise ValueError ("HDF5 Export requires all curves in plot to have same length")
if appendAllX or i == 0:
data.append(d[0])
tlen = len(d[0])
data.append(d[1])
# Check if the arrays are ragged
len_first = len(self.item.curves[0].getData()[0]) if self.item.curves[0] else None
ragged = any(len(i.getData()[0]) != len_first for i in self.item.curves)
if ragged:
dgroup = fd.create_group(dsname)
for i, c in enumerate(self.item.curves):
d = c.getData()
fdata = numpy.array([d[0], d[1]]).astype('double')
cname = c.name() if c.name() is not None else str(i)
dset = dgroup.create_dataset(cname, data=fdata)
else:
for i, c in enumerate(self.item.curves):
d = c.getData()
if appendAllX or i == 0:
data.append(d[0])
data.append(d[1])
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fd.close()
if HAVE_HDF5:

View File

@ -58,17 +58,17 @@ class ImageExporter(Exporter):
filter.insert(0, p)
self.fileSaveDialog(filter=filter)
return
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect()
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
w, h = self.params['width'], self.params['height']
w = int(self.params['width'])
h = int(self.params['height'])
if w == 0 or h == 0:
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h))
bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte)
raise Exception("Cannot export image with size=0 (requested "
"export size is %dx%d)" % (w, h))
targetRect = QtCore.QRect(0, 0, w, h)
sourceRect = self.getSourceRect()
bg = np.empty((h, w, 4), dtype=np.ubyte)
color = self.params['background']
bg[:,:,0] = color.blue()
bg[:,:,1] = color.green()

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
import pytest
import pyqtgraph as pg
from pyqtgraph.exporters import HDF5Exporter
import numpy as np
from numpy.testing import assert_equal
import h5py
import os
@pytest.fixture
def tmp_h5(tmp_path):
yield tmp_path / "data.h5"
@pytest.mark.parametrize("combine", [False, True])
def test_HDF5Exporter(tmp_h5, combine):
# Basic test of functionality: multiple curves with shared x array. Tests
# both options for stacking the data (columnMode).
x = np.linspace(0, 1, 100)
y1 = np.sin(x)
y2 = np.cos(x)
plt = pg.plot()
plt.plot(x=x, y=y1)
plt.plot(x=x, y=y2)
ex = HDF5Exporter(plt.plotItem)
if combine:
ex.parameters()['columnMode'] = '(x,y,y,y) for all plots'
ex.export(fileName=tmp_h5)
with h5py.File(tmp_h5, 'r') as f:
# should be a single dataset with the name of the exporter
dset = f[ex.parameters()['Name']]
assert isinstance(dset, h5py.Dataset)
if combine:
assert_equal(np.array([x, y1, y2]), dset)
else:
assert_equal(np.array([x, y1, x, y2]), dset)
def test_HDF5Exporter_unequal_lengths(tmp_h5):
# Test export with multiple curves of different size. The exporter should
# detect this and create multiple hdf5 datasets under a group.
x1 = np.linspace(0, 1, 10)
y1 = np.sin(x1)
x2 = np.linspace(0, 1, 100)
y2 = np.cos(x2)
plt = pg.plot()
plt.plot(x=x1, y=y1, name='plot0')
plt.plot(x=x2, y=y2)
ex = HDF5Exporter(plt.plotItem)
ex.export(fileName=tmp_h5)
with h5py.File(tmp_h5, 'r') as f:
# should be a group with the name of the exporter
group = f[ex.parameters()['Name']]
assert isinstance(group, h5py.Group)
# should be a dataset under the group with the name of the PlotItem
assert_equal(np.array([x1, y1]), group['plot0'])
# should be a dataset under the group with a default name that's the
# index of the curve in the PlotItem
assert_equal(np.array([x2, y2]), group['1'])

View File

@ -28,6 +28,7 @@ def test_plotscene():
ex.export(fileName=tempfilename)
# clean up after the test is done
os.unlink(tempfilename)
w.close()
def test_simple():
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name

View File

@ -834,9 +834,9 @@ class FlowchartWidget(dockarea.DockArea):
def buildMenu(self, pos=None):
def buildSubMenu(node, rootMenu, subMenus, pos=None):
for section, node in node.items():
menu = QtGui.QMenu(section)
rootMenu.addMenu(menu)
if isinstance(node, OrderedDict):
if isinstance(node, OrderedDict):
menu = QtGui.QMenu(section)
rootMenu.addMenu(menu)
buildSubMenu(node, menu, subMenus, pos=pos)
subMenus.append(menu)
else:

View File

@ -11,6 +11,7 @@ import numpy as np
import decimal, re
import ctypes
import sys, struct
from .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, QT_LIB
from . import getConfigOption, setConfigOptions
@ -78,7 +79,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True):
pref = SI_PREFIXES_ASCII[m+8]
p = .001**m
return (p, pref)
return (p, pref)
def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True):
@ -424,6 +425,8 @@ def eq(a, b):
3. When comparing arrays, returns False if the array shapes are not the same.
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
the == operator would return a boolean array).
5. Collections (dict, list, etc.) must have the same type to be considered equal. One
consequence is that comparing a dict to an OrderedDict will always return False.
"""
if a is b:
return True
@ -440,6 +443,28 @@ def eq(a, b):
if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype):
return False
# Recursively handle common containers
if isinstance(a, dict) and isinstance(b, dict):
if type(a) != type(b) or len(a) != len(b):
return False
if set(a.keys()) != set(b.keys()):
return False
for k, v in a.items():
if not eq(v, b[k]):
return False
if isinstance(a, OrderedDict) or sys.version_info >= (3, 7):
for a_item, b_item in zip(a.items(), b.items()):
if not eq(a_item, b_item):
return False
return True
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
if type(a) != type(b) or len(a) != len(b):
return False
for v1,v2 in zip(a, b):
if not eq(v1, v2):
return False
return True
# Test for equivalence.
# If the test raises a recognized exception, then return Falase
try:
@ -1035,7 +1060,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
============== ==================================================================================
"""
profile = debug.Profiler()
if data.ndim not in (2, 3):
raise TypeError("data must be 2D or 3D")
if data.ndim == 3 and data.shape[2] > 4:
@ -1083,7 +1107,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
dtype = np.ubyte
else:
dtype = np.min_scalar_type(lut.shape[0]-1)
# awkward, but fastest numpy native nan evaluation
#
nanMask = None
if data.dtype.kind == 'f' and np.isnan(data.min()):
nanMask = np.isnan(data)
# Apply levels if given
if levels is not None:
if isinstance(levels, np.ndarray) and levels.ndim == 2:
@ -1106,10 +1135,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal)
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
profile()
# apply LUT if given
if lut is not None:
data = applyLookupTable(data, lut)
@ -1152,7 +1179,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
imgData[..., 3] = 255
else:
alpha = True
# apply nan mask through alpha channel
if nanMask is not None:
alpha = True
imgData[nanMask, 3] = 0
profile()
return imgData, alpha

View File

@ -17,7 +17,7 @@ class AxisItem(GraphicsWidget):
If maxTickLength is negative, ticks point into the plot.
"""
def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
def __init__(self, orientation, pen=None, textPen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
"""
============== ===============================================================
**Arguments:**
@ -28,6 +28,7 @@ class AxisItem(GraphicsWidget):
to be linked to the visible range of a ViewBox.
showValues (bool) Whether to display values adjacent to ticks
pen (QPen) Pen used when drawing ticks.
textPen (QPen) Pen used when drawing tick labels.
text The text (excluding units) to display on the label for this
axis.
units The units for this axis. Units should generally be given
@ -97,6 +98,11 @@ class AxisItem(GraphicsWidget):
else:
self.setPen(pen)
if textPen is None:
self.setTextPen()
else:
self.setTextPen(pen)
self._linkedView = None
if linkView is not None:
self.linkToView(linkView)
@ -405,6 +411,25 @@ class AxisItem(GraphicsWidget):
self.setLabel()
self.update()
def textPen(self):
if self._textPen is None:
return fn.mkPen(getConfigOption('foreground'))
return fn.mkPen(self._textPen)
def setTextPen(self, *args, **kwargs):
"""
Set the pen used for drawing text.
If no arguments are given, the default foreground color will be used.
"""
self.picture = None
if args or kwargs:
self._textPen = fn.mkPen(*args, **kwargs)
else:
self._textPen = fn.mkPen(getConfigOption('foreground'))
self.labelStyle['color'] = '#' + fn.colorStr(self._textPen.color())[:6]
self.setLabel()
self.update()
def setScale(self, scale=None):
"""
Set the value scaling for this axis.
@ -444,7 +469,11 @@ class AxisItem(GraphicsWidget):
def updateAutoSIPrefix(self):
if self.label.isVisible():
(scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
if self.logMode:
_range = 10**np.array(self.range)
else:
_range = self.range
(scale, prefix) = fn.siScale(max(abs(_range[0]*self.scale), abs(_range[1]*self.scale)))
if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling.
scale = 1.0
prefix = ''
@ -771,7 +800,7 @@ class AxisItem(GraphicsWidget):
return strings
def logTickStrings(self, values, scale, spacing):
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)]
def generateDrawSpecs(self, p):
"""
@ -1044,13 +1073,13 @@ class AxisItem(GraphicsWidget):
p.drawLine(p1, p2)
profiler('draw ticks')
## Draw all text
# Draw all text
if self.tickFont is not None:
p.setFont(self.tickFont)
p.setPen(self.pen())
p.setPen(self.textPen())
for rect, flags, text in textSpecs:
p.drawText(rect, flags, text)
#p.drawRect(rect)
profiler('draw text')
def show(self):

View File

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
import operator
import weakref
import numpy as np
from ..Qt import QtGui, QtCore
from ..python2_3 import sortList
from .. import functions as fn
from .GraphicsObject import GraphicsObject
from .GraphicsWidget import GraphicsWidget
from ..widgets.SpinBox import SpinBox
from ..pgcollections import OrderedDict
from ..colormap import ColorMap
from ..python2_3 import cmp
__all__ = ['TickSliderItem', 'GradientEditorItem']
@ -352,8 +352,7 @@ class TickSliderItem(GraphicsWidget):
def listTicks(self):
"""Return a sorted list of all the Tick objects on the slider."""
## public
ticks = list(self.ticks.items())
sortList(ticks, lambda a,b: cmp(a[1], b[1])) ## see pyqtgraph.python2_3.sortList
ticks = sorted(self.ticks.items(), key=operator.itemgetter(1))
return ticks
@ -944,4 +943,3 @@ class TickMenu(QtGui.QMenu):
# self.fracPosSpin.blockSignals(True)
# self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick()))
# self.fracPosSpin.blockSignals(False)

View File

@ -98,8 +98,9 @@ class GraphicsItem(object):
Extends deviceTransform to automatically determine the viewportTransform.
"""
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
scaler = self._exportOpts.get('resolutionScale', 1.0)
return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1)
if viewportTransform is None:
view = self.getViewWidget()
if view is None:

View File

@ -167,6 +167,8 @@ class GraphicsLayout(GraphicsWidget):
def clear(self):
for i in list(self.items.keys()):
self.removeItem(i)
self.currentRow = 0
self.currentCol = 0
def setContentsMargins(self, *args):
# Wrap calls to layout. This should happen automatically, but there

View File

@ -3,6 +3,7 @@ from .UIGraphicsItem import *
import numpy as np
from ..Point import Point
from .. import functions as fn
from .. import getConfigOption
__all__ = ['GridItem']
class GridItem(UIGraphicsItem):
@ -12,16 +13,75 @@ class GridItem(UIGraphicsItem):
Displays a rectangular grid of lines indicating major divisions within a coordinate system.
Automatically determines what divisions to use.
"""
def __init__(self):
def __init__(self, pen='default', textPen='default'):
UIGraphicsItem.__init__(self)
#QtGui.QGraphicsItem.__init__(self, *args)
#self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape)
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.opts = {}
self.setPen(pen)
self.setTextPen(textPen)
self.setTickSpacing(x=[None, None, None], y=[None, None, None])
def setPen(self, *args, **kwargs):
"""Set the pen used to draw the grid."""
if kwargs == {} and (args == () or args == ('default',)):
self.opts['pen'] = fn.mkPen(getConfigOption('foreground'))
else:
self.opts['pen'] = fn.mkPen(*args, **kwargs)
self.picture = None
self.update()
def setTextPen(self, *args, **kwargs):
"""Set the pen used to draw the texts."""
if kwargs == {} and (args == () or args == ('default',)):
self.opts['textPen'] = fn.mkPen(getConfigOption('foreground'))
else:
if args == (None,):
self.opts['textPen'] = None
else:
self.opts['textPen'] = fn.mkPen(*args, **kargs)
self.picture = None
self.update()
def setTickSpacing(self, x=None, y=None):
"""
Set the grid tick spacing to use.
Tick spacing for each axis shall be specified as an array of
descending values, one for each tick scale. When the value
is set to None, grid line distance is chosen automatically
for this particular level.
Example:
Default setting of 3 scales for each axis:
setTickSpacing(x=[None, None, None], y=[None, None, None])
Single scale with distance of 1.0 for X axis, Two automatic
scales for Y axis:
setTickSpacing(x=[1.0], y=[None, None])
Single scale with distance of 1.0 for X axis, Two scales
for Y axis, one with spacing of 1.0, other one automatic:
setTickSpacing(x=[1.0], y=[1.0, None])
"""
self.opts['tickSpacing'] = (x or self.opts['tickSpacing'][0],
y or self.opts['tickSpacing'][1])
self.grid_depth = max([len(s) for s in self.opts['tickSpacing']])
self.picture = None
self.update()
def viewRangeChanged(self):
UIGraphicsItem.viewRangeChanged(self)
self.picture = None
@ -48,7 +108,6 @@ class GridItem(UIGraphicsItem):
p = QtGui.QPainter()
p.begin(self.picture)
dt = fn.invertQTransform(self.viewTransform())
vr = self.getViewWidget().rect()
unit = self.pixelWidth(), self.pixelHeight()
dim = [vr.width(), vr.height()]
@ -62,10 +121,22 @@ class GridItem(UIGraphicsItem):
x = ul[1]
ul[1] = br[1]
br[1] = x
for i in [2,1,0]: ## Draw three different scales of grid
lastd = [None, None]
for i in range(self.grid_depth - 1, -1, -1):
dist = br-ul
nlTarget = 10.**i
d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5)
for ax in range(0,2):
ts = self.opts['tickSpacing'][ax]
try:
if ts[i] is not None:
d[ax] = ts[i]
except IndexError:
pass
lastd[ax] = d[ax]
ul1 = np.floor(ul / d) * d
br1 = np.ceil(br / d) * d
dist = br1-ul1
@ -76,12 +147,25 @@ class GridItem(UIGraphicsItem):
#print " d", d
#print " nl", nl
for ax in range(0,2): ## Draw grid for both axes
if i >= len(self.opts['tickSpacing'][ax]):
continue
if d[ax] < lastd[ax]:
continue
ppl = dim[ax] / nl[ax]
c = np.clip(3.*(ppl-3), 0., 30.)
linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c))
textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2))
#linePen.setCosmetic(True)
#linePen.setWidth(1)
c = np.clip(5.*(ppl-3), 0., 50.)
linePen = self.opts['pen']
lineColor = self.opts['pen'].color()
lineColor.setAlpha(c)
linePen.setColor(lineColor)
textPen = self.opts['textPen']
if textPen is not None:
textColor = self.opts['textPen'].color()
textColor.setAlpha(c * 2)
textPen.setColor(textColor)
bx = (ax+1) % 2
for x in range(0, int(nl[ax])):
linePen.setCosmetic(False)
@ -102,8 +186,7 @@ class GridItem(UIGraphicsItem):
if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]):
continue
p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1]))
if i < 2:
p.setPen(textPen)
if i < 2 and textPen is not None:
if ax == 0:
x = p1[0] + unit[0]
y = ul[1] + unit[1] * 8.
@ -114,7 +197,13 @@ class GridItem(UIGraphicsItem):
tr = self.deviceTransform()
#tr.scale(1.5, 1.5)
p.setWorldTransform(fn.invertQTransform(tr))
for t in texts:
x = tr.map(t[0]) + Point(0.5, 0.5)
p.drawText(x, t[1])
if textPen is not None and len(texts) > 0:
# if there is at least one text, then c is set
textColor.setAlpha(c * 2)
p.setPen(QtGui.QPen(textColor))
for t in texts:
x = tr.map(t[0]) + Point(0.5, 0.5)
p.drawText(x, t[1])
p.end()

View File

@ -488,7 +488,7 @@ class ImageItem(GraphicsObject):
step = (step, step)
stepData = self.image[::step[0], ::step[1]]
if 'auto' == bins:
if isinstance(bins, str) and bins == 'auto':
mn = np.nanmin(stepData)
mx = np.nanmax(stepData)
if mx == mn:

View File

@ -314,8 +314,8 @@ class InfiniteLine(GraphicsObject):
length = br.width()
left = br.left() + length * self.span[0]
right = br.left() + length * self.span[1]
br.setLeft(left - w)
br.setRight(right + w)
br.setLeft(left)
br.setRight(right)
br = br.normalized()
vs = self.getViewBox().size()

View File

@ -8,6 +8,7 @@ from .PlotDataItem import PlotDataItem
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
__all__ = ['LegendItem']
class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
"""
Displays a legend used for describing the contents of a plot.
@ -19,47 +20,120 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
legend.setParentItem(plotItem)
"""
def __init__(self, size=None, offset=None):
def __init__(self, size=None, offset=None, horSpacing=25, verSpacing=0, pen=None,
brush=None, labelTextColor=None, **kwargs):
"""
============== ===============================================================
**Arguments:**
size Specifies the fixed size (width, height) of the legend. If
this argument is omitted, the legend will autimatically resize
this argument is omitted, the legend will automatically resize
to fit its contents.
offset Specifies the offset position relative to the legend's parent.
Positive values offset from the left or top; negative values
offset from the right or bottom. If offset is None, the
legend must be anchored manually by calling anchor() or
positioned by calling setPos().
horSpacing Specifies the spacing between the line symbol and the label.
verSpacing Specifies the spacing between individual entries of the legend
vertically. (Can also be negative to have them really close)
pen Pen to use when drawing legend border. Any single argument
accepted by :func:`mkPen <pyqtgraph.mkPen>` is allowed.
brush QBrush to use as legend background filling. Any single argument
accepted by :func:`mkBrush <pyqtgraph.mkBrush>` is allowed.
labelTextColor Pen to use when drawing legend text. Any single argument
accepted by :func:`mkPen <pyqtgraph.mkPen>` is allowed.
============== ===============================================================
"""
GraphicsWidget.__init__(self)
GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemIgnoresTransformations)
self.layout = QtGui.QGraphicsGridLayout()
self.layout.setVerticalSpacing(verSpacing)
self.layout.setHorizontalSpacing(horSpacing)
self.setLayout(self.layout)
self.items = []
self.size = size
self.offset = offset
if size is not None:
self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1]))
self.opts = {
'pen': fn.mkPen(pen),
'brush': fn.mkBrush(brush),
'labelTextColor': labelTextColor,
'offset': offset,
}
self.opts.update(kwargs)
def offset(self):
return self.opts['offset']
def setOffset(self, offset):
self.opts['offset'] = offset
offset = Point(self.opts['offset'])
anchorx = 1 if offset[0] <= 0 else 0
anchory = 1 if offset[1] <= 0 else 0
anchor = (anchorx, anchory)
self.anchor(itemPos=anchor, parentPos=anchor, offset=offset)
def pen(self):
return self.opts['pen']
def setPen(self, *args, **kargs):
"""
Sets the pen used to draw lines between points.
*pen* can be a QPen or any argument accepted by
:func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>`
"""
pen = fn.mkPen(*args, **kargs)
self.opts['pen'] = pen
self.paint()
def brush(self):
return self.opts['brush']
def setBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['brush'] == brush:
return
self.opts['brush'] = brush
self.paint()
def labelTextColor(self):
return self.opts['labelTextColor']
def setLabelTextColor(self, *args, **kargs):
"""
Sets the color of the label text.
*pen* can be a QPen or any argument accepted by
:func:`pyqtgraph.mkColor() <pyqtgraph.mkPen>`
"""
self.opts['labelTextColor'] = fn.mkColor(*args, **kargs)
for sample, label in self.items:
label.setAttr('color', self.opts['labelTextColor'])
self.paint()
def setParentItem(self, p):
ret = GraphicsWidget.setParentItem(self, p)
if self.offset is not None:
offset = Point(self.offset)
offset = Point(self.opts['offset'])
anchorx = 1 if offset[0] <= 0 else 0
anchory = 1 if offset[1] <= 0 else 0
anchor = (anchorx, anchory)
self.anchor(itemPos=anchor, parentPos=anchor, offset=offset)
return ret
def addItem(self, item, name):
"""
Add a new entry to the legend.
Add a new entry to the legend.
============== ========================================================
**Arguments:**
@ -70,36 +144,45 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
title The title to display for this item. Simple HTML allowed.
============== ========================================================
"""
label = LabelItem(name)
label = LabelItem(name, color=self.opts['labelTextColor'], justify='left')
if isinstance(item, ItemSample):
sample = item
else:
sample = ItemSample(item)
sample = ItemSample(item)
row = self.layout.rowCount()
self.items.append((sample, label))
self.layout.addItem(sample, row, 0)
self.layout.addItem(label, row, 1)
self.updateSize()
def removeItem(self, item):
"""
Removes one item from the legend.
Removes one item from the legend.
============== ========================================================
**Arguments:**
item The item to remove or its name.
============== ========================================================
"""
# Thanks, Ulrich!
# cycle for a match
for sample, label in self.items:
if sample.item is item or label.text == item:
self.items.remove( (sample, label) ) # remove from itemlist
self.items.remove((sample, label)) # remove from itemlist
self.layout.removeItem(sample) # remove from layout
sample.close() # remove from drawing
self.layout.removeItem(label)
label.close()
self.updateSize() # redraq box
return # return after first match
def clear(self):
"""Removes all items from legend."""
for sample, label in self.items:
self.layout.removeItem(sample)
self.layout.removeItem(label)
self.items = []
self.updateSize()
def clear(self):
"""
@ -113,29 +196,20 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
def updateSize(self):
if self.size is not None:
return
height = 0
width = 0
#print("-------")
for sample, label in self.items:
height += max(sample.height(), label.height()) + 3
width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() +
label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width()))
#print(width, height)
#print width, height
self.setGeometry(0, 0, width+25, height)
self.setGeometry(0, 0, 0, 0)
def boundingRect(self):
return QtCore.QRectF(0, 0, self.width(), self.height())
def paint(self, p, *args):
p.setPen(fn.mkPen(255,255,255,100))
p.setBrush(fn.mkBrush(100,100,100,50))
p.setPen(self.opts['pen'])
p.setBrush(self.opts['brush'])
p.drawRect(self.boundingRect())
def hoverEvent(self, ev):
ev.acceptDrags(QtCore.Qt.LeftButton)
def mouseDragEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
ev.accept()
@ -145,42 +219,39 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
class ItemSample(GraphicsWidget):
""" Class responsible for drawing a single item in a LegendItem (sans label).
This may be subclassed to draw custom graphics in a Legend.
"""
## Todo: make this more generic; let each item decide how it should be represented.
def __init__(self, item):
GraphicsWidget.__init__(self)
self.item = item
def boundingRect(self):
return QtCore.QRectF(0, 0, 20, 20)
def paint(self, p, *args):
#p.setRenderHint(p.Antialiasing) # only if the data is antialiased.
opts = self.item.opts
if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None:
p.setBrush(fn.mkBrush(opts['fillBrush']))
p.setPen(fn.mkPen(None))
p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)]))
if opts['antialias']:
p.setRenderHint(p.Antialiasing)
if not isinstance(self.item, ScatterPlotItem):
p.setPen(fn.mkPen(opts['pen']))
p.drawLine(2, 18, 18, 2)
p.drawLine(0, 11, 20, 11)
symbol = opts.get('symbol', None)
if symbol is not None:
if isinstance(self.item, PlotDataItem):
opts = self.item.scatter.opts
pen = fn.mkPen(opts['pen'])
brush = fn.mkBrush(opts['brush'])
size = opts['size']
p.translate(10,10)
p.translate(10, 10)
path = drawSymbol(p, symbol, size, pen, brush)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
try:
from ..Qt import QtOpenGL
@ -272,7 +273,7 @@ class PlotCurveItem(GraphicsObject):
self.update()
def setShadowPen(self, *args, **kargs):
"""Set the shadow pen used to draw behind tyhe primary pen.
"""Set the shadow pen used to draw behind the primary pen.
This pen must have a larger width than the primary
pen to be visible.
"""
@ -292,7 +293,7 @@ class PlotCurveItem(GraphicsObject):
self.fillPath = None
self.invalidateBounds()
self.update()
def setData(self, *args, **kargs):
"""
=============== ========================================================
@ -357,7 +358,7 @@ class PlotCurveItem(GraphicsObject):
kargs[k] = data
if not isinstance(data, np.ndarray) or data.ndim > 1:
raise Exception("Plot data must be 1D ndarray.")
if 'complex' in str(data.dtype):
if data.dtype.kind == 'c':
raise Exception("Can not plot complex data types.")
profiler("data checks")
@ -570,7 +571,7 @@ class PlotCurveItem(GraphicsObject):
gl.glEnable(gl.GL_BLEND)
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1])
gl.glDrawArrays(gl.GL_LINE_STRIP, 0, int(pos.size / pos.shape[-1]))
finally:
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
finally:
@ -638,4 +639,3 @@ class ROIPlotItem(PlotCurveItem):
def roiChangedEvent(self):
d = self.getRoiData()
self.updateData(d, self.xVals)

View File

@ -563,8 +563,8 @@ class PlotItem(GraphicsWidget):
if item in self.dataItems:
self.dataItems.remove(item)
if item.scene() is not None:
self.vb.removeItem(item)
self.vb.removeItem(item)
if item in self.curves:
self.curves.remove(item)
self.updateDecimation()
@ -677,7 +677,6 @@ class PlotItem(GraphicsWidget):
xRange = rect.left(), rect.right()
svg = ""
fh = open(fileName, 'w')
dx = max(rect.right(),0) - min(rect.left(),0)
ymn = min(rect.top(), rect.bottom())
@ -691,52 +690,68 @@ class PlotItem(GraphicsWidget):
sy *= 1000
sy *= -1
fh.write('<svg>\n')
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M%f,0 L%f,0"/>\n' % (rect.left()*sx, rect.right()*sx))
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M0,%f L0,%f"/>\n' % (rect.top()*sy, rect.bottom()*sy))
with open(fileName, 'w') as fh:
# fh.write('<svg viewBox="%f %f %f %f">\n' % (rect.left() * sx,
# rect.top() * sx,
# rect.width() * sy,
# rect.height()*sy))
fh.write('<svg>\n')
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" '
'stroke-width="1" d="M%f,0 L%f,0"/>\n' % (
rect.left() * sx, rect.right() * sx))
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" '
'stroke-width="1" d="M0,%f L0,%f"/>\n' % (
rect.top() * sy, rect.bottom() * sy))
for item in self.curves:
if isinstance(item, PlotCurveItem):
color = fn.colorStr(item.pen.color())
opacity = item.pen.color().alpha() / 255.
color = color[:6]
x, y = item.getData()
mask = (x > xRange[0]) * (x < xRange[1])
mask[:-1] += mask[1:]
m2 = mask.copy()
mask[1:] += m2[:-1]
x = x[mask]
y = y[mask]
x *= sx
y *= sy
fh.write('<path fill="none" stroke="#%s" stroke-opacity="%f" stroke-width="1" d="M%f,%f ' % (color, opacity, x[0], y[0]))
for i in range(1, len(x)):
fh.write('L%f,%f ' % (x[i], y[i]))
fh.write('"/>')
for item in self.dataItems:
if isinstance(item, ScatterPlotItem):
pRect = item.boundingRect()
vRect = pRect.intersected(rect)
for point in item.points():
pos = point.pos()
if not rect.contains(pos):
continue
color = fn.colorStr(point.brush.color())
opacity = point.brush.color().alpha() / 255.
for item in self.curves:
if isinstance(item, PlotCurveItem):
color = fn.colorStr(item.pen.color())
opacity = item.pen.color().alpha() / 255.
color = color[:6]
x = pos.x() * sx
y = pos.y() * sy
fh.write('<circle cx="%f" cy="%f" r="1" fill="#%s" stroke="none" fill-opacity="%f"/>\n' % (x, y, color, opacity))
fh.write("</svg>\n")
x, y = item.getData()
mask = (x > xRange[0]) * (x < xRange[1])
mask[:-1] += mask[1:]
m2 = mask.copy()
mask[1:] += m2[:-1]
x = x[mask]
y = y[mask]
x *= sx
y *= sy
# fh.write('<g fill="none" stroke="#%s" '
# 'stroke-opacity="1" stroke-width="1">\n' % (
# color, ))
fh.write('<path fill="none" stroke="#%s" '
'stroke-opacity="%f" stroke-width="1" '
'd="M%f,%f ' % (color, opacity, x[0], y[0]))
for i in range(1, len(x)):
fh.write('L%f,%f ' % (x[i], y[i]))
fh.write('"/>')
# fh.write("</g>")
for item in self.dataItems:
if isinstance(item, ScatterPlotItem):
pRect = item.boundingRect()
vRect = pRect.intersected(rect)
for point in item.points():
pos = point.pos()
if not rect.contains(pos):
continue
color = fn.colorStr(point.brush.color())
opacity = point.brush.color().alpha() / 255.
color = color[:6]
x = pos.x() * sx
y = pos.y() * sy
fh.write('<circle cx="%f" cy="%f" r="1" fill="#%s" '
'stroke="none" fill-opacity="%f"/>\n' % (
x, y, color, opacity))
fh.write("</svg>\n")
def writeSvg(self, fileName=None):
if fileName is None:
self._chooseFilenameDialog(handler=self.writeSvg)
@ -766,22 +781,21 @@ class PlotItem(GraphicsWidget):
fileName = str(fileName)
PlotItem.lastFileDir = os.path.dirname(fileName)
fd = open(fileName, 'w')
data = [c.getData() for c in self.curves]
i = 0
while True:
done = True
for d in data:
if i < len(d[0]):
fd.write('%g,%g,'%(d[0][i], d[1][i]))
done = False
else:
fd.write(' , ,')
fd.write('\n')
if done:
break
i += 1
fd.close()
with open(fileName, 'w') as fd:
i = 0
while True:
done = True
for d in data:
if i < len(d[0]):
fd.write('%g,%g,' % (d[0][i], d[1][i]))
done = False
else:
fd.write(' , ,')
fd.write('\n')
if done:
break
i += 1
def saveState(self):
state = self.stateGroup.state()

View File

@ -781,7 +781,7 @@ class ScatterPlotItem(GraphicsObject):
pts = pts[:,viewMask]
for i, rec in enumerate(data):
p.resetTransform()
p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width'])
p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2)
drawSymbol(p, *self.getSpotOpts(rec, scale))
else:
if self.picture is None:

View File

@ -110,9 +110,16 @@ class TextItem(GraphicsObject):
self.updateTextPos()
def setAngle(self, angle):
"""
Set the angle of the text in degrees.
This sets the rotation angle of the text as a whole, measured
counter-clockwise from the x axis of the parent. Note that this rotation
angle does not depend on horizontal/vertical scaling of the parent.
"""
self.angle = angle
self.updateTransform()
self.updateTransform(force=True)
def setAnchor(self, anchor):
self.anchor = Point(anchor)
self.updateTextPos()
@ -169,7 +176,7 @@ class TextItem(GraphicsObject):
p.setRenderHint(p.Antialiasing, True)
p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect()))
def updateTransform(self):
def updateTransform(self, force=False):
# update transform such that this item has the correct orientation
# and scaling relative to the scene, but inherits its position from its
# parent.
@ -181,7 +188,7 @@ class TextItem(GraphicsObject):
else:
pt = p.sceneTransform()
if pt == self._lastTransform:
if not force and pt == self._lastTransform:
return
t = pt.inverted()[0]

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
import weakref
import sys
from copy import deepcopy
import numpy as np
from ...Qt import QtGui, QtCore
from ...python2_3 import sortList, basestring, cmp
from ...python2_3 import basestring
from ...Point import Point
from ... import functions as fn
from .. ItemGroup import ItemGroup
@ -399,10 +400,12 @@ class ViewBox(GraphicsWidget):
"""
if item.zValue() < self.zValue():
item.setZValue(self.zValue()+1)
scene = self.scene()
if scene is not None and scene is not item.scene():
scene.addItem(item) ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
item.setParentItem(self.childGroup)
if not ignoreBounds:
self.addedItems.append(item)
self.updateAutoRange()
@ -413,7 +416,12 @@ class ViewBox(GraphicsWidget):
self.addedItems.remove(item)
except:
pass
self.scene().removeItem(item)
scene = self.scene()
if scene is not None:
scene.removeItem(item)
item.setParentItem(None)
self.updateAutoRange()
def clear(self):
@ -1596,16 +1604,13 @@ class ViewBox(GraphicsWidget):
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
def view_key(view):
return (view.window() is self.window(), view.name)
## make a sorted list of all named views
nv = list(ViewBox.NamedViews.values())
sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList
nv = sorted(ViewBox.NamedViews.values(), key=view_key)
if self in nv:
nv.remove(self)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from ...Qt import QtCore, QtGui, QT_LIB
from ...python2_3 import asUnicode
from ...WidgetGroup import WidgetGroup
@ -48,8 +49,8 @@ class ViewBoxMenu(QtGui.QMenu):
connects = [
(ui.mouseCheck.toggled, 'MouseToggled'),
(ui.manualRadio.clicked, 'ManualClicked'),
(ui.minText.editingFinished, 'MinTextChanged'),
(ui.maxText.editingFinished, 'MaxTextChanged'),
(ui.minText.editingFinished, 'RangeTextChanged'),
(ui.maxText.editingFinished, 'RangeTextChanged'),
(ui.autoRadio.clicked, 'AutoClicked'),
(ui.autoPercentSpin.valueChanged, 'AutoSpinChanged'),
(ui.linkCombo.currentIndexChanged, 'LinkComboChanged'),
@ -162,14 +163,10 @@ class ViewBoxMenu(QtGui.QMenu):
def xManualClicked(self):
self.view().enableAutoRange(ViewBox.XAxis, False)
def xMinTextChanged(self):
def xRangeTextChanged(self):
self.ctrl[0].manualRadio.setChecked(True)
self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
self.view().setXRange(*self._validateRangeText(0), padding=0)
def xMaxTextChanged(self):
self.ctrl[0].manualRadio.setChecked(True)
self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
def xAutoClicked(self):
val = self.ctrl[0].autoPercentSpin.value() * 0.01
self.view().enableAutoRange(ViewBox.XAxis, val)
@ -194,13 +191,9 @@ class ViewBoxMenu(QtGui.QMenu):
def yManualClicked(self):
self.view().enableAutoRange(ViewBox.YAxis, False)
def yMinTextChanged(self):
def yRangeTextChanged(self):
self.ctrl[1].manualRadio.setChecked(True)
self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
def yMaxTextChanged(self):
self.ctrl[1].manualRadio.setChecked(True)
self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
self.view().setYRange(*self._validateRangeText(1), padding=0)
def yAutoClicked(self):
val = self.ctrl[1].autoPercentSpin.value() * 0.01
@ -265,6 +258,20 @@ class ViewBoxMenu(QtGui.QMenu):
if changed:
c.setCurrentIndex(0)
c.currentIndexChanged.emit(c.currentIndex())
def _validateRangeText(self, axis):
"""Validate range text inputs. Return current value(s) if invalid."""
inputs = (self.ctrl[axis].minText.text(),
self.ctrl[axis].maxText.text())
vals = self.view().viewRange()[axis]
for i, text in enumerate(inputs):
try:
vals[i] = float(text)
except ValueError:
# could not convert string to float
pass
return vals
from .ViewBox import ViewBox

View File

@ -71,6 +71,8 @@ def test_ViewBox():
size1 = QRectF(0, h, w, -h)
assertMapping(vb, view1, size1)
win.close()
skipreason = "Skipping this test until someone has time to fix it."
@pytest.mark.skipif(True, reason=skipreason)

View File

@ -28,3 +28,5 @@ def test_AxisItem_stopAxisAtTick(monkeypatch):
monkeypatch.setattr(left, "drawPicture", test_left)
plot.show()
app.processEvents()
plot.close()

View File

@ -35,3 +35,5 @@ def test_ErrorBarItem_defer_data():
r_clear_ebi = plot.viewRect()
assert r_clear_ebi == r_no_ebi
plot.close()

View File

@ -27,7 +27,8 @@ def test_PlotCurveItem():
c.setData(data, connect=np.array([1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,0]))
assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.")
p.close()
if __name__ == '__main__':

View File

@ -86,3 +86,5 @@ def test_clipping():
assert xDisp[0] <= vr.left()
assert xDisp[-1] >= vr.right()
w.close()

View File

@ -0,0 +1,23 @@
import pytest
import pyqtgraph as pg
app = pg.mkQApp()
def test_TextItem_setAngle():
plt = pg.plot()
plt.setXRange(-10, 10)
plt.setYRange(-20, 20)
item = pg.TextItem(text="test")
plt.addItem(item)
t1 = item.transform()
item.setAngle(30)
app.processEvents()
t2 = item.transform()
assert t1 != t2
assert not t1.isRotating()
assert t2.isRotating()

View File

@ -45,10 +45,7 @@ class TabWindow(QtGui.QMainWindow):
self.show()
def __getattr__(self, attr):
if hasattr(self.cw, attr):
return getattr(self.cw, attr)
else:
raise NameError(attr)
return getattr(self.cw, attr)
class PlotWindow(PlotWidget):

View File

@ -12,10 +12,9 @@ More info at http://www.scipy.org/Cookbook/MetaArray
import types, copy, threading, os, re
import pickle
from functools import reduce
import numpy as np
from ..python2_3 import basestring
#import traceback
## By default, the library will use HDF5 when writing files.
## This can be overridden by setting USE_HDF5 = False
@ -103,7 +102,7 @@ class MetaArray(object):
since the actual values are described (name and units) in the column info for the first axis.
"""
version = '2'
version = u'2'
# Default hdf5 compression to use when writing
# 'gzip' is widely available and somewhat slow
@ -358,9 +357,12 @@ class MetaArray(object):
else:
return np.array(self._data)
def __array__(self):
def __array__(self, dtype=None):
## supports np.array(metaarray_instance)
return self.asarray()
if dtype is None:
return self.asarray()
else:
return self.asarray().astype(dtype)
def view(self, typ):
## deprecated; kept for backward compatibility
@ -741,7 +743,7 @@ class MetaArray(object):
## decide which read function to use
with open(filename, 'rb') as fd:
magic = fd.read(8)
if magic == '\x89HDF\r\n\x1a\n':
if magic == b'\x89HDF\r\n\x1a\n':
fd.close()
self._readHDF5(filename, **kwargs)
self._isHDF = True
@ -766,7 +768,7 @@ class MetaArray(object):
"""Read meta array from the top of a file. Read lines until a blank line is reached.
This function should ideally work for ALL versions of MetaArray.
"""
meta = ''
meta = u''
## Read meta information until the first blank line
while True:
line = fd.readline().strip()
@ -776,7 +778,7 @@ class MetaArray(object):
ret = eval(meta)
#print ret
return ret
def _readData1(self, fd, meta, mmap=False, **kwds):
## Read array data from the file descriptor for MetaArray v1 files
## read in axis values for any axis that specifies a length
@ -844,7 +846,7 @@ class MetaArray(object):
frames = []
frameShape = list(meta['shape'])
frameShape[dynAxis] = 1
frameSize = reduce(lambda a,b: a*b, frameShape)
frameSize = np.prod(frameShape)
n = 0
while True:
## Extract one non-blank line
@ -886,10 +888,8 @@ class MetaArray(object):
newSubset = list(subset[:])
newSubset[dynAxis] = slice(dStart, dStop)
if dStop > dStart:
#print n, data.shape, " => ", newSubset, data[tuple(newSubset)].shape
frames.append(data[tuple(newSubset)].copy())
else:
#data = data[subset].copy() ## what's this for??
frames.append(data)
n += inf['numFrames']
@ -900,12 +900,8 @@ class MetaArray(object):
ax['values'] = np.array(xVals, dtype=ax['values_type'])
del ax['values_len']
del ax['values_type']
#subarr = subarr.view(subtype)
#subarr._info = meta['info']
self._info = meta['info']
self._data = subarr
#raise Exception() ## stress-testing
#return subarr
def _readHDF5(self, fileName, readAllData=None, writable=False, **kargs):
if 'close' in kargs and readAllData is None: ## for backward compatibility
@ -935,6 +931,10 @@ class MetaArray(object):
f = h5py.File(fileName, mode)
ver = f.attrs['MetaArray']
try:
ver = ver.decode('utf-8')
except:
pass
if ver > MetaArray.version:
print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version)))
meta = MetaArray.readHDF5Meta(f['info'])
@ -964,11 +964,6 @@ class MetaArray(object):
ma = MetaArray._h5py_metaarray.MetaArray(file=fileName)
self._data = ma.asarray()._getValue()
self._info = ma._info._getValue()
#print MetaArray._hdf5Process
#import inspect
#print MetaArray, id(MetaArray), inspect.getmodule(MetaArray)
@staticmethod
def mapHDF5Array(data, writable=False):
@ -980,9 +975,6 @@ class MetaArray(object):
if off is None:
raise Exception("This dataset uses chunked storage; it can not be memory-mapped. (store using mappable=True)")
return np.memmap(filename=data.file.filename, offset=off, dtype=data.dtype, shape=data.shape, mode=mode)
@staticmethod
def readHDF5Meta(root, mmap=False):
@ -991,6 +983,8 @@ class MetaArray(object):
## Pull list of values from attributes and child objects
for k in root.attrs:
val = root.attrs[k]
if isinstance(val, bytes):
val = val.decode()
if isinstance(val, basestring): ## strings need to be re-evaluated to their original types
try:
val = eval(val)
@ -1011,6 +1005,10 @@ class MetaArray(object):
data[k] = val
typ = root.attrs['_metaType_']
try:
typ = typ.decode('utf-8')
except:
pass
del data['_metaType_']
if typ == 'dict':
@ -1024,7 +1022,6 @@ class MetaArray(object):
return d2
else:
raise Exception("Don't understand metaType '%s'" % typ)
def write(self, fileName, **opts):
"""Write this object to a file. The object can be restored by calling MetaArray(file=fileName)
@ -1033,12 +1030,13 @@ class MetaArray(object):
appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis.
compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc.
chunks: bool or tuple specifying chunk shape
"""
if USE_HDF5 and HAVE_HDF5:
"""
if USE_HDF5 is False:
return self.writeMa(fileName, **opts)
elif HAVE_HDF5 is True:
return self.writeHDF5(fileName, **opts)
else:
return self.writeMa(fileName, **opts)
raise Exception("h5py is required for writing .ma hdf5 files, but it could not be imported.")
def writeMeta(self, fileName):
"""Used to re-write meta info to the given file.
@ -1051,7 +1049,6 @@ class MetaArray(object):
self.writeHDF5Meta(f, 'info', self._info)
f.close()
def writeHDF5(self, fileName, **opts):
## default options for writing datasets
comp = self.defaultCompression
@ -1087,8 +1084,7 @@ class MetaArray(object):
## update options if they were passed in
for k in dsOpts:
if k in opts:
dsOpts[k] = opts[k]
dsOpts[k] = opts[k]
## If mappable is in options, it disables chunking/compression
if opts.get('mappable', False):
@ -1298,7 +1294,7 @@ class MetaArray(object):
#frames = []
#frameShape = list(meta['shape'])
#frameShape[dynAxis] = 1
#frameSize = reduce(lambda a,b: a*b, frameShape)
#frameSize = np.prod(frameShape)
#n = 0
#while True:
### Extract one non-blank line

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os, sys, time, multiprocessing, re
from .processes import ForkedProcess
from .remoteproxy import ClosedError
@ -213,14 +214,14 @@ class Parallelize(object):
try:
cores = {}
pid = None
for line in open('/proc/cpuinfo'):
m = re.match(r'physical id\s+:\s+(\d+)', line)
if m is not None:
pid = m.groups()[0]
m = re.match(r'cpu cores\s+:\s+(\d+)', line)
if m is not None:
cores[pid] = int(m.groups()[0])
with open('/proc/cpuinfo') as fd:
for line in fd:
m = re.match(r'physical id\s+:\s+(\d+)', line)
if m is not None:
pid = m.groups()[0]
m = re.match(r'cpu cores\s+:\s+(\d+)', line)
if m is not None:
cores[pid] = int(m.groups()[0])
return sum(cores.values())
except:
return multiprocessing.cpu_count()

View File

@ -1,22 +0,0 @@
try:
import numpy as np
## Wrap np.concatenate to catch and avoid a segmentation fault bug
## (numpy trac issue #2084)
if not hasattr(np, 'concatenate_orig'):
np.concatenate_orig = np.concatenate
def concatenate(vals, *args, **kwds):
"""Wrapper around numpy.concatenate (see pyqtgraph/numpy_fix.py)"""
dtypes = [getattr(v, 'dtype', None) for v in vals]
names = [getattr(dt, 'names', None) for dt in dtypes]
if len(dtypes) < 2 or all([n is None for n in names]):
return np.concatenate_orig(vals, *args, **kwds)
if any([dt != dtypes[0] for dt in dtypes[1:]]):
raise TypeError("Cannot concatenate structured arrays of different dtype.")
return np.concatenate_orig(vals, *args, **kwds)
np.concatenate = concatenate
except ImportError:
pass

View File

@ -8,7 +8,7 @@ class GLBarGraphItem(GLMeshItem):
pos is (...,3) array of the bar positions (the corner of each bar)
size is (...,3) array of the sizes of each bar
"""
nCubes = reduce(lambda a,b: a*b, pos.shape[:-1])
nCubes = np.prod(pos.shape[:-1])
cubeVerts = np.mgrid[0:2,0:2,0:2].reshape(3,8).transpose().reshape(1,8,3)
cubeFaces = np.array([
[0,1,2], [3,2,1],
@ -22,8 +22,5 @@ class GLBarGraphItem(GLMeshItem):
verts = cubeVerts * size + pos
faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1)
md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3))
GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False)
GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False)

View File

@ -61,7 +61,7 @@ class GLScatterPlotItem(GLGraphicsItem):
## Generate texture for rendering points
w = 64
def fn(x,y):
r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5
r = ((x-(w-1)/2.)**2 + (y-(w-1)/2.)**2) ** 0.5
return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.))
pData = np.empty((w, w, 4))
pData[:] = 255
@ -123,7 +123,7 @@ class GLScatterPlotItem(GLGraphicsItem):
try:
pos = self.pos
#if pos.ndim > 2:
#pos = pos.reshape((reduce(lambda a,b: a*b, pos.shape[:-1]), pos.shape[-1]))
#pos = pos.reshape((-1, pos.shape[-1]))
glVertexPointerf(pos)
if isinstance(self.color, np.ndarray):

View File

@ -612,7 +612,10 @@ class ActionParameterItem(ParameterItem):
self.layout = QtGui.QHBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layoutWidget.setLayout(self.layout)
self.button = QtGui.QPushButton(param.name())
title = param.opts.get('title', None)
if title is None:
title = param.name()
self.button = QtGui.QPushButton(title)
#self.layout.addSpacing(100)
self.layout.addWidget(self.button)
self.layout.addStretch()

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import numpy as np
from PyQt4 import QtGui
import os, pickle, sys
@ -14,6 +15,5 @@ for f in os.listdir(path):
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2)
pixmaps[f] = pickle.dumps(arr)
ver = sys.version_info[0]
fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w')
fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps))
with open(os.path.join(path, 'pixmapData_%d.py' % (ver, )), 'w') as fh:
fh.write("import numpy as np; pixmapData=%s" % (repr(pixmaps), ))

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
Helper functions that smooth out the differences between python 2 and 3.
"""
@ -13,46 +14,12 @@ def asUnicode(x):
return unicode(x)
else:
return str(x)
def cmpToKey(mycmp):
'Convert a cmp= function into a key= function'
class K(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K
def sortList(l, cmpFunc):
if sys.version_info[0] == 2:
l.sort(cmpFunc)
else:
l.sort(key=cmpToKey(cmpFunc))
if sys.version_info[0] == 3:
basestring = str
def cmp(a,b):
if a>b:
return 1
elif b > a:
return -1
else:
return 0
xrange = range
else:
import __builtin__
basestring = __builtin__.basestring
cmp = __builtin__.cmp
xrange = __builtin__.xrange

View File

@ -306,7 +306,8 @@ if __name__ == '__main__':
import os
if not os.path.isdir('test1'):
os.mkdir('test1')
open('test1/__init__.py', 'w')
with open('test1/__init__.py', 'w'):
pass
modFile1 = "test1/test1.py"
modCode1 = """
import sys
@ -345,8 +346,10 @@ def fn():
print("fn: %s")
"""
open(modFile1, 'w').write(modCode1%(1,1))
open(modFile2, 'w').write(modCode2%"message 1")
with open(modFile1, 'w') as f:
f.write(modCode1 % (1, 1))
with open(modFile2, 'w') as f:
f.write(modCode2 % ("message 1", ))
import test1.test1 as test1
import test2
print("Test 1 originals:")
@ -382,7 +385,8 @@ def fn():
c1.fn()
os.remove(modFile1+'c')
open(modFile1, 'w').write(modCode1%(2,2))
with open(modFile1, 'w') as f:
f.write(modCode1 %(2, 2))
print("\n----RELOAD test1-----\n")
reloadAll(os.path.abspath(__file__)[:10], debug=True)
@ -393,7 +397,8 @@ def fn():
os.remove(modFile2+'c')
open(modFile2, 'w').write(modCode2%"message 2")
with open(modFile2, 'w') as f:
f.write(modCode2 % ("message 2", ))
print("\n----RELOAD test2-----\n")
reloadAll(os.path.abspath(__file__)[:10], debug=True)
@ -429,8 +434,10 @@ def fn():
os.remove(modFile1+'c')
os.remove(modFile2+'c')
open(modFile1, 'w').write(modCode1%(3,3))
open(modFile2, 'w').write(modCode2%"message 3")
with open(modFile1, 'w') as f:
f.write(modCode1 % (3, 3))
with open(modFile2, 'w') as f:
f.write(modCode2 % ("message 3", ))
print("\n----RELOAD-----\n")
reloadAll(os.path.abspath(__file__)[:10], debug=True)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
@ -59,7 +60,8 @@ def test_exit_crash():
print(name)
argstr = initArgs.get(name, "")
open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr))
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

View File

@ -1,11 +1,15 @@
import pyqtgraph as pg
import numpy as np
import sys
from copy import deepcopy
from collections import OrderedDict
from numpy.testing import assert_array_almost_equal, assert_almost_equal
import pytest
np.random.seed(12345)
def testSolve3D():
p1 = np.array([[0,0,0,1],
[1,0,0,1],
@ -356,6 +360,29 @@ def test_eq():
assert eq(a4, a4.copy())
assert not eq(a4, a4.T)
# test containers
assert not eq({'a': 1}, {'a': 1, 'b': 2})
assert not eq({'a': 1}, {'a': 2})
d1 = {'x': 1, 'y': np.nan, 3: ['a', np.nan, a3, 7, 2.3], 4: a4}
d2 = deepcopy(d1)
assert eq(d1, d2)
assert eq(OrderedDict(d1), OrderedDict(d2))
assert not eq(OrderedDict(d1), d2)
items = list(d1.items())
assert not eq(OrderedDict(items), OrderedDict(reversed(items)))
assert not eq([1,2,3], [1,2,3,4])
l1 = [d1, np.inf, -np.inf, np.nan]
l2 = deepcopy(l1)
t1 = tuple(l1)
t2 = tuple(l2)
assert eq(l1, l2)
assert eq(t1, t2)
assert eq(set(range(10)), set(range(10)))
assert not eq(set(range(10)), set(range(9)))
if __name__ == '__main__':
test_interpolateArray()

View File

@ -411,7 +411,11 @@ class GraphicsView(QtGui.QGraphicsView):
try:
if self.parentWidget() is None and self.isVisible():
msg = "Visible window deleted. To prevent this, store a reference to the window object."
warnings.warn(msg, RuntimeWarning, stacklevel=2)
try:
warnings.warn(msg, RuntimeWarning, stacklevel=2)
except TypeError:
# warnings module not available during interpreter shutdown
pass
except RuntimeError:
pass

View File

@ -13,7 +13,7 @@ __all__ = ['HistogramLUTWidget']
class HistogramLUTWidget(GraphicsView):
def __init__(self, parent=None, *args, **kargs):
background = kargs.get('background', 'default')
background = kargs.pop('background', 'default')
GraphicsView.__init__(self, parent, useOpenGL=False, background=background)
self.item = HistogramLUTItem(*args, **kargs)
self.setCentralItem(self.item)

View File

@ -355,7 +355,8 @@ class TableWidget(QtGui.QTableWidget):
fileName = fileName[0] # Qt4/5 API difference
if fileName == '':
return
open(str(fileName), 'w').write(data)
with open(fileName, 'w') as fd:
fd.write(data)
def contextMenuEvent(self, ev):
self.contextMenu.popup(ev.globalPos())

View File

@ -1,7 +1,6 @@
from ..Qt import QtCore, QtGui
from ..ptime import time
from .. import functions as fn
from functools import reduce
__all__ = ['ValueLabel']
@ -54,7 +53,7 @@ class ValueLabel(QtGui.QLabel):
self.averageTime = t
def averageValue(self):
return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values))
return sum(v[1] for v in self.values) / float(len(self.values))
def paintEvent(self, ev):

View File

@ -0,0 +1,44 @@
"""
HistogramLUTWidget test:
Tests the creation of a HistogramLUTWidget.
"""
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
import numpy as np
def testHistogramLUTWidget():
pg.mkQApp()
win = QtGui.QMainWindow()
win.show()
cw = QtGui.QWidget()
win.setCentralWidget(cw)
l = QtGui.QGridLayout()
cw.setLayout(l)
l.setSpacing(0)
v = pg.GraphicsView()
vb = pg.ViewBox()
vb.setAspectLocked()
v.setCentralItem(vb)
l.addWidget(v, 0, 0, 3, 1)
w = pg.HistogramLUTWidget(background='w')
l.addWidget(w, 0, 1)
data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0))
for i in range(32):
for j in range(32):
data[i*8, j*8] += .1
img = pg.ImageItem(data)
vb.addItem(img)
vb.autoRange()
w.setImageItem(img)
QtGui.QApplication.processEvents()

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
DESCRIPTION = """\
PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and
PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PyQt5/PySide/PySide2 and
numpy.
It is intended for use in mathematics / scientific / engineering applications.
@ -12,14 +13,13 @@ setupOpts = dict(
name='pyqtgraph',
description='Scientific Graphics and GUI Library for Python',
long_description=DESCRIPTION,
license='MIT',
license = 'MIT',
url='http://www.pyqtgraph.org',
author='Luke Campagnola',
author_email='luke.campagnola@gmail.com',
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
@ -141,8 +141,7 @@ setup(
package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source
package_data={'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg']},
install_requires = [
'numpy',
'numpy>=1.8.0',
],
**setupOpts
)

View File

@ -10,14 +10,15 @@ except ImportError:
output = proc.stdout.read()
proc.wait()
if proc.returncode != 0:
ex = Exception("Process had nonzero return value %d" % proc.returncode)
ex = Exception("Process had nonzero return value "
+ "%d " % proc.returncode)
ex.returncode = proc.returncode
ex.output = output
raise ex
return output
# Maximum allowed repository size difference (in kB) following merge.
# This is used to prevent large files from being inappropriately added to
# This is used to prevent large files from being inappropriately added to
# the repository history.
MERGE_SIZE_LIMIT = 100
@ -42,19 +43,19 @@ FLAKE_MANDATORY = set([
'E901', # SyntaxError or IndentationError
'E902', # IOError
'W191', # indentation contains tabs
'W601', # .has_key() is deprecated, use in
'W602', # deprecated form of raising exception
'W603', # <> is deprecated, use !=
'W604', # backticks are deprecated, use repr()
'W604', # backticks are deprecated, use repr()
])
FLAKE_RECOMMENDED = set([
'E124', # closing bracket does not match visual indentation
'E231', # missing whitespace after ,
'E211', # whitespace before (
'E261', # at least two spaces before inline comment
'E271', # multiple spaces after keyword
@ -65,10 +66,10 @@ FLAKE_RECOMMENDED = set([
'F402', # import module from line N shadowed by loop variable
'F403', # from module import * used; unable to detect undefined names
'F404', # future import(s) name after other statements
'E501', # line too long (82 > 79 characters)
'E502', # the backslash is redundant between brackets
'E702', # multiple statements on one line (semicolon)
'E703', # statement ends with a semicolon
'E711', # comparison to None should be if cond is None:
@ -82,7 +83,7 @@ FLAKE_RECOMMENDED = set([
'F823', # local variable name ... referenced before assignment
'F831', # duplicate argument name in function definition
'F841', # local variable name is assigned to but never used
'W292', # no newline at end of file
])
@ -93,7 +94,7 @@ FLAKE_OPTIONAL = set([
'E126', # continuation line over-indented for hanging indent
'E127', # continuation line over-indented for visual indent
'E128', # continuation line under-indented for visual indent
'E201', # whitespace after (
'E202', # whitespace before )
'E203', # whitespace before :
@ -105,19 +106,19 @@ FLAKE_OPTIONAL = set([
'E228', # missing whitespace around modulo operator
'E241', # multiple spaces after ,
'E251', # unexpected spaces around keyword / parameter equals
'E262', # inline comment should start with #
'E262', # inline comment should start with #
'E301', # expected 1 blank line, found 0
'E302', # expected 2 blank lines, found 0
'E303', # too many blank lines (3)
'E401', # multiple imports on one line
'E701', # multiple statements on one line (colon)
'W291', # trailing whitespace
'W293', # blank line contains whitespace
'W391', # blank line at end of file
])
@ -128,23 +129,10 @@ FLAKE_IGNORE = set([
])
#def checkStyle():
#try:
#out = check_output(['flake8', '--select=%s' % FLAKE_TESTS, '--statistics', 'pyqtgraph/'])
#ret = 0
#print("All style checks OK.")
#except Exception as e:
#out = e.output
#ret = e.returncode
#print(out.decode('utf-8'))
#return ret
def checkStyle():
""" Run flake8, checking only lines that are modified since the last
git commit. """
test = [ 1,2,3 ]
# First check _all_ code against mandatory error codes
print('flake8: check all code against mandatory error set...')
errors = ','.join(FLAKE_MANDATORY)
@ -154,39 +142,47 @@ def checkStyle():
output = proc.stdout.read().decode('utf-8')
ret = proc.wait()
printFlakeOutput(output)
# Check for DOS newlines
print('check line endings in all files...')
count = 0
allowedEndings = set([None, '\n'])
for path, dirs, files in os.walk('.'):
if path.startswith("." + os.path.sep + ".tox"):
continue
for f in files:
if os.path.splitext(f)[1] not in ('.py', '.rst'):
continue
filename = os.path.join(path, f)
fh = open(filename, 'U')
x = fh.readlines()
endings = set(fh.newlines if isinstance(fh.newlines, tuple) else (fh.newlines,))
_ = fh.readlines()
endings = set(
fh.newlines
if isinstance(fh.newlines, tuple)
else (fh.newlines,)
)
endings -= allowedEndings
if len(endings) > 0:
print("\033[0;31m" + "File has invalid line endings: %s" % filename + "\033[0m")
print("\033[0;31m"
+ "File has invalid line endings: "
+ "%s" % filename + "\033[0m")
ret = ret | 2
count += 1
print('checked line endings in %d files' % count)
# Next check new code with optional error codes
print('flake8: check new code against recommended error set...')
diff = subprocess.check_output(['git', 'diff'])
proc = subprocess.Popen(['flake8', '--diff', #'--show-source',
proc = subprocess.Popen(['flake8', '--diff', # '--show-source',
'--ignore=' + errors],
stdin=subprocess.PIPE,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
proc.stdin.write(diff)
proc.stdin.close()
output = proc.stdout.read().decode('utf-8')
ret |= printFlakeOutput(output)
if ret == 0:
print('style test passed.')
else:
@ -244,14 +240,20 @@ def unitTests():
return ret
def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, targetRepo=None):
def checkMergeSize(
sourceBranch=None,
targetBranch=None,
sourceRepo=None,
targetRepo=None
):
"""
Check that a git merge would not increase the repository size by MERGE_SIZE_LIMIT.
Check that a git merge would not increase the repository size by
MERGE_SIZE_LIMIT.
"""
if sourceBranch is None:
sourceBranch = getGitBranch()
sourceRepo = '..'
if targetBranch is None:
if sourceBranch == 'develop':
targetBranch = 'develop'
@ -259,38 +261,38 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target
else:
targetBranch = 'develop'
targetRepo = '..'
workingDir = '__merge-test-clone'
env = dict(TARGET_BRANCH=targetBranch,
SOURCE_BRANCH=sourceBranch,
TARGET_REPO=targetRepo,
env = dict(TARGET_BRANCH=targetBranch,
SOURCE_BRANCH=sourceBranch,
TARGET_REPO=targetRepo,
SOURCE_REPO=sourceRepo,
WORKING_DIR=workingDir,
)
print("Testing merge size difference:\n"
" SOURCE: {SOURCE_REPO} {SOURCE_BRANCH}\n"
" TARGET: {TARGET_BRANCH} {TARGET_REPO}".format(**env))
setup = """
mkdir {WORKING_DIR} && cd {WORKING_DIR} &&
git init && git remote add -t {TARGET_BRANCH} target {TARGET_REPO} &&
git fetch target {TARGET_BRANCH} &&
git checkout -qf target/{TARGET_BRANCH} &&
git fetch target {TARGET_BRANCH} &&
git checkout -qf target/{TARGET_BRANCH} &&
git gc -q --aggressive
""".format(**env)
checkSize = """
cd {WORKING_DIR} &&
cd {WORKING_DIR} &&
du -s . | sed -e "s/\t.*//"
""".format(**env)
merge = """
cd {WORKING_DIR} &&
git pull -q {SOURCE_REPO} {SOURCE_BRANCH} &&
git pull -q {SOURCE_REPO} {SOURCE_BRANCH} &&
git gc -q --aggressive
""".format(**env)
try:
print("Check out target branch:\n" + setup)
check_call(setup, shell=True)
@ -300,13 +302,17 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target
check_call(merge, shell=True)
mergeSize = int(check_output(checkSize, shell=True))
print("MERGE SIZE: %d kB" % mergeSize)
diff = mergeSize - targetSize
if diff <= MERGE_SIZE_LIMIT:
print("DIFFERENCE: %d kB [OK]" % diff)
return 0
else:
print("\033[0;31m" + "DIFFERENCE: %d kB [exceeds %d kB]" % (diff, MERGE_SIZE_LIMIT) + "\033[0m")
print("\033[0;31m"
+ "DIFFERENCE: %d kB [exceeds %d kB]" % (
diff,
MERGE_SIZE_LIMIT)
+ "\033[0m")
return 2
finally:
if os.path.isdir(workingDir):
@ -327,7 +333,11 @@ def mergeTests():
def listAllPackages(pkgroot):
path = os.getcwd()
n = len(path.split(os.path.sep))
subdirs = [i[0].split(os.path.sep)[n:] for i in os.walk(os.path.join(path, pkgroot)) if '__init__.py' in i[2]]
subdirs = [
i[0].split(os.path.sep)[n:]
for i in os.walk(os.path.join(path, pkgroot))
if '__init__.py' in i[2]
]
return ['.'.join(p) for p in subdirs]
@ -338,48 +348,61 @@ def getInitVersion(pkgroot):
init = open(initfile).read()
m = re.search(r'__version__ = (\S+)\n', init)
if m is None or len(m.groups()) != 1:
raise Exception("Cannot determine __version__ from init file: '%s'!" % initfile)
raise Exception("Cannot determine __version__ from init file: "
+ "'%s'!" % initfile)
version = m.group(1).strip('\'\"')
return version
def gitCommit(name):
"""Return the commit ID for the given name."""
commit = check_output(['git', 'show', name], universal_newlines=True).split('\n')[0]
commit = check_output(
['git', 'show', name],
universal_newlines=True).split('\n')[0]
assert commit[:7] == 'commit '
return commit[7:]
def getGitVersion(tagPrefix):
"""Return a version string with information about this git checkout.
If the checkout is an unmodified, tagged commit, then return the tag version.
If this is not a tagged commit, return the output of ``git describe --tags``.
If the checkout is an unmodified, tagged commit, then return the tag
version
If this is not a tagged commit, return the output of
``git describe --tags``
If this checkout has been modified, append "+" to the version.
"""
path = os.getcwd()
if not os.path.isdir(os.path.join(path, '.git')):
return None
v = check_output(['git', 'describe', '--tags', '--dirty', '--match=%s*'%tagPrefix]).strip().decode('utf-8')
v = check_output(['git',
'describe',
'--tags',
'--dirty',
'--match=%s*'%tagPrefix]).strip().decode('utf-8')
# chop off prefix
assert v.startswith(tagPrefix)
v = v[len(tagPrefix):]
# split up version parts
parts = v.split('-')
# has working tree been modified?
modified = False
if parts[-1] == 'dirty':
modified = True
parts = parts[:-1]
# have commits been added on top of last tagged version?
# (git describe adds -NNN-gXXXXXXX if this is the case)
local = None
if len(parts) > 2 and re.match(r'\d+', parts[-2]) and re.match(r'g[0-9a-f]{7}', parts[-1]):
if (len(parts) > 2 and
re.match(r'\d+', parts[-2]) and
re.match(r'g[0-9a-f]{7}', parts[-1])):
local = parts[-1]
parts = parts[:-2]
gitVersion = '-'.join(parts)
if local is not None:
gitVersion += '+' + local
@ -389,7 +412,10 @@ def getGitVersion(tagPrefix):
return gitVersion
def getGitBranch():
m = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True))
m = re.search(
r'\* (.*)',
check_output(['git', 'branch'],
universal_newlines=True))
if m is None:
return ''
else:
@ -397,32 +423,33 @@ def getGitBranch():
def getVersionStrings(pkg):
"""
Returns 4 version strings:
Returns 4 version strings:
* the version string to use for this build,
* version string requested with --force-version (or None)
* version string that describes the current git checkout (or None).
* version string in the pkg/__init__.py,
* version string in the pkg/__init__.py,
The first return value is (forceVersion or gitVersion or initVersion).
"""
## Determine current version string from __init__.py
initVersion = getInitVersion(pkgroot=pkg)
## If this is a git checkout, try to generate a more descriptive version string
# If this is a git checkout
# try to generate a more descriptive version string
try:
gitVersion = getGitVersion(tagPrefix=pkg+'-')
except:
gitVersion = None
sys.stderr.write("This appears to be a git checkout, but an error occurred "
"while attempting to determine a version string for the "
"current commit.\n")
sys.stderr.write("This appears to be a git checkout, but an error "
"occurred while attempting to determine a version "
"string for the current commit.\n")
sys.excepthook(*sys.exc_info())
# See whether a --force-version flag was given
forcedVersion = None
for i,arg in enumerate(sys.argv):
for i, arg in enumerate(sys.argv):
if arg.startswith('--force-version'):
if arg == '--force-version':
forcedVersion = sys.argv[i+1]
@ -431,8 +458,8 @@ def getVersionStrings(pkg):
elif arg.startswith('--force-version='):
forcedVersion = sys.argv[i].replace('--force-version=', '')
sys.argv.pop(i)
## Finally decide on a version string to use:
if forcedVersion is not None:
version = forcedVersion
@ -443,7 +470,8 @@ def getVersionStrings(pkg):
_, local = gitVersion.split('+')
if local != '':
version = version + '+' + local
sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version)
sys.stderr.write("Detected git commit; "
+ "will use version string: '%s'\n" % version)
return version, forcedVersion, gitVersion, initVersion
@ -457,29 +485,31 @@ class DebCommand(Command):
maintainer = "Luke Campagnola <luke.campagnola@gmail.com>"
debTemplate = "debian"
debDir = "deb_build"
user_options = []
def initialize_options(self):
self.cwd = None
def finalize_options(self):
self.cwd = os.getcwd()
def run(self):
version = self.distribution.get_version()
pkgName = self.distribution.get_name()
debName = "python-" + pkgName
debDir = self.debDir
assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd
assert os.getcwd() == self.cwd, 'Must be in package root: '
+ '%s' % self.cwd
if os.path.isdir(debDir):
raise Exception('DEB build dir already exists: "%s"' % debDir)
sdist = "dist/%s-%s.tar.gz" % (pkgName, version)
if not os.path.isfile(sdist):
raise Exception("No source distribution; run `setup.py sdist` first.")
raise Exception("No source distribution; "
+ "run `setup.py sdist` first.")
# copy sdist to build directory and extract
os.mkdir(debDir)
renamedSdist = '%s_%s.orig.tar.gz' % (debName, version)
@ -489,16 +519,20 @@ class DebCommand(Command):
if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0:
raise Exception("Error extracting source distribution.")
buildDir = '%s/%s-%s' % (debDir, pkgName, version)
# copy debian control structure
print("copytree %s => %s" % (self.debTemplate, buildDir+'/debian'))
shutil.copytree(self.debTemplate, buildDir+'/debian')
# Write new changelog
chlog = generateDebianChangelog(pkgName, 'CHANGELOG', version, self.maintainer)
chlog = generateDebianChangelog(
pkgName,
'CHANGELOG',
version,
self.maintainer)
print("write changelog %s" % buildDir+'/debian/changelog')
open(buildDir+'/debian/changelog', 'w').write(chlog)
# build package
print('cd %s; debuild -us -uc' % buildDir)
if os.system('cd %s; debuild -us -uc' % buildDir) != 0:
@ -521,43 +555,45 @@ class DebugCommand(Command):
class TestCommand(Command):
description = "Run all package tests and exit immediately with informative return code."
description = "Run all package tests and exit immediately with ", \
"informative return code."
user_options = []
def run(self):
sys.exit(unitTests())
def initialize_options(self):
pass
def finalize_options(self):
pass
class StyleCommand(Command):
description = "Check all code for style, exit immediately with informative return code."
description = "Check all code for style, exit immediately with ", \
"informative return code."
user_options = []
def run(self):
sys.exit(checkStyle())
def initialize_options(self):
pass
def finalize_options(self):
pass
class MergeTestCommand(Command):
description = "Run all tests needed to determine whether the current code is suitable for merge."
description = "Run all tests needed to determine whether the current ",\
"code is suitable for merge."
user_options = []
def run(self):
sys.exit(mergeTests())
def initialize_options(self):
pass
def finalize_options(self):
pass