Merge remote-tracking branch 'upstream/develop' into patch-3
This commit is contained in:
commit
262004e2ec
17
.travis.yml
17
.travis.yml
|
@ -9,21 +9,17 @@ sudo: false
|
|||
notifications:
|
||||
email: false
|
||||
|
||||
virtualenv:
|
||||
system_site_packages: true
|
||||
|
||||
|
||||
env:
|
||||
# Enable python 2 and python 3 builds
|
||||
# Note that the 2.6 build doesn't get flake8, and runs old versions of
|
||||
# Pyglet and GLFW to make sure we deal with those correctly
|
||||
#- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended
|
||||
# Note that the python 2.6 support ended.
|
||||
- PYTHON=2.7 QT=pyqt4 TEST=extra
|
||||
- PYTHON=2.7 QT=pyside TEST=standard
|
||||
- PYTHON=3.5 QT=pyqt5 TEST=standard
|
||||
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
|
||||
#- PYTHON=3.2 QT=pyqt5 TEST=standard
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
||||
before_install:
|
||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi
|
||||
|
@ -67,11 +63,7 @@ install:
|
|||
fi;
|
||||
- pip install pytest-xdist # multi-thread py.test
|
||||
- pip install pytest-cov # add coverage stats
|
||||
|
||||
# required for example testing on python 2.6
|
||||
- if [ "${PYTHON}" == "2.6" ]; then
|
||||
pip install importlib;
|
||||
fi;
|
||||
- pip install pytest-faulthandler # activate faulthandler
|
||||
|
||||
# Debugging helpers
|
||||
- uname -a
|
||||
|
@ -85,7 +77,6 @@ install:
|
|||
before_script:
|
||||
# We need to create a (fake) display on Travis, let's use a funny resolution
|
||||
- export DISPLAY=:99.0
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render
|
||||
|
||||
# Make sure everyone uses the correct python (this is handled by conda)
|
||||
|
|
69
CONTRIBUTING.md
Normal file
69
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Contributing to PyQtGraph
|
||||
|
||||
Contributions to pyqtgraph are welcome!
|
||||
|
||||
Please use the following guidelines when preparing changes:
|
||||
|
||||
## Submitting Code Changes
|
||||
|
||||
* The preferred method for submitting changes is by github pull request against the "develop" branch.
|
||||
* Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected.
|
||||
* For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort.
|
||||
* Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy.
|
||||
|
||||
## Documentation
|
||||
|
||||
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code.
|
||||
* Documentation is generated with sphinx; please check that docstring changes compile correctly
|
||||
|
||||
## Style guidelines
|
||||
|
||||
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable.
|
||||
* Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8.
|
||||
* Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt
|
||||
* Exception 2: Function docstrings use ReStructuredText tables for describing arguments:
|
||||
|
||||
```text
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
argName1 (type) Description of argument
|
||||
argName2 (type) Description of argument. Longer descriptions must
|
||||
be wrapped within the column guidelines defined by the
|
||||
"====" header and footer.
|
||||
============== ========================================================
|
||||
```
|
||||
|
||||
QObject subclasses that implement new signals should also describe
|
||||
these in a similar table.
|
||||
|
||||
## Testing Setting up a test environment
|
||||
|
||||
### Dependencies
|
||||
|
||||
* tox
|
||||
* tox-conda
|
||||
* pytest
|
||||
* pytest-cov
|
||||
* pytest-xdist
|
||||
* pytest-faulthandler
|
||||
* Optional: pytest-xvfb
|
||||
|
||||
### Tox
|
||||
|
||||
As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin.
|
||||
|
||||
* Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%.
|
||||
* To measure the test coverage, un `pytest --cov -n 4` to run the test suite with coverage on 4 cores.
|
||||
|
||||
### Continous Integration
|
||||
|
||||
For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations
|
||||
|
||||
* Python2.7 with PyQt4
|
||||
* Python2.7 with PySide
|
||||
* Python3.6 with PyQt5-5.9
|
||||
* Python3.6 with PySide2-5.9
|
||||
* Python3.7 with PyQt5-5.12
|
||||
* Python3.7 with PySide2-5.12
|
||||
|
||||
More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1)
|
|
@ -1,58 +0,0 @@
|
|||
Contributions to pyqtgraph are welcome!
|
||||
|
||||
Please use the following guidelines when preparing changes:
|
||||
|
||||
* The preferred method for submitting changes is by github pull request
|
||||
against the "develop" branch.
|
||||
|
||||
* Pull requests should include only a focused and related set of changes.
|
||||
Mixed features and unrelated changes may be rejected.
|
||||
|
||||
* For major changes, it is recommended to discuss your plans on the mailing
|
||||
list or in a github issue before putting in too much effort.
|
||||
|
||||
* Along these lines, please note that pyqtgraph.opengl will be deprecated
|
||||
soon and replaced with VisPy.
|
||||
|
||||
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph
|
||||
uses nose / py.test style testing, so tests should usually be included in a
|
||||
tests/ directory adjacent to the relevant code.
|
||||
|
||||
* Documentation is generated with sphinx; please check that docstring changes
|
||||
compile correctly.
|
||||
|
||||
* Style guidelines:
|
||||
|
||||
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced
|
||||
rigorously as long as the code is clean and readable.
|
||||
|
||||
* Use `python setup.py style` to see whether your code follows
|
||||
the mandatory style guidelines checked by flake8.
|
||||
|
||||
* Exception 1: All variable names should use camelCase rather than
|
||||
underscore_separation. This is done for consistency with Qt
|
||||
|
||||
* Exception 2: Function docstrings use ReStructuredText tables for
|
||||
describing arguments:
|
||||
|
||||
```
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
argName1 (type) Description of argument
|
||||
argName2 (type) Description of argument. Longer descriptions must
|
||||
be wrapped within the column guidelines defined by the
|
||||
"====" header and footer.
|
||||
============== ========================================================
|
||||
```
|
||||
|
||||
QObject subclasses that implement new signals should also describe
|
||||
these in a similar table.
|
||||
|
||||
* Setting up a test environment.
|
||||
|
||||
Tests for a module should ideally cover all code in that module,
|
||||
i.e., statement coverage should be at 100%.
|
||||
|
||||
To measure the test coverage, install py.test, pytest-cov and pytest-xdist.
|
||||
Then run 'py.test --cov -n 4' to run the test suite with coverage on 4 cores.
|
||||
|
51
README.md
51
README.md
|
@ -1,12 +1,13 @@
|
|||
[![Build Status](https://travis-ci.org/pyqtgraph/pyqtgraph.svg?branch=develop)](https://travis-ci.org/pyqtgraph/pyqtgraph)
|
||||
[![codecov.io](http://codecov.io/github/pyqtgraph/pyqtgraph/coverage.svg?branch=develop)](http://codecov.io/github/pyqtgraph/pyqtgraph?branch=develop)
|
||||
|
||||
[![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop)
|
||||
|
||||
|
||||
PyQtGraph
|
||||
=========
|
||||
|
||||
A pure-Python graphics library for PyQt/PySide
|
||||
A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2
|
||||
|
||||
Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill
|
||||
Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill
|
||||
|
||||
<http://www.pyqtgraph.org>
|
||||
|
||||
|
@ -15,28 +16,47 @@ Despite being written entirely in python, the library is fast due to its
|
|||
heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
|
||||
2D display, and OpenGL for 3D display.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* PyQt 4.7+, PySide, PyQt5, or PySide2
|
||||
* python 2.7, or 3.x
|
||||
* NumPy
|
||||
* For 3D graphics: pyopengl and qt-opengl
|
||||
* Known to run on Windows, Linux, and Mac.
|
||||
* PyQt 4.8+, PySide, PyQt5, or PySide2
|
||||
* python 2.7, or 3.x
|
||||
* Required
|
||||
* `numpy`, `scipy`
|
||||
* Optional
|
||||
* `pyopengl` for 3D graphics
|
||||
* `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy`
|
||||
* `hdf5` for large hdf5 binary format support
|
||||
* Known to run on Windows, Linux, and macOS.
|
||||
|
||||
Qt Bindings Test Matrix
|
||||
-----------------------
|
||||
|
||||
Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings.
|
||||
|
||||
| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 |
|
||||
| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: |
|
||||
| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
|
||||
| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: |
|
||||
| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
||||
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
||||
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
||||
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
||||
|
||||
Installation Methods
|
||||
--------------------
|
||||
|
||||
* From pypi:
|
||||
- Last released version: `pip install pyqtgraph`
|
||||
- Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph`
|
||||
* From PyPI:
|
||||
* Last released version: `pip install pyqtgraph`
|
||||
* Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop`
|
||||
* From conda
|
||||
* Last released version: `conda install pyqtgraph`
|
||||
* To install system-wide from source distribution: `python setup.py install`
|
||||
* Many linux package repositories have release versions.
|
||||
* To use with a specific project, simply copy the pyqtgraph subdirectory
|
||||
|
@ -49,4 +69,3 @@ Documentation
|
|||
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu.
|
||||
|
||||
The official documentation lives at http://pyqtgraph.org/documentation
|
||||
|
||||
|
|
38
azure-pipelines.yml
Normal file
38
azure-pipelines.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
############################################################################################
|
||||
# This config was rectrieved in no small part from https://github.com/slaclab/pydm
|
||||
############################################################################################
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- '*' # Build for all branches if they have a azure-pipelines.yml file.
|
||||
tags:
|
||||
include:
|
||||
- 'v*' # Ensure that we are building for tags starting with 'v' (Official Versions)
|
||||
|
||||
# Build only for PRs for master branch
|
||||
pr:
|
||||
autoCancel: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- develop
|
||||
|
||||
variables:
|
||||
OFFICIAL_REPO: 'pyqtgraph/pyqtgraph'
|
||||
|
||||
jobs:
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: Linux
|
||||
vmImage: 'Ubuntu 16.04'
|
||||
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: Windows
|
||||
vmImage: 'vs2017-win2016'
|
||||
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: MacOS
|
||||
vmImage: 'macOS-10.13'
|
196
azure-test-template.yml
Normal file
196
azure-test-template.yml
Normal file
|
@ -0,0 +1,196 @@
|
|||
# Azure Pipelines CI job template for PyDM Tests
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/anaconda?view=azure-devops
|
||||
parameters:
|
||||
name: ''
|
||||
vmImage: ''
|
||||
|
||||
jobs:
|
||||
- job: ${{ parameters.name }}
|
||||
pool:
|
||||
vmImage: ${{ parameters.vmImage }}
|
||||
strategy:
|
||||
matrix:
|
||||
Python27-PyQt4-4.8:
|
||||
python.version: '2.7'
|
||||
qt.bindings: "pyqt=4"
|
||||
install.method: "conda"
|
||||
Python27-PySide-4.8:
|
||||
python.version: '2.7'
|
||||
qt.bindings: "pyside"
|
||||
install.method: "conda"
|
||||
Python36-PyQt-5.9:
|
||||
python.version: "3.6"
|
||||
qt.bindings: "pyqt"
|
||||
install.method: "conda"
|
||||
Python36-PySide2-5.9:
|
||||
python.version: "3.6"
|
||||
qt.bindings: "pyside2"
|
||||
install.method: "conda"
|
||||
Python37-PyQt-5.12:
|
||||
python.version: '3.7'
|
||||
qt.bindings: "PyQt5"
|
||||
install.method: "pip"
|
||||
Python37-PySide2-5.12:
|
||||
python.version: "3.7"
|
||||
qt.bindings: "PySide2"
|
||||
install.method: "pip"
|
||||
|
||||
steps:
|
||||
- task: ScreenResolutionUtility@1
|
||||
inputs:
|
||||
displaySettings: 'specific'
|
||||
width: '1920'
|
||||
height: '1080'
|
||||
condition: eq(variables['agent.os'], 'Windows_NT' )
|
||||
|
||||
- script: |
|
||||
curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe
|
||||
7z x mesa3d-19.1.0-release-msvc.exe
|
||||
cd x64
|
||||
xcopy opengl32.dll C:\windows\system32\mesadrv.dll*
|
||||
xcopy opengl32.dll C:\windows\syswow64\mesadrv.dll*
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||
displayName: "Install Windows-Mesa OpenGL DLL"
|
||||
condition: eq(variables['agent.os'], 'Windows_NT')
|
||||
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: $(python.version)
|
||||
condition: eq(variables['install.method'], 'pip')
|
||||
|
||||
- bash: |
|
||||
if [ $(agent.os) == 'Linux' ]
|
||||
then
|
||||
echo '##vso[task.prependpath]/usr/share/miniconda/bin'
|
||||
elif [ $(agent.os) == 'Darwin' ]
|
||||
then
|
||||
echo '##vso[task.prependpath]$CONDA/bin'
|
||||
sudo install -d -m 0777 /usr/local/miniconda/envs
|
||||
elif [ $(agent.os) == 'Windows_NT' ]
|
||||
then
|
||||
echo "##vso[task.prependpath]$env:CONDA\Scripts"
|
||||
else
|
||||
echo 'Just what OS are you using?'
|
||||
fi
|
||||
displayName: 'Add Conda to $PATH'
|
||||
condition: eq(variables['install.method'], 'conda' )
|
||||
|
||||
- task: CondaEnvironment@0
|
||||
displayName: 'Create Conda Environment'
|
||||
condition: eq(variables['install.method'], 'conda')
|
||||
inputs:
|
||||
environmentName: 'test-environment-$(python.version)'
|
||||
packageSpecs: 'python=$(python.version)'
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet
|
||||
else
|
||||
pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage
|
||||
fi
|
||||
pip install pytest-xdist pytest-cov pytest-faulthandler
|
||||
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
|
||||
|
||||
- bash: |
|
||||
sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
pip install pytest-xvfb
|
||||
displayName: "Virtual Display Setup"
|
||||
condition: eq(variables['agent.os'], 'Linux' )
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
echo python location: `which python`
|
||||
echo python version: `python --version`
|
||||
echo pytest location: `which pytest`
|
||||
echo installed packages
|
||||
pip list
|
||||
echo pyqtgraph system info
|
||||
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||
echo display information
|
||||
if [ $(agent.os) == 'Linux' ]
|
||||
then
|
||||
export DISPLAY=:99.0
|
||||
Xvfb :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset &
|
||||
sleep 3
|
||||
fi
|
||||
python -m pyqtgraph.util.get_resolution
|
||||
echo openGL information
|
||||
python -c "from pyqtgraph.opengl.glInfo import GLTest"
|
||||
displayName: 'Debug Info'
|
||||
continueOnError: false
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
mkdir -p "$SCREENSHOT_DIR"
|
||||
# echo "If Screenshots are generated, they may be downloaded from:"
|
||||
# echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0"
|
||||
pytest . -sv \
|
||||
--junitxml=junit/test-results.xml \
|
||||
-n 1 --cov pyqtgraph --cov-report=xml --cov-report=html
|
||||
displayName: 'Unit tests'
|
||||
env:
|
||||
AZURE: 1
|
||||
SCREENSHOT_DIR: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Screenshots'
|
||||
condition: failed()
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
artifactName: Screenshots
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/test-*.xml'
|
||||
testRunTitle: 'Test Results for $(agent.os) - $(python.version) - $(qt.bindings) - $(install.method)'
|
||||
publishRunAttachments: true
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
|
||||
reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
|
|
@ -9,8 +9,9 @@ There are many different ways to install pyqtgraph, depending on your needs:
|
|||
|
||||
Some users may need to call ``pip3`` instead. This method should work on
|
||||
all platforms.
|
||||
* To get access to the very latest features and bugfixes, clone pyqtgraph from
|
||||
github::
|
||||
* To get access to the very latest features and bugfixes you have three choice::
|
||||
|
||||
1. Clone pyqtgraph from github::
|
||||
|
||||
$ git clone https://github.com/pyqtgraph/pyqtgraph
|
||||
|
||||
|
@ -18,9 +19,18 @@ There are many different ways to install pyqtgraph, depending on your needs:
|
|||
|
||||
$ python setup.py install
|
||||
|
||||
..or you can simply place the pyqtgraph folder someplace importable, such as
|
||||
2. Directly install from GitHub repo::
|
||||
|
||||
$ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop
|
||||
|
||||
You can change to ``develop`` of the above command to the branch
|
||||
name or the commit you prefer.
|
||||
|
||||
3.
|
||||
You can simply place the pyqtgraph folder someplace importable, such as
|
||||
inside the root of another project. PyQtGraph does not need to be "built" or
|
||||
compiled in any way.
|
||||
|
||||
* Packages for pyqtgraph are also available in a few other forms:
|
||||
|
||||
* **Anaconda**: ``conda install pyqtgraph``
|
||||
|
|
|
@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None):
|
|||
rois = []
|
||||
rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9)))
|
||||
rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9)))
|
||||
rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
||||
rois.append(pg.MultiRectROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
||||
rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9)))
|
||||
rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9)))
|
||||
rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
||||
rois.append(pg.PolyLineROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
||||
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
|
||||
|
||||
## Add each ROI to the scene and link its data to a plot curve with the same color
|
||||
|
|
|
@ -9,8 +9,8 @@ import subprocess
|
|||
from pyqtgraph.python2_3 import basestring
|
||||
from pyqtgraph.Qt import QtGui, QT_LIB
|
||||
|
||||
from .utils import buildFileList, path, examples
|
||||
|
||||
from .utils import buildFileList, testFile, path, examples
|
||||
|
||||
if QT_LIB == 'PySide':
|
||||
from .exampleLoaderTemplate_pyside import Ui_Form
|
||||
|
@ -117,30 +117,7 @@ class ExampleLoader(QtGui.QMainWindow):
|
|||
def run():
|
||||
app = QtGui.QApplication([])
|
||||
loader = ExampleLoader()
|
||||
|
||||
app.exec_()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
if '--test' in args:
|
||||
# get rid of orphaned cache files first
|
||||
pg.renamePyc(path)
|
||||
|
||||
files = buildFileList(examples)
|
||||
if '--pyside' in args:
|
||||
lib = 'PySide'
|
||||
elif '--pyqt' in args or '--pyqt4' in args:
|
||||
lib = 'PyQt4'
|
||||
elif '--pyqt5' in args:
|
||||
lib = 'PyQt5'
|
||||
else:
|
||||
lib = ''
|
||||
|
||||
exe = sys.executable
|
||||
print("Running tests:", lib, sys.executable)
|
||||
for f in files:
|
||||
testFile(f[0], f[1], exe, lib)
|
||||
else:
|
||||
run()
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
from __future__ import print_function, division, absolute_import
|
||||
from pyqtgraph import Qt
|
||||
from . import utils
|
||||
from collections import namedtuple
|
||||
import errno
|
||||
import importlib
|
||||
import itertools
|
||||
import pkgutil
|
||||
import pytest
|
||||
import os, sys
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# printing on travis ci frequently leads to "interrupted system call" errors.
|
||||
# as a workaround, we overwrite the built-in print function (bleh)
|
||||
if os.getenv('TRAVIS') is not None:
|
||||
|
@ -32,17 +40,8 @@ if os.getenv('TRAVIS') is not None:
|
|||
print("Installed wrapper for flaky print.")
|
||||
|
||||
|
||||
# apparently importlib does not exist in python 2.6...
|
||||
try:
|
||||
import importlib
|
||||
except ImportError:
|
||||
# we are on python 2.6
|
||||
print("If you want to test the examples, please install importlib from "
|
||||
"pypi\n\npip install importlib\n\n")
|
||||
pass
|
||||
|
||||
files = utils.buildFileList(utils.examples)
|
||||
frontends = {Qt.PYQT4: False, Qt.PYSIDE: False}
|
||||
files = sorted(set(utils.buildFileList(utils.examples)))
|
||||
frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False}
|
||||
# sort out which of the front ends are available
|
||||
for frontend in frontends.keys():
|
||||
try:
|
||||
|
@ -51,15 +50,99 @@ for frontend in frontends.keys():
|
|||
except ImportError:
|
||||
pass
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"frontend, f", itertools.product(sorted(list(frontends.keys())), files))
|
||||
def test_examples(frontend, f):
|
||||
# Test the examples with all available front-ends
|
||||
print('frontend = %s. f = %s' % (frontend, f))
|
||||
if not frontends[frontend]:
|
||||
pytest.skip('%s is not installed. Skipping tests' % frontend)
|
||||
utils.testFile(f[0], f[1], utils.sys.executable, frontend)
|
||||
"frontend, f",
|
||||
[
|
||||
pytest.param(
|
||||
frontend,
|
||||
f,
|
||||
marks=pytest.mark.skipif(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)]
|
||||
)
|
||||
def testExamples(frontend, f, graphicsSystem=None):
|
||||
# runExampleFile(f[0], f[1], sys.executable, frontend)
|
||||
|
||||
name, file = f
|
||||
global path
|
||||
fn = os.path.join(path,file)
|
||||
os.chdir(path)
|
||||
sys.stdout.write("{} ".format(name))
|
||||
sys.stdout.flush()
|
||||
import1 = "import %s" % frontend if frontend != '' else ''
|
||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
||||
code = """
|
||||
try:
|
||||
%s
|
||||
import initExample
|
||||
import pyqtgraph as pg
|
||||
%s
|
||||
import %s
|
||||
import sys
|
||||
print("test complete")
|
||||
sys.stdout.flush()
|
||||
import time
|
||||
while True: ## run a little event loop
|
||||
pg.QtGui.QApplication.processEvents()
|
||||
time.sleep(0.01)
|
||||
except:
|
||||
print("test failed")
|
||||
raise
|
||||
|
||||
""" % (import1, graphicsSystem, import2)
|
||||
if sys.platform.startswith('win'):
|
||||
process = subprocess.Popen([sys.executable],
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
else:
|
||||
process = subprocess.Popen(['exec %s -i' % (sys.executable)],
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close() ##?
|
||||
output = ''
|
||||
fail = False
|
||||
while True:
|
||||
try:
|
||||
c = process.stdout.read(1).decode()
|
||||
except IOError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
# Interrupted system call; just try again.
|
||||
c = ''
|
||||
else:
|
||||
raise
|
||||
output += c
|
||||
|
||||
if output.endswith('test complete'):
|
||||
break
|
||||
if output.endswith('test failed'):
|
||||
fail = True
|
||||
break
|
||||
time.sleep(1)
|
||||
process.kill()
|
||||
#res = process.communicate()
|
||||
res = (process.stdout.read(), process.stderr.read())
|
||||
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
||||
print(res[0].decode())
|
||||
print(res[1].decode())
|
||||
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.cmdline.main()
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
from __future__ import division, print_function, absolute_import
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
from pyqtgraph.pgcollections import OrderedDict
|
||||
from pyqtgraph.python2_3 import basestring
|
||||
|
||||
|
@ -86,7 +82,6 @@ examples = OrderedDict([
|
|||
#('VerticalLabel', '../widgets/VerticalLabel.py'),
|
||||
('JoystickButton', 'JoystickButton.py'),
|
||||
])),
|
||||
|
||||
('Flowcharts', 'Flowchart.py'),
|
||||
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
|
||||
])
|
||||
|
@ -103,73 +98,3 @@ def buildFileList(examples, files=None):
|
|||
else:
|
||||
buildFileList(val, files)
|
||||
return files
|
||||
|
||||
def testFile(name, f, exe, lib, graphicsSystem=None):
|
||||
global path
|
||||
fn = os.path.join(path,f)
|
||||
#print "starting process: ", fn
|
||||
os.chdir(path)
|
||||
sys.stdout.write(name)
|
||||
sys.stdout.flush()
|
||||
|
||||
import1 = "import %s" % lib if lib != '' else ''
|
||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
||||
code = """
|
||||
try:
|
||||
%s
|
||||
import initExample
|
||||
import pyqtgraph as pg
|
||||
%s
|
||||
import %s
|
||||
import sys
|
||||
print("test complete")
|
||||
sys.stdout.flush()
|
||||
import time
|
||||
while True: ## run a little event loop
|
||||
pg.QtGui.QApplication.processEvents()
|
||||
time.sleep(0.01)
|
||||
except:
|
||||
print("test failed")
|
||||
raise
|
||||
|
||||
""" % (import1, graphicsSystem, import2)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close()
|
||||
else:
|
||||
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close() ##?
|
||||
output = ''
|
||||
fail = False
|
||||
while True:
|
||||
try:
|
||||
c = process.stdout.read(1).decode()
|
||||
except IOError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
# Interrupted system call; just try again.
|
||||
c = ''
|
||||
else:
|
||||
raise
|
||||
output += c
|
||||
#sys.stdout.write(c)
|
||||
#sys.stdout.flush()
|
||||
if output.endswith('test complete'):
|
||||
break
|
||||
if output.endswith('test failed'):
|
||||
fail = True
|
||||
break
|
||||
time.sleep(1)
|
||||
process.kill()
|
||||
#res = process.communicate()
|
||||
res = (process.stdout.read(), process.stderr.read())
|
||||
|
||||
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
||||
print('.' * (50-len(name)) + 'FAILED')
|
||||
print(res[0].decode())
|
||||
print(res[1].decode())
|
||||
else:
|
||||
print('.' * (50-len(name)) + 'passed')
|
||||
|
|
|
@ -263,6 +263,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||
for item in prevItems:
|
||||
event.currentItem = item
|
||||
try:
|
||||
if item.scene() is self:
|
||||
item.hoverEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending hover exit event:")
|
||||
|
@ -288,7 +289,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||
else:
|
||||
acceptedItem = None
|
||||
|
||||
if acceptedItem is not None:
|
||||
if acceptedItem is not None and acceptedItem.scene() is self:
|
||||
#print "Drag -> pre-selected item:", acceptedItem
|
||||
self.dragItem = acceptedItem
|
||||
event.currentItem = self.dragItem
|
||||
|
@ -435,6 +436,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||
for item in items:
|
||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||
continue
|
||||
if item.scene() is not self:
|
||||
continue
|
||||
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||
if shape is None:
|
||||
continue
|
||||
|
|
|
@ -100,9 +100,12 @@ def _loadUiType(uiFile):
|
|||
how to make PyQt4 and pyside look the same...
|
||||
http://stackoverflow.com/a/8717832
|
||||
"""
|
||||
|
||||
if QT_LIB == "PYSIDE":
|
||||
import pysideuic
|
||||
else:
|
||||
import pyside2uic as pysideuic
|
||||
import xml.etree.ElementTree as xml
|
||||
#from io import StringIO
|
||||
|
||||
parsed = xml.parse(uiFile)
|
||||
widget_class = parsed.find('widget').get('class')
|
||||
|
@ -216,8 +219,12 @@ elif QT_LIB == PYSIDE2:
|
|||
except ImportError as err:
|
||||
QtTest = FailedImport(err)
|
||||
|
||||
try:
|
||||
import shiboken2
|
||||
isQObjectAlive = shiboken2.isValid
|
||||
except ImportError:
|
||||
# use approximate version
|
||||
isQObjectAlive = _isQObjectAlive
|
||||
|
||||
import PySide2
|
||||
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
|
||||
|
||||
|
|
|
@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject):
|
|||
"""If there is a signal queued up, send it now."""
|
||||
if self.args is None or self.block:
|
||||
return False
|
||||
#self.emit(self.signal, *self.args)
|
||||
self.sigDelayed.emit(self.args)
|
||||
self.args = None
|
||||
args, self.args = self.args, None
|
||||
self.timer.stop()
|
||||
self.lastFlushTime = time()
|
||||
#self.emit(self.signal, *self.args)
|
||||
self.sigDelayed.emit(args)
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
|
|
|
@ -141,7 +141,7 @@ class ColorMap(object):
|
|||
|
||||
pos, color = self.getStops(mode=self.BYTE)
|
||||
color = [QtGui.QColor(*x) for x in color]
|
||||
g.setStops(zip(pos, color))
|
||||
g.setStops(list(zip(pos, color)))
|
||||
|
||||
#if self.colorMode == 'rgb':
|
||||
#ticks = self.listTicks()
|
||||
|
|
|
@ -33,9 +33,8 @@ class ParseError(Exception):
|
|||
msg = "Error parsing string at line %d:\n" % self.lineNum
|
||||
else:
|
||||
msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum)
|
||||
msg += "%s\n%s" % (self.line, self.message)
|
||||
msg += "%s\n%s" % (self.line, Exception.__str__(self))
|
||||
return msg
|
||||
#raise Exception()
|
||||
|
||||
|
||||
def writeConfigFile(data, fname):
|
||||
|
@ -93,13 +92,14 @@ def genString(data, indent=''):
|
|||
s += indent + sk + ':\n'
|
||||
s += genString(data[k], indent + ' ')
|
||||
else:
|
||||
s += indent + sk + ': ' + repr(data[k]) + '\n'
|
||||
s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n'
|
||||
return s
|
||||
|
||||
def parseString(lines, start=0):
|
||||
|
||||
data = OrderedDict()
|
||||
if isinstance(lines, basestring):
|
||||
lines = lines.replace("\\\n", "")
|
||||
lines = lines.split('\n')
|
||||
lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines
|
||||
|
||||
|
|
|
@ -346,9 +346,9 @@ class DockLabel(VerticalLabel):
|
|||
ev.accept()
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
ev.accept()
|
||||
if not self.startedDrag:
|
||||
self.sigClicked.emit(self, ev)
|
||||
ev.accept()
|
||||
|
||||
def mouseDoubleClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
|
|
|
@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow):
|
|||
|
||||
def closeEvent(self, ev):
|
||||
MatplotlibExporter.windows.remove(self)
|
||||
|
||||
|
||||
self.deleteLater()
|
||||
|
|
|
@ -190,12 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}):
|
|||
## this is taken care of in generateSvg instead.
|
||||
#if hasattr(item, 'setExportMode'):
|
||||
#item.setExportMode(False)
|
||||
|
||||
if QT_LIB in ['PySide', 'PySide2']:
|
||||
xmlStr = str(arr)
|
||||
else:
|
||||
xmlStr = bytes(arr).decode('utf-8')
|
||||
doc = xml.parseString(xmlStr.encode('utf-8'))
|
||||
doc = xml.parseString(arr.data())
|
||||
|
||||
try:
|
||||
## Get top-level group for this item
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
SVG export test
|
||||
CSV export test
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
import pyqtgraph as pg
|
||||
|
@ -33,7 +33,8 @@ def test_CSVExporter():
|
|||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||
ex.export(fileName=tempfilename)
|
||||
|
||||
r = csv.reader(open(tempfilename, 'r'))
|
||||
with open(tempfilename, 'r') as csv_file:
|
||||
r = csv.reader(csv_file)
|
||||
lines = [line for line in r]
|
||||
header = lines.pop(0)
|
||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||
|
|
|
@ -27,6 +27,7 @@ from .. import configfile as configfile
|
|||
from .. import dockarea as dockarea
|
||||
from . import FlowchartGraphicsView
|
||||
from .. import functions as fn
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
|
@ -502,8 +503,8 @@ class Flowchart(Node):
|
|||
finally:
|
||||
self.blockSignals(False)
|
||||
|
||||
self.sigChartLoaded.emit()
|
||||
self.outputChanged()
|
||||
self.sigChartLoaded.emit()
|
||||
self.sigStateChanged.emit()
|
||||
|
||||
def loadFile(self, fileName=None, startDir=None):
|
||||
|
@ -519,7 +520,7 @@ class Flowchart(Node):
|
|||
self.fileDialog.fileSelected.connect(self.loadFile)
|
||||
return
|
||||
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
||||
fileName = unicode(fileName)
|
||||
fileName = asUnicode(fileName)
|
||||
state = configfile.readConfigFile(fileName)
|
||||
self.restoreState(state, clear=True)
|
||||
self.viewBox.autoRange()
|
||||
|
@ -534,11 +535,12 @@ class Flowchart(Node):
|
|||
if startDir is None:
|
||||
startDir = '.'
|
||||
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
self.fileDialog.setDefaultSuffix("fc")
|
||||
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
self.fileDialog.show()
|
||||
self.fileDialog.fileSelected.connect(self.saveFile)
|
||||
return
|
||||
fileName = unicode(fileName)
|
||||
fileName = asUnicode(fileName)
|
||||
configfile.writeConfigFile(self.saveState(), fileName)
|
||||
self.sigFileSaved.emit(fileName)
|
||||
|
||||
|
@ -662,7 +664,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
|||
#self.setCurrentFile(newFile)
|
||||
|
||||
def fileSaved(self, fileName):
|
||||
self.setCurrentFile(unicode(fileName))
|
||||
self.setCurrentFile(asUnicode(fileName))
|
||||
self.ui.saveBtn.success("Saved.")
|
||||
|
||||
def saveClicked(self):
|
||||
|
@ -691,7 +693,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
|||
#self.setCurrentFile(newFile)
|
||||
|
||||
def setCurrentFile(self, fileName):
|
||||
self.currentFileName = unicode(fileName)
|
||||
self.currentFileName = asUnicode(fileName)
|
||||
if fileName is None:
|
||||
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
||||
else:
|
||||
|
|
|
@ -373,7 +373,7 @@ class Node(QtCore.QObject):
|
|||
pos = self.graphicsItem().pos()
|
||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||
for term in self._inputs.values() + self._outputs.values():
|
||||
for term in list(self._inputs.values()) + list(self._outputs.values()):
|
||||
termsEditable |= term._renamable | term._removable | term._multiable
|
||||
if termsEditable:
|
||||
state['terminals'] = self.saveTerminals()
|
||||
|
|
|
@ -1057,6 +1057,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
|||
raise Exception('levels argument is required for float input types')
|
||||
if not isinstance(levels, np.ndarray):
|
||||
levels = np.array(levels)
|
||||
levels = levels.astype(np.float)
|
||||
if levels.ndim == 1:
|
||||
if levels.shape[0] != 2:
|
||||
raise Exception('levels argument must have length 2')
|
||||
|
@ -1093,7 +1094,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
|||
for i in range(data.shape[-1]):
|
||||
minVal, maxVal = levels[i]
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||
rng = maxVal-minVal
|
||||
rng = 1 if rng == 0 else rng
|
||||
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
|
||||
|
@ -1103,7 +1104,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
|||
minVal, maxVal = levels
|
||||
if minVal != 0 or maxVal != scale:
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
|
||||
|
||||
|
||||
|
@ -1380,7 +1381,7 @@ def gaussianFilter(data, sigma):
|
|||
# clip off extra data
|
||||
sl = [slice(None)] * data.ndim
|
||||
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
|
||||
filtered = filtered[sl]
|
||||
filtered = filtered[tuple(sl)]
|
||||
return filtered + baseline
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
|
||||
"""
|
||||
============== ===============================================================
|
||||
**Arguments:**
|
||||
|
@ -28,6 +28,14 @@ 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.
|
||||
text The text (excluding units) to display on the label for this
|
||||
axis.
|
||||
units The units for this axis. Units should generally be given
|
||||
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
||||
scaling prefix will be automatically prepended based on the
|
||||
range of data displayed.
|
||||
**args All extra keyword arguments become CSS style options for
|
||||
the <span> tag which will surround the axis label and units.
|
||||
============== ===============================================================
|
||||
"""
|
||||
|
||||
|
@ -67,10 +75,10 @@ class AxisItem(GraphicsWidget):
|
|||
self.fixedWidth = None
|
||||
self.fixedHeight = None
|
||||
|
||||
self.labelText = ''
|
||||
self.labelUnits = ''
|
||||
self.labelUnitPrefix=''
|
||||
self.labelStyle = {}
|
||||
self.labelText = text
|
||||
self.labelUnits = units
|
||||
self.labelUnitPrefix = unitPrefix
|
||||
self.labelStyle = args
|
||||
self.logMode = False
|
||||
self.tickFont = None
|
||||
|
||||
|
@ -80,6 +88,8 @@ class AxisItem(GraphicsWidget):
|
|||
self.autoSIPrefix = True
|
||||
self.autoSIPrefixScale = 1.0
|
||||
|
||||
self.showLabel(False)
|
||||
|
||||
self.setRange(0, 1)
|
||||
|
||||
if pen is None:
|
||||
|
@ -91,8 +101,6 @@ class AxisItem(GraphicsWidget):
|
|||
if linkView is not None:
|
||||
self.linkToView(linkView)
|
||||
|
||||
self.showLabel(False)
|
||||
|
||||
self.grid = False
|
||||
#self.setCacheMode(self.DeviceCoordinateCache)
|
||||
|
||||
|
@ -256,11 +264,14 @@ class AxisItem(GraphicsWidget):
|
|||
axis.setLabel('label text', units='V', **labelStyle)
|
||||
|
||||
"""
|
||||
show_label = False
|
||||
if text is not None:
|
||||
self.labelText = text
|
||||
self.showLabel()
|
||||
show_label = True
|
||||
if units is not None:
|
||||
self.labelUnits = units
|
||||
show_label = True
|
||||
if show_label:
|
||||
self.showLabel()
|
||||
if unitPrefix is not None:
|
||||
self.labelUnitPrefix = unitPrefix
|
||||
|
@ -632,10 +643,9 @@ class AxisItem(GraphicsWidget):
|
|||
maxTickCount = size / minSpacing
|
||||
if dif / intervals[minorIndex] <= maxTickCount:
|
||||
levels.append((intervals[minorIndex], 0))
|
||||
|
||||
return levels
|
||||
|
||||
|
||||
|
||||
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
|
||||
### Determine major/minor tick spacings which flank the optimal spacing.
|
||||
#intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
|
||||
|
@ -889,16 +899,20 @@ class AxisItem(GraphicsWidget):
|
|||
|
||||
|
||||
if self.style['stopAxisAtTick'][0] is True:
|
||||
stop = max(span[0].y(), min(map(min, tickPositions)))
|
||||
minTickPosition = min(map(min, tickPositions))
|
||||
if axis == 0:
|
||||
stop = max(span[0].y(), minTickPosition)
|
||||
span[0].setY(stop)
|
||||
else:
|
||||
stop = max(span[0].x(), minTickPosition)
|
||||
span[0].setX(stop)
|
||||
if self.style['stopAxisAtTick'][1] is True:
|
||||
stop = min(span[1].y(), max(map(max, tickPositions)))
|
||||
maxTickPosition = max(map(max, tickPositions))
|
||||
if axis == 0:
|
||||
stop = min(span[1].y(), maxTickPosition)
|
||||
span[1].setY(stop)
|
||||
else:
|
||||
stop = min(span[1].x(), maxTickPosition)
|
||||
span[1].setX(stop)
|
||||
axisSpec = (self.pen(), span[0], span[1])
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ class ErrorBarItem(GraphicsObject):
|
|||
beam=None,
|
||||
pen=None
|
||||
)
|
||||
self.setVisible(False)
|
||||
self.setData(**opts)
|
||||
|
||||
def setData(self, **opts):
|
||||
|
@ -45,6 +46,7 @@ class ErrorBarItem(GraphicsObject):
|
|||
This method was added in version 0.9.9. For prior versions, use setOpts.
|
||||
"""
|
||||
self.opts.update(opts)
|
||||
self.setVisible(all(self.opts[ax] is not None for ax in ['x', 'y']))
|
||||
self.path = None
|
||||
self.update()
|
||||
self.prepareGeometryChange()
|
||||
|
@ -59,6 +61,7 @@ class ErrorBarItem(GraphicsObject):
|
|||
|
||||
x, y = self.opts['x'], self.opts['y']
|
||||
if x is None or y is None:
|
||||
self.path = p
|
||||
return
|
||||
|
||||
beam = self.opts['beam']
|
||||
|
|
|
@ -186,7 +186,7 @@ class HistogramLUTItem(GraphicsWidget):
|
|||
"""Return a lookup table from the color gradient defined by this
|
||||
HistogramLUTItem.
|
||||
"""
|
||||
if self.levelMode is not 'mono':
|
||||
if self.levelMode != 'mono':
|
||||
return None
|
||||
if n is None:
|
||||
if img.dtype == np.uint8:
|
||||
|
@ -205,8 +205,8 @@ class HistogramLUTItem(GraphicsWidget):
|
|||
def regionChanging(self):
|
||||
if self.imageItem() is not None:
|
||||
self.imageItem().setLevels(self.getLevels())
|
||||
self.sigLevelsChanged.emit(self)
|
||||
self.update()
|
||||
self.sigLevelsChanged.emit(self)
|
||||
|
||||
def imageChanged(self, autoLevel=False, autoRange=False):
|
||||
if self.imageItem() is None:
|
||||
|
|
|
@ -2,13 +2,17 @@ from __future__ import division
|
|||
|
||||
from ..Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import collections
|
||||
from .. import functions as fn
|
||||
from .. import debug as debug
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from ..Point import Point
|
||||
from .. import getConfigOption
|
||||
|
||||
try:
|
||||
from collections.abc import Callable
|
||||
except ImportError:
|
||||
# fallback for python < 3.3
|
||||
from collections import Callable
|
||||
|
||||
__all__ = ['ImageItem']
|
||||
|
||||
|
@ -357,7 +361,7 @@ class ImageItem(GraphicsObject):
|
|||
|
||||
# Request a lookup table if this image has only one channel
|
||||
if self.image.ndim == 2 or self.image.shape[2] == 1:
|
||||
if isinstance(self.lut, collections.Callable):
|
||||
if isinstance(self.lut, Callable):
|
||||
lut = self.lut(self.image)
|
||||
else:
|
||||
lut = self.lut
|
||||
|
@ -484,9 +488,12 @@ class ImageItem(GraphicsObject):
|
|||
step = (step, step)
|
||||
stepData = self.image[::step[0], ::step[1]]
|
||||
|
||||
if bins == 'auto':
|
||||
if 'auto' == bins:
|
||||
mn = np.nanmin(stepData)
|
||||
mx = np.nanmax(stepData)
|
||||
if mx == mn:
|
||||
# degenerate image, arange will fail
|
||||
mx += 1
|
||||
if np.isnan(mn) or np.isnan(mx):
|
||||
# the data are all-nan
|
||||
return None, None
|
||||
|
@ -621,7 +628,7 @@ class ImageItem(GraphicsObject):
|
|||
mask = self.drawMask
|
||||
src = dk
|
||||
|
||||
if isinstance(self.drawMode, collections.Callable):
|
||||
if isinstance(self.drawMode, Callable):
|
||||
self.drawMode(dk, self.image, mask, ss, ts, ev)
|
||||
else:
|
||||
src = src[ss]
|
||||
|
|
|
@ -59,7 +59,6 @@ class PlotCurveItem(GraphicsObject):
|
|||
|
||||
self.metaData = {}
|
||||
self.opts = {
|
||||
'pen': fn.mkPen('w'),
|
||||
'shadowPen': None,
|
||||
'fillLevel': None,
|
||||
'brush': None,
|
||||
|
@ -70,6 +69,8 @@ class PlotCurveItem(GraphicsObject):
|
|||
'mouseWidth': 8, # width of shape responding to mouse click
|
||||
'compositionMode': None,
|
||||
}
|
||||
if 'pen' not in kargs:
|
||||
self.opts['pen'] = fn.mkPen('w')
|
||||
self.setClickable(kargs.get('clickable', False))
|
||||
self.setData(*args, **kargs)
|
||||
|
||||
|
@ -482,8 +483,8 @@ class PlotCurveItem(GraphicsObject):
|
|||
p.fillPath(self.fillPath, self.opts['brush'])
|
||||
profiler('draw fill path')
|
||||
|
||||
sp = fn.mkPen(self.opts['shadowPen'])
|
||||
cp = fn.mkPen(self.opts['pen'])
|
||||
sp = self.opts['shadowPen']
|
||||
cp = self.opts['pen']
|
||||
|
||||
## Copy pens and apply alpha adjustment
|
||||
#sp = QtGui.QPen(self.opts['shadowPen'])
|
||||
|
@ -496,12 +497,13 @@ class PlotCurveItem(GraphicsObject):
|
|||
#pen.setColor(c)
|
||||
##pen.setCosmetic(True)
|
||||
|
||||
|
||||
|
||||
if sp is not None and sp.style() != QtCore.Qt.NoPen:
|
||||
p.setPen(sp)
|
||||
p.drawPath(path)
|
||||
p.setPen(cp)
|
||||
if self.fillPath is not None:
|
||||
p.drawPath(self.fillPath)
|
||||
else:
|
||||
p.drawPath(path)
|
||||
profiler('drawPath')
|
||||
|
||||
|
|
|
@ -515,6 +515,8 @@ class PlotDataItem(GraphicsObject):
|
|||
if self.opts['logMode'][0]:
|
||||
x=x[1:]
|
||||
y=y[1:]
|
||||
|
||||
with np.errstate(divide='ignore'):
|
||||
if self.opts['logMode'][0]:
|
||||
x = np.log10(x)
|
||||
if self.opts['logMode'][1]:
|
||||
|
@ -643,8 +645,8 @@ class PlotDataItem(GraphicsObject):
|
|||
#self.yClean = None
|
||||
self.xDisp = None
|
||||
self.yDisp = None
|
||||
self.curve.setData([])
|
||||
self.scatter.setData([])
|
||||
self.curve.clear()
|
||||
self.scatter.clear()
|
||||
|
||||
def appendData(self, *args, **kargs):
|
||||
pass
|
||||
|
|
|
@ -545,9 +545,9 @@ class PlotItem(GraphicsWidget):
|
|||
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
|
||||
Returns the item created.
|
||||
"""
|
||||
pos = kwds.get('pos', x if x is not None else y)
|
||||
angle = kwds.get('angle', 0 if x is None else 90)
|
||||
line = InfiniteLine(pos, angle, **kwds)
|
||||
kwds['pos'] = kwds.get('pos', x if x is not None else y)
|
||||
kwds['angle'] = kwds.get('angle', 0 if x is None else 90)
|
||||
line = InfiniteLine(**kwds)
|
||||
self.addItem(line)
|
||||
if z is not None:
|
||||
line.setZValue(z)
|
||||
|
@ -986,7 +986,7 @@ class PlotItem(GraphicsWidget):
|
|||
self._menuEnabled = enableMenu
|
||||
if enableViewBoxMenu is None:
|
||||
return
|
||||
if enableViewBoxMenu is 'same':
|
||||
if enableViewBoxMenu == 'same':
|
||||
enableViewBoxMenu = enableMenu
|
||||
self.vb.setMenuEnabled(enableViewBoxMenu)
|
||||
|
||||
|
|
|
@ -428,6 +428,7 @@ class ROI(GraphicsObject):
|
|||
|
||||
def handleMoveStarted(self):
|
||||
self.preMoveState = self.getState()
|
||||
self.sigRegionChangeStarted.emit(self)
|
||||
|
||||
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
|
||||
"""
|
||||
|
@ -711,10 +712,10 @@ class ROI(GraphicsObject):
|
|||
|
||||
if hover:
|
||||
self.setMouseHover(True)
|
||||
self.sigHoverEvent.emit(self)
|
||||
ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion.
|
||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||
ev.acceptClicks(QtCore.Qt.MidButton)
|
||||
self.sigHoverEvent.emit(self)
|
||||
else:
|
||||
self.setMouseHover(False)
|
||||
|
||||
|
@ -928,6 +929,7 @@ class ROI(GraphicsObject):
|
|||
|
||||
if h['type'] == 'rf':
|
||||
h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle
|
||||
h['pos'] = self.mapFromParent(p1)
|
||||
|
||||
elif h['type'] == 'sr':
|
||||
if h['center'][0] == h['pos'][0]:
|
||||
|
@ -1102,7 +1104,7 @@ class ROI(GraphicsObject):
|
|||
return bounds, tr
|
||||
|
||||
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
||||
"""Use the position and orientation of this ROI relative to an imageItem
|
||||
r"""Use the position and orientation of this ROI relative to an imageItem
|
||||
to pull a slice from an array.
|
||||
|
||||
=================== ====================================================
|
||||
|
@ -1524,7 +1526,7 @@ class TestROI(ROI):
|
|||
|
||||
|
||||
class RectROI(ROI):
|
||||
"""
|
||||
r"""
|
||||
Rectangular ROI subclass with a single scale handle at the top-right corner.
|
||||
|
||||
============== =============================================================
|
||||
|
@ -1553,7 +1555,7 @@ class RectROI(ROI):
|
|||
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
||||
|
||||
class LineROI(ROI):
|
||||
"""
|
||||
r"""
|
||||
Rectangular ROI subclass with scale-rotate handles on either side. This
|
||||
allows the ROI to be positioned as if moving the ends of a line segment.
|
||||
A third handle controls the width of the ROI orthogonal to its "line" axis.
|
||||
|
@ -1586,8 +1588,10 @@ class LineROI(ROI):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
class MultiRectROI(QtGui.QGraphicsObject):
|
||||
"""
|
||||
r"""
|
||||
Chain of rectangular ROIs connected by handles.
|
||||
|
||||
This is generally used to mark a curved path through
|
||||
|
@ -1665,7 +1669,7 @@ class MultiRectROI(QtGui.QGraphicsObject):
|
|||
ms = min([r.shape[axes[1]] for r in rgns])
|
||||
sl = [slice(None)] * rgns[0].ndim
|
||||
sl[axes[1]] = slice(0,ms)
|
||||
rgns = [r[sl] for r in rgns]
|
||||
rgns = [r[tuple(sl)] for r in rgns]
|
||||
#print [r.shape for r in rgns], axes
|
||||
|
||||
return np.concatenate(rgns, axis=axes[0])
|
||||
|
@ -1726,7 +1730,7 @@ class MultiLineROI(MultiRectROI):
|
|||
|
||||
|
||||
class EllipseROI(ROI):
|
||||
"""
|
||||
r"""
|
||||
Elliptical ROI subclass with one scale handle and one rotation handle.
|
||||
|
||||
|
||||
|
@ -1810,8 +1814,9 @@ class EllipseROI(ROI):
|
|||
return self.path
|
||||
|
||||
|
||||
|
||||
class CircleROI(EllipseROI):
|
||||
"""
|
||||
r"""
|
||||
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
|
||||
proportionally to maintain its aspect ratio.
|
||||
|
||||
|
@ -1880,7 +1885,7 @@ class PolygonROI(ROI):
|
|||
|
||||
|
||||
class PolyLineROI(ROI):
|
||||
"""
|
||||
r"""
|
||||
Container class for multiple connected LineSegmentROIs.
|
||||
|
||||
This class allows the user to draw paths of multiple line segments.
|
||||
|
@ -2076,7 +2081,7 @@ class PolyLineROI(ROI):
|
|||
|
||||
|
||||
class LineSegmentROI(ROI):
|
||||
"""
|
||||
r"""
|
||||
ROI subclass with two freely-moving handles defining a line.
|
||||
|
||||
============== =============================================================
|
||||
|
|
|
@ -834,8 +834,8 @@ class ScatterPlotItem(GraphicsObject):
|
|||
pts = self.pointsAt(ev.pos())
|
||||
if len(pts) > 0:
|
||||
self.ptsClicked = pts
|
||||
self.sigClicked.emit(self, self.ptsClicked)
|
||||
ev.accept()
|
||||
self.sigClicked.emit(self, self.ptsClicked)
|
||||
else:
|
||||
#print "no spots"
|
||||
ev.ignore()
|
||||
|
|
|
@ -208,7 +208,10 @@ class ViewBox(GraphicsWidget):
|
|||
self.setAspectLocked(lockAspect)
|
||||
|
||||
self.border = fn.mkPen(border)
|
||||
if enableMenu:
|
||||
self.menu = ViewBoxMenu(self)
|
||||
else:
|
||||
self.menu = None
|
||||
|
||||
self.register(name)
|
||||
if name is None:
|
||||
|
@ -274,7 +277,7 @@ class ViewBox(GraphicsWidget):
|
|||
scene = self.scene()
|
||||
if scene == self._lastScene:
|
||||
return
|
||||
if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'):
|
||||
if self._lastScene is not None and hasattr(self._lastScene, 'sigPrepareForPaint'):
|
||||
self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint)
|
||||
if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
|
||||
scene.sigPrepareForPaint.connect(self.prepareForPaint)
|
||||
|
@ -315,6 +318,13 @@ class ViewBox(GraphicsWidget):
|
|||
del state['linkedViews']
|
||||
|
||||
self.state.update(state)
|
||||
|
||||
if self.state['enableMenu'] and self.menu is None:
|
||||
self.menu = ViewBoxMenu(self)
|
||||
self.updateViewLists()
|
||||
else:
|
||||
self.menu = None
|
||||
|
||||
self.updateViewRange()
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
|
@ -368,6 +378,13 @@ class ViewBox(GraphicsWidget):
|
|||
|
||||
def setMenuEnabled(self, enableMenu=True):
|
||||
self.state['enableMenu'] = enableMenu
|
||||
if enableMenu:
|
||||
if self.menu is None:
|
||||
self.menu = ViewBoxMenu(self)
|
||||
self.updateViewLists()
|
||||
else:
|
||||
self.menu.setParent(None)
|
||||
self.menu = None
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
def menuEnabled(self):
|
||||
|
@ -410,8 +427,8 @@ class ViewBox(GraphicsWidget):
|
|||
self.updateAutoRange()
|
||||
self.updateViewRange()
|
||||
self._matrixNeedsUpdate = True
|
||||
self.sigStateChanged.emit(self)
|
||||
self.background.setRect(self.rect())
|
||||
self.sigStateChanged.emit(self)
|
||||
self.sigResized.emit(self)
|
||||
|
||||
def viewRange(self):
|
||||
|
@ -544,8 +561,6 @@ class ViewBox(GraphicsWidget):
|
|||
|
||||
# If nothing has changed, we are done.
|
||||
if any(changed):
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
# Update target rect for debugging
|
||||
if self.target.isVisible():
|
||||
self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
|
||||
|
@ -557,6 +572,8 @@ class ViewBox(GraphicsWidget):
|
|||
elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
|
||||
self._autoRangeNeedsUpdate = True
|
||||
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
def setYRange(self, min, max, padding=None, update=True):
|
||||
"""
|
||||
Set the visible Y range of the view to [*min*, *max*].
|
||||
|
@ -1014,7 +1031,10 @@ class ViewBox(GraphicsWidget):
|
|||
self.updateViewRange()
|
||||
self.update()
|
||||
self.sigStateChanged.emit(self)
|
||||
if ax:
|
||||
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax]))
|
||||
else:
|
||||
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][ax]))
|
||||
|
||||
def invertY(self, b=True):
|
||||
"""
|
||||
|
@ -1136,8 +1156,8 @@ class ViewBox(GraphicsWidget):
|
|||
|
||||
self._resetTarget()
|
||||
self.scaleBy(s, center)
|
||||
self.sigRangeChangedManually.emit(mask)
|
||||
ev.accept()
|
||||
self.sigRangeChangedManually.emit(mask)
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.RightButton and self.menuEnabled():
|
||||
|
@ -1146,6 +1166,7 @@ class ViewBox(GraphicsWidget):
|
|||
|
||||
def raiseContextMenu(self, ev):
|
||||
menu = self.getMenu(ev)
|
||||
if menu is not None:
|
||||
self.scene().addParentContextMenus(self, menu, ev)
|
||||
menu.popup(ev.screenPos().toPoint())
|
||||
|
||||
|
@ -1475,15 +1496,8 @@ class ViewBox(GraphicsWidget):
|
|||
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
||||
self.state['viewRange'] = viewRange
|
||||
|
||||
# emit range change signals
|
||||
if changed[0]:
|
||||
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
|
||||
if changed[1]:
|
||||
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
|
||||
|
||||
if any(changed):
|
||||
self._matrixNeedsUpdate = True
|
||||
self.sigRangeChanged.emit(self, self.state['viewRange'])
|
||||
self.update()
|
||||
|
||||
# Inform linked views that the range has changed
|
||||
|
@ -1494,6 +1508,13 @@ class ViewBox(GraphicsWidget):
|
|||
if link is not None:
|
||||
link.linkedViewChanged(self, ax)
|
||||
|
||||
# emit range change signals
|
||||
if changed[0]:
|
||||
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
|
||||
if changed[1]:
|
||||
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
|
||||
self.sigRangeChanged.emit(self, self.state['viewRange'])
|
||||
|
||||
def updateMatrix(self, changed=None):
|
||||
if not self._matrixNeedsUpdate:
|
||||
return
|
||||
|
@ -1521,9 +1542,9 @@ class ViewBox(GraphicsWidget):
|
|||
m.translate(-st[0], -st[1])
|
||||
|
||||
self.childGroup.setTransform(m)
|
||||
self._matrixNeedsUpdate = False
|
||||
|
||||
self.sigTransformChanged.emit(self) ## segfaults here: 1
|
||||
self._matrixNeedsUpdate = False
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
self.checkSceneChange()
|
||||
|
@ -1566,6 +1587,7 @@ class ViewBox(GraphicsWidget):
|
|||
if self in nv:
|
||||
nv.remove(self)
|
||||
|
||||
if self.menu is not None:
|
||||
self.menu.setViewList(nv)
|
||||
|
||||
for ax in [0,1]:
|
||||
|
|
|
@ -31,7 +31,6 @@ def init_viewbox():
|
|||
|
||||
g = pg.GridItem()
|
||||
vb.addItem(g)
|
||||
|
||||
app.processEvents()
|
||||
|
||||
def test_ViewBox():
|
||||
|
|
30
pyqtgraph/graphicsItems/tests/test_AxisItem.py
Normal file
30
pyqtgraph/graphicsItems/tests/test_AxisItem.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import pyqtgraph as pg
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
def test_AxisItem_stopAxisAtTick(monkeypatch):
|
||||
def test_bottom(p, axisSpec, tickSpecs, textSpecs):
|
||||
assert view.mapToView(axisSpec[1]).x() == 0.25
|
||||
assert view.mapToView(axisSpec[2]).x() == 0.75
|
||||
|
||||
def test_left(p, axisSpec, tickSpecs, textSpecs):
|
||||
assert view.mapToView(axisSpec[1]).y() == 0.875
|
||||
assert view.mapToView(axisSpec[2]).y() == 0.125
|
||||
|
||||
plot = pg.PlotWidget()
|
||||
view = plot.plotItem.getViewBox()
|
||||
bottom = plot.getAxis("bottom")
|
||||
bottom.setRange(0, 1)
|
||||
bticks = [(0.25, "a"), (0.6, "b"), (0.75, "c")]
|
||||
bottom.setTicks([bticks, bticks])
|
||||
bottom.setStyle(stopAxisAtTick=(True, True))
|
||||
monkeypatch.setattr(bottom, "drawPicture", test_bottom)
|
||||
|
||||
left = plot.getAxis("left")
|
||||
lticks = [(0.125, "a"), (0.55, "b"), (0.875, "c")]
|
||||
left.setTicks([lticks, lticks])
|
||||
left.setRange(0, 1)
|
||||
left.setStyle(stopAxisAtTick=(True, True))
|
||||
monkeypatch.setattr(left, "drawPicture", test_left)
|
||||
|
||||
plot.show()
|
37
pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py
Normal file
37
pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
|
||||
def test_ErrorBarItem_defer_data():
|
||||
plot = pg.PlotWidget()
|
||||
plot.show()
|
||||
|
||||
# plot some data away from the origin to set the view rect
|
||||
x = np.arange(5) + 10
|
||||
curve = pg.PlotCurveItem(x=x, y=x)
|
||||
plot.addItem(curve)
|
||||
app.processEvents()
|
||||
r_no_ebi = plot.viewRect()
|
||||
|
||||
# ErrorBarItem with no data shouldn't affect the view rect
|
||||
err = pg.ErrorBarItem()
|
||||
plot.addItem(err)
|
||||
app.processEvents()
|
||||
r_empty_ebi = plot.viewRect()
|
||||
|
||||
assert r_no_ebi == r_empty_ebi
|
||||
|
||||
err.setData(x=x, y=x, bottom=x, top=x)
|
||||
app.processEvents()
|
||||
r_ebi = plot.viewRect()
|
||||
|
||||
assert r_empty_ebi != r_ebi
|
||||
|
||||
# unset data, ErrorBarItem disappears and view rect goes back to original
|
||||
err.setData(x=None, y=None)
|
||||
app.processEvents()
|
||||
r_clear_ebi = plot.viewRect()
|
||||
|
||||
assert r_clear_ebi == r_no_ebi
|
|
@ -58,3 +58,9 @@ def test_clear():
|
|||
|
||||
assert pdi.xData == None
|
||||
assert pdi.yData == None
|
||||
|
||||
def test_clear_in_step_mode():
|
||||
w = pg.PlotWidget()
|
||||
c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True)
|
||||
w.addItem(c)
|
||||
c.clear()
|
||||
|
|
|
@ -633,7 +633,7 @@ class ImageView(QtGui.QWidget):
|
|||
ax = np.argmax(data.shape)
|
||||
sl = [slice(None)] * data.ndim
|
||||
sl[ax] = slice(None, None, 2)
|
||||
data = data[sl]
|
||||
data = data[tuple(sl)]
|
||||
|
||||
cax = self.axes['c']
|
||||
if cax is None:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os, time, sys, traceback, weakref
|
||||
import numpy as np
|
||||
import threading
|
||||
import warnings
|
||||
try:
|
||||
import __builtin__ as builtins
|
||||
import cPickle as pickle
|
||||
|
@ -21,6 +22,9 @@ class NoResultError(Exception):
|
|||
because the call has not yet returned."""
|
||||
pass
|
||||
|
||||
class RemoteExceptionWarning(UserWarning):
|
||||
"""Emitted when a request to a remote object results in an Exception """
|
||||
pass
|
||||
|
||||
class RemoteEventHandler(object):
|
||||
"""
|
||||
|
@ -499,9 +503,9 @@ class RemoteEventHandler(object):
|
|||
#print ''.join(result)
|
||||
exc, excStr = result
|
||||
if exc is not None:
|
||||
print("===== Remote process raised exception on request: =====")
|
||||
print(''.join(excStr))
|
||||
print("===== Local Traceback to request follows: =====")
|
||||
warnings.warn("===== Remote process raised exception on request: =====", RemoteExceptionWarning)
|
||||
warnings.warn(''.join(excStr), RemoteExceptionWarning)
|
||||
warnings.warn("===== Local Traceback to request follows: =====", RemoteExceptionWarning)
|
||||
raise exc
|
||||
else:
|
||||
print(''.join(excStr))
|
||||
|
|
|
@ -236,6 +236,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||
glPopMatrix()
|
||||
|
||||
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
|
||||
if pos is not None:
|
||||
self.opts['center'] = pos
|
||||
if distance is not None:
|
||||
self.opts['distance'] = distance
|
||||
if elevation is not None:
|
||||
|
@ -427,7 +429,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||
ver = glGetString(GL_VERSION).split()[0]
|
||||
if int(ver.split('.')[0]) < 2:
|
||||
from .. import debug
|
||||
pyqtgraph.debug.printExc()
|
||||
debug.printExc()
|
||||
raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
|
||||
else:
|
||||
raise
|
||||
|
|
|
@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget):
|
|||
def __init__(self):
|
||||
QtOpenGL.QGLWidget.__init__(self)
|
||||
self.makeCurrent()
|
||||
print("GL version:" + glGetString(GL_VERSION))
|
||||
print("GL version:" + glGetString(GL_VERSION).decode("utf-8"))
|
||||
print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE))
|
||||
print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE))
|
||||
print("Extensions: " + glGetString(GL_EXTENSIONS))
|
||||
print("Extensions: " + glGetString(GL_EXTENSIONS).decode("utf-8").replace(" ", "\n"))
|
||||
|
||||
GLTest()
|
||||
|
||||
|
|
|
@ -559,8 +559,8 @@ class Parameter(QtCore.QObject):
|
|||
self.childs.insert(pos, child)
|
||||
|
||||
child.parentChanged(self)
|
||||
self.sigChildAdded.emit(self, child, pos)
|
||||
child.sigTreeStateChanged.connect(self.treeStateChanged)
|
||||
self.sigChildAdded.emit(self, child, pos)
|
||||
return child
|
||||
|
||||
def removeChild(self, child):
|
||||
|
@ -571,11 +571,11 @@ class Parameter(QtCore.QObject):
|
|||
del self.names[name]
|
||||
self.childs.pop(self.childs.index(child))
|
||||
child.parentChanged(None)
|
||||
self.sigChildRemoved.emit(self, child)
|
||||
try:
|
||||
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
||||
except (TypeError, RuntimeError): ## already disconnected
|
||||
pass
|
||||
self.sigChildRemoved.emit(self, child)
|
||||
|
||||
def clearChildren(self):
|
||||
"""Remove all child parameters."""
|
||||
|
@ -612,7 +612,7 @@ class Parameter(QtCore.QObject):
|
|||
|
||||
def incrementName(self, name):
|
||||
## return an unused name by adding a number to the name given
|
||||
base, num = re.match('(.*)(\d*)', name).groups()
|
||||
base, num = re.match(r'(.*)(\d*)', name).groups()
|
||||
numLen = len(num)
|
||||
if numLen == 0:
|
||||
num = 2
|
||||
|
|
|
@ -10,8 +10,9 @@ Includes:
|
|||
- ThreadsafeDict, ThreadsafeList - Self-mutexed data structures
|
||||
"""
|
||||
|
||||
import threading, sys, copy, collections
|
||||
#from debug import *
|
||||
import threading
|
||||
import sys
|
||||
import copy
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
|
@ -19,6 +20,12 @@ except ImportError:
|
|||
# fallback: try to use the ordereddict backport when using python 2.6
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
try:
|
||||
from collections.abc import Sequence
|
||||
except ImportError:
|
||||
# fallback for python < 3.3
|
||||
from collections import Sequence
|
||||
|
||||
|
||||
class ReverseDict(dict):
|
||||
"""extends dict so that reverse lookups are possible by requesting the key as a list of length 1:
|
||||
|
@ -326,7 +333,7 @@ class ProtectedDict(dict):
|
|||
|
||||
|
||||
|
||||
class ProtectedList(collections.Sequence):
|
||||
class ProtectedList(Sequence):
|
||||
"""
|
||||
A class allowing read-only 'view' of a list or dict.
|
||||
The object can be treated like a normal list, but will never modify the original list it points to.
|
||||
|
@ -408,7 +415,7 @@ class ProtectedList(collections.Sequence):
|
|||
raise Exception("This is a list. It does not poop.")
|
||||
|
||||
|
||||
class ProtectedTuple(collections.Sequence):
|
||||
class ProtectedTuple(Sequence):
|
||||
"""
|
||||
A class allowing read-only 'view' of a tuple.
|
||||
The object can be treated like a normal tuple, but its contents will be returned as protected objects.
|
||||
|
|
|
@ -47,7 +47,7 @@ def reloadAll(prefix=None, debug=False):
|
|||
continue
|
||||
|
||||
## Ignore if the file name does not start with prefix
|
||||
if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']:
|
||||
if not hasattr(mod, '__file__') or mod.__file__ is None or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']:
|
||||
continue
|
||||
if prefix is not None and mod.__file__[:len(prefix)] != prefix:
|
||||
continue
|
||||
|
|
|
@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
|
||||
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
|
||||
except Exception:
|
||||
|
||||
if stdFileName in gitStatus(dataPath):
|
||||
print("\n\nWARNING: unit test failed against modified standard "
|
||||
"image %s.\nTo revert this file, run `cd %s; git checkout "
|
||||
|
@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
|
||||
else:
|
||||
if os.getenv('TRAVIS') is not None:
|
||||
saveFailedTest(image, stdImage, standardFile, upload=True)
|
||||
elif os.getenv('AZURE') is not None:
|
||||
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
|
||||
saveFailedTest(image, stdImage, standardFile)
|
||||
print(graphstate)
|
||||
raise
|
||||
|
@ -253,7 +257,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
|||
assert im1.dtype == im2.dtype
|
||||
|
||||
if pxCount == -1:
|
||||
if QT_LIB == 'PyQt5':
|
||||
if QT_LIB in {'PyQt5', 'PySide2'}:
|
||||
# Qt5 generates slightly different results; relax the tolerance
|
||||
# until test images are updated.
|
||||
pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
|
||||
|
@ -281,15 +285,9 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
|||
assert corr >= minCorr
|
||||
|
||||
|
||||
def saveFailedTest(data, expect, filename):
|
||||
def saveFailedTest(data, expect, filename, upload=False):
|
||||
"""Upload failed test images to web server to allow CI test debugging.
|
||||
"""
|
||||
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
|
||||
name = filename.split('/')
|
||||
name.insert(-1, commit.strip())
|
||||
filename = '/'.join(name)
|
||||
host = 'data.pyqtgraph.org'
|
||||
|
||||
# concatenate data, expect, and diff into a single image
|
||||
ds = data.shape
|
||||
es = expect.shape
|
||||
|
@ -306,15 +304,31 @@ def saveFailedTest(data, expect, filename):
|
|||
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
|
||||
|
||||
png = makePng(img)
|
||||
directory = os.path.dirname(filename)
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
with open(filename + ".png", "wb") as png_file:
|
||||
png_file.write(png)
|
||||
print("\nImage comparison failed. Test result: %s %s Expected result: "
|
||||
"%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
|
||||
if upload:
|
||||
uploadFailedTest(filename, png)
|
||||
|
||||
|
||||
def uploadFailedTest(filename, png):
|
||||
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
|
||||
name = filename.split(os.path.sep)
|
||||
name.insert(-1, commit.strip())
|
||||
filename = os.path.sep.join(name)
|
||||
|
||||
host = 'data.pyqtgraph.org'
|
||||
conn = httplib.HTTPConnection(host)
|
||||
req = urllib.urlencode({'name': filename,
|
||||
'data': base64.b64encode(png)})
|
||||
conn.request('POST', '/upload.py', req)
|
||||
response = conn.getresponse().read()
|
||||
conn.close()
|
||||
print("\nImage comparison failed. Test result: %s %s Expected result: "
|
||||
"%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
|
||||
|
||||
print("Uploaded to: \nhttp://%s/data/%s" % (host, filename))
|
||||
if not response.startswith(b'OK'):
|
||||
print("WARNING: Error uploading data to %s" % host)
|
||||
|
@ -495,7 +509,7 @@ def getTestDataRepo():
|
|||
if not os.path.isdir(parentPath):
|
||||
os.makedirs(parentPath)
|
||||
|
||||
if os.getenv('TRAVIS') is not None:
|
||||
if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None:
|
||||
# Create a shallow clone of the test-data repository (to avoid
|
||||
# downloading more data than is necessary)
|
||||
os.makedirs(dataPath)
|
||||
|
|
36
pyqtgraph/tests/test_configparser.py
Normal file
36
pyqtgraph/tests/test_configparser.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from pyqtgraph import configfile
|
||||
import numpy as np
|
||||
import tempfile, os
|
||||
|
||||
def test_longArrays():
|
||||
"""
|
||||
Test config saving and loading of long arrays.
|
||||
"""
|
||||
tmp = tempfile.mktemp(".cfg")
|
||||
|
||||
arr = np.arange(20)
|
||||
configfile.writeConfigFile({'arr':arr}, tmp)
|
||||
config = configfile.readConfigFile(tmp)
|
||||
|
||||
assert all(config['arr'] == arr)
|
||||
|
||||
os.remove(tmp)
|
||||
|
||||
def test_multipleParameters():
|
||||
"""
|
||||
Test config saving and loading of multiple parameters.
|
||||
"""
|
||||
tmp = tempfile.mktemp(".cfg")
|
||||
|
||||
par1 = [1,2,3]
|
||||
par2 = "Test"
|
||||
par3 = {'a':3,'b':'c'}
|
||||
|
||||
configfile.writeConfigFile({'par1':par1, 'par2':par2, 'par3':par3}, tmp)
|
||||
config = configfile.readConfigFile(tmp)
|
||||
|
||||
assert config['par1'] == par1
|
||||
assert config['par2'] == par2
|
||||
assert config['par3'] == par3
|
||||
|
||||
os.remove(tmp)
|
|
@ -10,7 +10,6 @@ def test_isQObjectAlive():
|
|||
o2 = pg.QtCore.QObject()
|
||||
o2.setParent(o1)
|
||||
del o1
|
||||
gc.collect()
|
||||
assert not pg.Qt.isQObjectAlive(o2)
|
||||
|
||||
@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be '
|
||||
|
|
|
@ -4,6 +4,7 @@ import pyqtgraph.reload
|
|||
|
||||
|
||||
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
|
||||
pgpath_repr = repr(pgpath)
|
||||
|
||||
# make temporary directory to write module code
|
||||
path = None
|
||||
|
@ -22,7 +23,7 @@ def teardown_module():
|
|||
|
||||
code = """
|
||||
import sys
|
||||
sys.path.append('{path}')
|
||||
sys.path.append({path_repr})
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
|
@ -47,7 +48,7 @@ def test_reload():
|
|||
# write a module
|
||||
mod = os.path.join(path, 'reload_test_mod.py')
|
||||
print("\nRELOAD FILE:", mod)
|
||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1"))
|
||||
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version1"))
|
||||
|
||||
# import the new module
|
||||
import reload_test_mod
|
||||
|
@ -63,7 +64,7 @@ def test_reload():
|
|||
|
||||
|
||||
# write again and reload
|
||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
||||
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2"))
|
||||
remove_cache(mod)
|
||||
pg.reload.reloadAll(path, debug=True)
|
||||
if py3:
|
||||
|
@ -87,7 +88,7 @@ def test_reload():
|
|||
|
||||
|
||||
# write again and reload
|
||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
||||
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2"))
|
||||
remove_cache(mod)
|
||||
pg.reload.reloadAll(path, debug=True)
|
||||
if py3:
|
||||
|
|
15
pyqtgraph/util/get_resolution.py
Normal file
15
pyqtgraph/util/get_resolution.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from .. import mkQApp
|
||||
|
||||
def test_screenInformation():
|
||||
qApp = mkQApp()
|
||||
desktop = qApp.desktop()
|
||||
resolution = desktop.screenGeometry()
|
||||
availableResolution = desktop.availableGeometry()
|
||||
print("Screen resolution: {}x{}".format(resolution.width(), resolution.height()))
|
||||
print("Available geometry: {}x{}".format(availableResolution.width(), availableResolution.height()))
|
||||
print("Number of Screens: {}".format(desktop.screenCount()))
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_screenInformation()
|
|
@ -50,11 +50,11 @@ class ColorButton(QtGui.QPushButton):
|
|||
def setColor(self, color, finished=True):
|
||||
"""Sets the button's color and emits both sigColorChanged and sigColorChanging."""
|
||||
self._color = functions.mkColor(color)
|
||||
self.update()
|
||||
if finished:
|
||||
self.sigColorChanged.emit(self)
|
||||
else:
|
||||
self.sigColorChanging.emit(self)
|
||||
self.update()
|
||||
|
||||
def selectColor(self):
|
||||
self.origColor = self.color()
|
||||
|
|
|
@ -227,13 +227,13 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||
else:
|
||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||
|
||||
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||
self.sigDeviceTransformChanged.emit(self)
|
||||
|
||||
if propagate:
|
||||
for v in self.lockedViewports:
|
||||
v.setXRange(self.range, padding=0)
|
||||
|
||||
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||
self.sigDeviceTransformChanged.emit(self)
|
||||
|
||||
def viewRect(self):
|
||||
"""Return the boundaries of the view in scene coordinates"""
|
||||
## easier to just return self.range ?
|
||||
|
@ -262,7 +262,6 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||
h = self.range.height() / scale[1]
|
||||
self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h)
|
||||
|
||||
|
||||
self.updateMatrix()
|
||||
self.sigScaleChanged.emit(self)
|
||||
|
||||
|
@ -362,7 +361,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||
def mouseMoveEvent(self, ev):
|
||||
if self.lastMousePos is None:
|
||||
self.lastMousePos = Point(ev.pos())
|
||||
delta = Point(ev.pos() - self.lastMousePos)
|
||||
delta = Point(ev.pos() - QtCore.QPoint(*self.lastMousePos))
|
||||
self.lastMousePos = Point(ev.pos())
|
||||
|
||||
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
||||
|
|
|
@ -39,7 +39,7 @@ class LayoutWidget(QtGui.QWidget):
|
|||
Returns the created widget.
|
||||
"""
|
||||
text = QtGui.QLabel(text, **kargs)
|
||||
self.addItem(text, row, col, rowspan, colspan)
|
||||
self.addWidget(text, row, col, rowspan, colspan)
|
||||
return text
|
||||
|
||||
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||
|
@ -49,7 +49,7 @@ class LayoutWidget(QtGui.QWidget):
|
|||
Returns the created widget.
|
||||
"""
|
||||
layout = LayoutWidget(**kargs)
|
||||
self.addItem(layout, row, col, rowspan, colspan)
|
||||
self.addWidget(layout, row, col, rowspan, colspan)
|
||||
return layout
|
||||
|
||||
def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1):
|
||||
|
@ -75,7 +75,7 @@ class LayoutWidget(QtGui.QWidget):
|
|||
|
||||
def getWidget(self, row, col):
|
||||
"""Return the widget in (*row*, *col*)"""
|
||||
return self.row[row][col]
|
||||
return self.rows[row][col]
|
||||
|
||||
#def itemIndex(self, item):
|
||||
#for i in range(self.layout.count()):
|
||||
|
|
|
@ -201,7 +201,7 @@ class TreeWidget(QtGui.QTreeWidget):
|
|||
return item
|
||||
|
||||
def topLevelItems(self):
|
||||
return map(self.topLevelItem, xrange(self.topLevelItemCount()))
|
||||
return [self.topLevelItem(i) for i in range(self.topLevelItemCount())]
|
||||
|
||||
def clear(self):
|
||||
items = self.topLevelItems()
|
||||
|
|
15
pytest.ini
Normal file
15
pytest.ini
Normal file
|
@ -0,0 +1,15 @@
|
|||
[pytest]
|
||||
xvfb_width = 1920
|
||||
xvfb_height = 1080
|
||||
# use this due to some issues with ndarray reshape errors on CI systems
|
||||
xvfb_colordepth = 24
|
||||
xvfb_args=-ac +extension GLX +render
|
||||
addopts = --faulthandler-timeout=15
|
||||
|
||||
filterwarnings =
|
||||
# comfortable skipping these warnings runtime warnings
|
||||
# https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility
|
||||
ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning
|
||||
# Warnings generated from PyQt5.9
|
||||
ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning
|
||||
ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning
|
6
test.py
6
test.py
|
@ -15,10 +15,10 @@ elif '--pyqt4' in args:
|
|||
elif '--pyqt5' in args:
|
||||
args.remove('--pyqt5')
|
||||
import PyQt5
|
||||
elif '--pyside2' in args:
|
||||
args.remove('--pyside2')
|
||||
import PySide2
|
||||
|
||||
import pyqtgraph as pg
|
||||
pg.systemInfo()
|
||||
|
||||
pytest.main(args)
|
||||
|
||||
|
51
tox.ini
Normal file
51
tox.ini
Normal file
|
@ -0,0 +1,51 @@
|
|||
[tox]
|
||||
envlist =
|
||||
; qt 5.12.x
|
||||
py{27,37}-pyside2-pip
|
||||
py{35,37}-pyqt5-pip
|
||||
|
||||
; qt 5.9.7
|
||||
py{27,37}-pyqt5-conda
|
||||
py{27,37}-pyside2-conda
|
||||
|
||||
; qt 5.6.2
|
||||
py35-pyqt5-conda
|
||||
; consider dropping support...
|
||||
; py35-pyside2-conda
|
||||
|
||||
; qt 4.8.7
|
||||
py{27,36}-pyqt4-conda
|
||||
py{27,36}-pyside-conda
|
||||
|
||||
|
||||
[base]
|
||||
deps =
|
||||
pytest
|
||||
numpy
|
||||
scipy
|
||||
pyopengl
|
||||
flake8
|
||||
six
|
||||
coverage
|
||||
|
||||
[testenv]
|
||||
passenv = DISPLAY XAUTHORITY
|
||||
deps=
|
||||
{[base]deps}
|
||||
pytest-cov
|
||||
pytest-xdist
|
||||
pytest-faulthandler
|
||||
pyside2-pip: pyside2
|
||||
pyqt5-pip: pyqt5
|
||||
|
||||
conda_deps=
|
||||
pyside2-conda: pyside2
|
||||
pyside-conda: pyside
|
||||
pyqt5-conda: pyqt
|
||||
pyqt4-conda: pyqt=4
|
||||
|
||||
conda_channels=
|
||||
conda-forge
|
||||
commands=
|
||||
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||
pytest {posargs:.}
|
Loading…
Reference in New Issue
Block a user