Merge branch 'develop' into update_changelog
This commit is contained in:
commit
bedb34bf8d
49
.flake8
Normal file
49
.flake8
Normal 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
11
.pre-commit-config.yaml
Normal 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]
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
----------------------
|
||||
|
@ -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'},
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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 *
|
||||
|
@ -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
|
||||
|
@ -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'):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
71
pyqtgraph/exporters/tests/test_hdf5.py
Normal file
71
pyqtgraph/exporters/tests/test_hdf5.py
Normal 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'])
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -28,3 +28,5 @@ def test_AxisItem_stopAxisAtTick(monkeypatch):
|
||||
monkeypatch.setattr(left, "drawPicture", test_left)
|
||||
|
||||
plot.show()
|
||||
app.processEvents()
|
||||
plot.close()
|
||||
|
@ -35,3 +35,5 @@ def test_ErrorBarItem_defer_data():
|
||||
r_clear_ebi = plot.viewRect()
|
||||
|
||||
assert r_clear_ebi == r_no_ebi
|
||||
|
||||
plot.close()
|
||||
|
@ -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__':
|
||||
|
@ -86,3 +86,5 @@ def test_clipping():
|
||||
|
||||
assert xDisp[0] <= vr.left()
|
||||
assert xDisp[-1] >= vr.right()
|
||||
|
||||
w.close()
|
||||
|
23
pyqtgraph/graphicsItems/tests/test_TextItem.py
Normal file
23
pyqtgraph/graphicsItems/tests/test_TextItem.py
Normal 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()
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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), ))
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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):
|
||||
|
44
pyqtgraph/widgets/tests/test_histogramlutwidget.py
Normal file
44
pyqtgraph/widgets/tests/test_histogramlutwidget.py
Normal 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()
|
||||
|
9
setup.py
9
setup.py
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user