diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6f3596ac0..3f46ee995 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.1.0 +current_version = 4.3.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/.travis.yml b/.travis.yml index d82fb472e..b95076515 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,53 @@ +# Use old ubuntu image as a workarount to Tango DS launch problems +group: deprecated-2017Q4 + language: python +python: + - 3.6 + sudo: required +services: + - docker + +env: + global: + - secure: "CCVtBQ8xIrOwUJGXxd81wyD1ng72Hf6d9y2U+5X88aVGTrOa8/hut10C+Jmnyf0NTZmGh/49eVvoWRvLDhjpECFMuO/bLkiNtVjz0VtWAHT2W98QJYmeymPzx86tGa+iAZCwlgXeRQFJCw1eqQvBYnjumMZWb9kj3fqgpqpSRH5SWnuRCmbxOoelmtTTUC8YKkzasAHYs03faR0DCq0oBmDy9nU2cfcRN7oE5wXUfEnDwNaoHbiQA4wiJbzNpBV432bIDtzD7gsFdiIT6ExJVFHi1gWB32bGZdbiDPpej7I2fW5qunbzUXS5doVhoBU67qqhI241RJ+AOBVb+sQnF8gwwi9/5/mcORiSBX7yI59YsOXYwo2YW+8PGr3OlF3t0+z92Q3uPytUXysdtVO4dExnLbV8OEzgmWCkv2M3GIajjE3isYAaBItqSBJHJnRClza4nNg2WLwmqLPBgM4AuSUZEpB/8kbz9kTecVEb13WrlTCNc8KVRGR+EGa4KmWADwOOxurxeb/NsTteOnzdyfrP2TXKeGOkN2uqBGYJaI7OoefsgLG7VF/+Sz4MTETMs/gZojwpO6igKBS1sJlcXujz/kt125b8gcSnrAiU1TjbZIBew/D40H64tpBcuk+dqF6i6HCoV2QmZ1QEpHOSoDk9FEaKMlgKhQj59/cWcI8=" + + matrix: + - DOCKER_IMG=cpascual/taurus-test:debian-jessie + - DOCKER_IMG=cpascual/taurus-test:debian-stretch + - DOCKER_IMG=cpascual/taurus-test:debian-buster + matrix: - include: - - python: 2.7 - os: linux - sudo: required - services: - - docker - env: - - DOCKER_IMG=cpascual/taurus-test:latest - - - python: 2.7 - os: linux - sudo: required - services: - - docker - env: - - DOCKER_IMG=cpascual/taurus-test:debian9 + allow_failures: + - env: DOCKER_IMG=cpascual/taurus-test:debian-buster before_install: - # run cpascual/taurus-test docker container (Debian8 with taurus-deps and xvfb installed) - docker run -d --name=taurus-test -h taurus-test --volume=`pwd`:/taurus $DOCKER_IMG - sleep 10 script: - - docker exec taurus-test /bin/bash -c "cd taurus ; python setup.py install" - - docker exec taurus-test /bin/bash -c "TAURUS_STARTER_WAIT=5 taurustestsuite -e 'taurus\.core\.util\.test\.test_timer'" + - python -c "import fcntl; fcntl.fcntl(1, fcntl.F_SETFL, 0)" + - set -e + - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python setup.py install" + - docker exec -t taurus-test /bin/bash -c "TAURUS_STARTER_WAIT=5 taurustestsuite -e 'taurus\.core\.util\.test\.test_timer'" + - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python setup.py build_sphinx" + # deploy sphinx-built docs to taurus-doc repo + - if [[ "$DOCKER_IMG" == "cpascual/taurus-test:debian-stretch" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then + pip install doctr ; + docker exec taurus-test /bin/bash -c "touch /taurus/build/sphinx/html/.nojekyll" ; + if [[ "${TRAVIS_BRANCH}" == "develop" ]]; then + doctr deploy . ; + else + doctr deploy "v-$TRAVIS_BRANCH" ; + fi; + fi + +doctr: + key-path : ci/github_deploy_key.enc + deploy-repo : taurus-org/taurus-doc + require-master: false + sync: true + built-docs: build/sphinx/html/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 910cbd1e1..ba471e613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,62 @@ Note: changes in the [support-3.x] branch (which was split from the master branch after [3.7.1] and maintained in parallel to the develop branch) won't be reflected in this file. +## [4.3.0] - 2018-03-01 +### Deprecated +- taurus.core.tango.search +- TaurusMainWindow's "Change Tango Host" action (#379) + +### Added +- User Interface to set custom formatters (#564) +- Re-added `taurus.external.ordereddict` (#599) +- Option to ignore outdated Tango events (#559) +- Travis-built docs (not yet replacing the RTD ones) (#572) +- TaurusLed now supports non-boolean attributes (#617) +- Support for arbitrary bgRole in labels (#629) +- `--import-ascii` option in `taurusplot` launcher (#632) +- State and event support in TangoSchemeTest DS (#628, #655) +- Model info in widget tooltips (#640) +- (experimental) Delayed event subscription API (#605, #593) +- (experimental) Entry point for taurus.qt.qtgui extensions (#684) +- Support DevVoid in Tango-to-numpy type translation dicts (#666) +- `removeLogHandler` method to `Logger` class (#691) +- modelChooserDlg static method now accepts listedModels arg (#693) + +### Changed +- Treat unit="No unit" as unitless in Tango attributes (#662) +- taurus.qt widgets can now be used without installing PyTango (#590) +- Tango model name validators now always return FQDN instead of PQDN + for the tango host (#488, #589) +- Improved docs (#525, #540, #546, #548, #636) (thanks @PhilLAL !) +- Make spyder dependency optional (#556) + +### Fixed +- Wrong "missing units" warnings for non-numerical attributes (#580) +- Taurus3 backwards compatibility issues (#496, #550) +- False positives in taurus.check_dependencies (#612) +- Main Window Splash screen not showing (#595) +- TaurusTrend2DDialog not usable from designer (#597) +- Missing icons in buttons (#583, #598) +- Exception in TaurusCommandForm (#608) +- Launchers not showing output on MS Windows (#644) +- Various issues with input widgets (#623, #661, #663, #669, #674, #681) +- Exceptions in TaurusMessagePanel (#704) +- TangoAttribute receiving events after being deleted (#692) +- Regressions in: + - TaurusTrend (#618) + - TaurusGrid (#609) + - TaurusGUI edit with `taurusgui --new-gui` (#532) +- Epics scheme is now case sensitive (#694) +- [Many other issues](https://github.com/taurus-org/taurus/issues?utf8=%E2%9C%93&q=milestone%3AJan18%20label%3Abug%20) + +### Removed +- taurus.qt.qtgui.panel.taurusfilterpanel + +## [4.1.1] - 2017-07-21 + +### Fixed +- Issue with PyPI metadata (hotfix 4.1.1) + ## [4.1.0] - 2017-07-21 @@ -63,7 +119,7 @@ For a full log of commits since Jul16, run (in your git repo): `git log 4.0.1..4.0.3` ### Added -- Generic Attribute, Device and Authority getters in TaurusFactory` +- Generic Attribute, Device and Authority getters in TaurusFactory - spyder >=3 support (#343) - bumpversion support (for maintainers) (#347) - Contribution policy explicited in CONTRIBUTING.md @@ -89,7 +145,7 @@ For a full log of commits since Jul16, run (in your git repo): - Macrolistener (affects sardana) (#373) - Synoptics (#363) - TaurusValueLineEdit (#265) - - taurusgui.macrolistener` (#260) + - taurusgui.macrolistener (#260) - TaurusEditor (#343) - Bug causing high CPU usage in TaurusForms (#247) - Deprecation warnings in `TaurusWheelEdit` (#337) @@ -241,6 +297,8 @@ and several other places](https://sf.net/p/tauruslib/tickets/milestone/Jul15/) [TEP3]: http://www.taurus-scada.org/tep/?TEP3.md [TEP14]: http://www.taurus-scada.org/tep/?TEP14.md [Unreleased]: https://github.com/taurus-org/taurus/tree/develop +[4.3.0]: https://github.com/taurus-org/taurus/tree/4.3.0 +[4.1.1]: https://github.com/taurus-org/taurus/tree/4.1.1 [4.1.0]: https://github.com/taurus-org/taurus/tree/4.1.0 [4.0.3]: https://github.com/taurus-org/taurus/tree/4.0.3 [4.0.1]: https://github.com/taurus-org/taurus/tree/4.0.1 diff --git a/ci/github_deploy_key.enc b/ci/github_deploy_key.enc new file mode 100644 index 000000000..99431ba75 --- /dev/null +++ b/ci/github_deploy_key.enc @@ -0,0 +1 @@ +gAAAAABZuQvyDarhMR1dsxi0HwOZMmZuh3xh6R-Wm_7L1Ahk_pWGykQZlt0SBf2ywO2l0F44y09V8cuHLQk16B_DzESjxtwFVAjowCiMOYv75MUfXHbcPOiVQFeGzDmt9o9gHLO7U4lqiKl_LTKsbppbnpm-csItyNV1IW610nr-YTcfJEJHKrNdXOU94VqdJmpKl7u0HwRsoCeiC0Wdvs4RGc-IvEhn3fO4wKZCOiCCPLMBtvOcN3ASd3P00yv7x_P3cpxOymn-nEosLGT1R_CQaXnL1IeHahqARScYoKix-RLtDgwwsfVcWJeXhxrOtoTYQa0Q9gR84ubu3bfJJSuzWrbfxZgrIb00TPLGT6sOOcrQZdkJF84ABz_fz2FVkxxWTJ9tqDaamwQHWzGMmESddhrS4Y_QxDDKASFiTbAnzTgpQIxky4H8xnc3pfe-tAsJrJjTXzJ-whdrqUPjfzyDwkJywJiZiDtv3E32JYHRz_saxNBN8vjZVTh6FXJl92gR8hNHgT7webQg2ymcMmkxSiS4SxX8sfznmHfakKAy421yjQGtjQM0Zm7Id8DYtMd6J7obIo4bFpEbROBE3V92Vy1FQdAVKAGVYrMHDsZrxLZoypqLvy8GylGZxW0TslxCk2RfP_3PnLx6scroAdsaYebDv3iId1l7xUMYYx8fB0znJx9QFvLgUnQd1a0pDXGREaJIwoOYJKzy5kMgJ4WLof41uDgHYhA-M354d99UpzQZsdnyB2HeI-brkWSt6Xg_EUeC1S9f_egsBb42v0wznIna5ESYhAzOhGCSipEnBBMUH5-qUZNGIMtsXT59tHQXACp8kId5t_QcdxOgefxUBiz-rR3UKMo63F7IRfcj27OEdZ5rmY8EdgsLtkj9gtWz2TilLqMQEnec1NRfjOtKaPKLpQPs1KWQ-l7GGyyvgPcUiP4FYKAh_whGq1YeTIySXgMQM6fjeqQP1SMcUESLVkH2QFYzYR-zcqJx86HGV6wOMTbrC6G10sl3M5vrBPgPrULsw-5mB1gys4lhaAmfX0TIySwsqhSm0fpe9fxTH6Df1-FEOH8cEH7blQyyaoralpdfqRw4uQMTvjBiGYNn4WZFeC1InC9fFh46tZbZe_oJ7CPaSxkHnEkWkJFxpyW9UHbM-N4Tfozr0C1xCmMD17crTke4M_D2cXF_DN4ywz8VmLOuDdTTZTar9HF7BNrCGiRTGhUDiM4tpC1llrUvHO6Cy1qHrpNfFSJZ69lrwUeKg5Kthn6M6fDyFdKmF7LQcNDys2KCD-ctr5uTbMEHOG58CcB_kTYkzGRIVnRt1CQswGbJ-8dxgBppf_urhHeImp5w0sdvssZ6g7xr4TN203iiumv0ujdH4IdPzpR162pwAXHFtidY4KnbpXH9VxfikH1QRMG0ym0ToaMM_WyWMLCA6wbqFQ_-UMnBET89Q5_j-q9509HBY6FDZV1lz6PHEqTFHhDwZ4_kd91K0M0ohifHaieneU2TK4aklVh4zZslhF52kMhkTY9O-4EeL-cRBudMTvlT11VCj7EDiPvOve29dLoNUYy1p__Q5HLjC--o4ONIYexuQneStfVcHlA6HOOiYf2nUCDEhiLjPaIOJmJy5IP4LuOujXIl_bFMJjU3n9j5FheAvipxvZoi30HchB0zKHBLGv8O2KSUCCgqH0_BAmNIB7p9rakkzD0ttMOdnt-9gKQ2AL0HM27AYBZE7xi_wvqC1JN-W8Y0IioCiqwa3DXZCvNB-_2qZjsSIFmeDb9Qz3RWhG6AuensX6MpQ1mWkWdEYvkuC2D49Ce6LRQcomR7l_rjOrrOa0rVG_L2nr4C190qHqPgxt-k1rrW2Q2EZV4flxtlmgaTbSNO5jvPkeBKW8J32rty9kMc_PGshkWCkz8DpVeHgis4gnuwFT903TvJhFUFMVczUh7HipcM-3sJ__M1AbP8IfDMhWs4RpEwyG7kEn-bwGjcbSXihqfU_rvbBb_EHytBjEtUgfz30yH-UD0Q2Ig4a4c69__f9OLR34--RvrhykBAujBy6_Bt6gu0Ijig1H6zkSprJazDWTIrORdH3168aGKN0AvucJabL-iTJOmo4FTHLP68UgsLhckvnxoRmUWitctUa7qAbQBH-E3_S8Ndl0Y6rjZSVwnh-veYhBoazBV_IQAYKGJ1U9pXdzZ5EgsrMzOoxHutJ1uZpl5LOUv1wHRvquLlbbhp3tAFcobBYPSe5UrVu7MVvCtFUuxH-r8PYbEUTWJJVyE__xq7FIBjSy2Zkg8mjPWlNM9jXGfc7Tfnfcul41rlEcSbdM021eDHSD7M6KQv0CwcCMCptWftfRTricnW6qn29Zx2OKN7amGuN4fu2GmN14hjsmS-ZANGZd9rwOsIDBk8zWF8cbGXQN6jTzYoGPfmB822vPy8aeTTDpBJPDLdESxhISP_9KIx4M6mebKhaBnZ7VXFkjFZYVlftWffDvC56sCdHwE-DGM7UFh4ZNjx-zzT-fFTLjSzKapOqYU8Fz8rMzghsc9Qw2lBOAwb2qeMUTPE4goUOcV5SElvYMrUefQIr8DqTMw6lgba4AEzq21zHCTW1TDanjZI0WNI8teZYnMRIH9_3BP8bvZwaQpnhY0PdDy09xLirKE7RwP7VKqY8aZjp7TS3l5EDlOKnPjRHgLR_nmDfvUdelE2LINGz_k7jdIfCgl0u3_xYyeLz8NfHIB3zmfD59TPV67BO2bYV8xYnXqhiNC16fNzI_AxSujefb580J-a8WvOo0FAHa8xiXkLOARhQQ4Vp6oavah-BEfMoWEZwaig14ZIFbPM330jbJlfwy-nkb7z9HopooWsOvekVq4gYCXll2M0g5wC5rev86IQPxsZbBvogqUv7ZD6vFqiBo59FsNziN7jedMl3diCaaPPjPEDtU2IbMyt9b27OeBIodqCfIylTmjHUPv1-0XrZFF1WOcA26OkYDGwZ9Cwp6buGvHErmdG6KGCSim5il9uYD8CRHmY7g4PGQL6Px9YUARt9TMZAgxbwB9d4Rv7ALLVnA286kgpqj3xeRgRGE8VEtfwqaOzCl06cIij_nRa6ht--CDMah-IRrlC3QMN70339HthhgkBzEVcVS0WO8P1enaNwblM5htYNBcx48o_jVRl6CfvzN38hdXO8eYCASCf_5wmp_50yVjfWcRDckFnXr-WKzc85eIYiHLHkRr6JbXHWcfiFsFCzerKQnNI5fKQKC8Cs_bmkeO56AC7xfOYODgwQWokGhK_aiBfyJZM80i57I473bpjvVKmbXp3XcCOI3Ec_ajrgu_TrHievcn1EKUFYE14lVufIaqFEttN_P_CkQFzLL58-XBvbBzlfMIqEFWQwfJX2nVaSiEBJXKwrZb-PqhoX8YR0qo5MOya3zVVPCI5Qxko5DB9NsJvj8FdPzlp-mobCxqBt2Mn_MHY1p66xSGX-e7TLOrvVGVm3rkn6FWpJhcEEQxd0N3Te7h9fA7UpHygEW1kLoE130m-o1Uc-W0aiL0Zh1LUpENdVWTz5AI4qHS7dfZkBeAE-Qgv86uhV0mMOqTDepd8WqygaFTPFJpblNZX1DfozDjr4Ut8eYhg_DyDmIGKcbdpU5QlcXywTN4G87MY3zodhO-SV5b-8aWiiJOMgjlPPdtDaRHIEdZgWcY3inosO5OiwH_7wYihx5MriL3k35jVbFmxHZXeFsA0vxwQCkejRUcQ6WELOlMh4i2IZJ2WJYYrxwUjYCLIBHiWhfU6h7Ml6BT3jmVk4Om5HNf9WFlwKd4jY-8MsiEpkrUMH4KyGhC26OMr_s_WK9sLzF3wkUYtcOykI-Ql6Gmi3lfitpZnI0daeGs6jPianv9q5yBD6EGtlMRePQbt1SPI3ndBzkS4DlrTAP7-iSFvjNRq1__9Xt-g4TXdWf7w9_hK1syXI3tlP2uVtOEbm7uPZlpG-nhKKfvbOXG66zhbLJpgZRr9zIj_iWBBdZmaZkjOgQh980b-qEBzKX3vTFJkR-PI9gGq8jUMh5AULhX9DQ6E3Z8M0SPZj0-500gYF3LC2WT7U7FmXxIPCfFT7HZrkY_ezm0kDWlqCEo0BG1byvTnzZhhCo_eqcPMXKD80920IDbsSKjMW3hR-WUTwRHjLLzqI2f5_l0nzPpxhytDmhKDFyRtKC-IUrmNcC4ciYmCaCSsBX2QEBJf-k8dkZuzgqqPcrRgsh_dJ0EHcdysx6G7waBih3GzijR2ViQjZLhZtqltmM2ytcNkqLmTfsnNWUuHosckCbd7YVbanHmQ5bb7SUpHQYernrkQrK3uy_IbmzOSDIIMicM--30rSJ4_cQI_E_aLz_u6bqtbk08mrYgzxOKFe7yXB_dKPPqtUcrK4FdkkzQ1NzCEOYqkJXYQK3JnUSY= \ No newline at end of file diff --git a/doc/how_to_release.md b/doc/how_to_release.md index cacc6aaa0..c4036f999 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -8,19 +8,21 @@ of stuff that should be manually tested. 1. During all the development, use the Milestones to keep track of the intended release for each issue 2. Previous to the release deadline, re-check the open issues/PR and update the assignation issues/PR to the release milestone. Request feedback from the devel community. -3. Work to close all the PR/issues remaining open in the milestone. This can be either done in develop or in a release branch called `release-XXX` (where `XXX` is the milestone name, e.g. `Jul17`). If a release branch is used, `develop` is freed to continue with integrations that may not be suitable for this release. On the other hand, it adds a bit more work because the release-related PRs (which are done against the `release-XXX` branch), may need to be also merged to develop. Note: the `release-XXX` branch *can* live in the taurus-org repo or on a personal fork (in which case you should do step 4.4 and **now** to allow other integrators to push directly to it). +3. Work to close all the PR/issues remaining open in the milestone. This can be either done in develop or in a release branch called `release-XXX` (where `XXX` is the milestone name, e.g. `Jul17`). If a release branch is used, `develop` is freed to continue with integrations that may not be suitable for this release. On the other hand, it adds a bit more work because the release-related PRs (which are done against the `release-XXX` branch), may need to be also merged to develop. Note: the `release-XXX` branch *can* live in the taurus-org repo or on a personal fork (in which case you should do step 4.iv **now** to allow other integrators to push directly to it). 4. Create the release branch if it was not done already in the previous step and: -4.1. Review and update the CHANGELOG.md if necessary. See [this](http://keepachangelog.com) -4.2. Bump version using `bumpversion && bumpversion release` (use [semver](http://semver.org/) criteria to choose amongst `major`, `minor` or `patch` -4.3. Update man pages: - `cd /doc` - `./makeman` - `git add man` - `git commit -m "Update man pages"` -4.4. Create a PR to merge the `release-XXX` against the **`master`** branch of the taurus-org repo + 1. Review and update the CHANGELOG.md if necessary. See [this](http://keepachangelog.com) + 2. Bump version using `bumpversion && bumpversion release` (use [semver](http://semver.org/) criteria to choose amongst `major`, `minor` or `patch` + 3. Update man pages: + ``` + cd /doc + ./makeman + git add man + git commit -m "Update man pages" + ``` + 4. Create a PR to merge the `release-XXX` against the **`master`** branch of the taurus-org repo 5. Request reviews in the PR from at least one integrator from each participating institute. The master branch is protected, so the reviews need to be cleared (or dismissed with an explanation) before the release can be merged. 6. Perform manual tests (see checklist below). You may use the CI artifacts (e.g., from appveyor) and post the results in the comments of the PR. -7. Once all reviews a cleared, merge the PR and tag in master +7. Once all reviews a cleared, update the date of the release in the CHANGELOG.md, merge the PR and tag in master 8. Merge also the `release-XXX` branch into develop, and bump the version of develop with `bumpversion patch` @@ -152,4 +154,4 @@ Hint: this list can be used as a template to be copy-pasted on a release PR ### taurusiconcatalog - [ ] Launch `taurusiconcatalog`. Several tabs with an array of icons [should be displayed](http://taurus-scada.org/en/latest/devel/icon_guide.html#taurus-icon-catalog) - [ ] Check that tooltips give info on each icon -- [ ] Click on some icons and check that they give a bigger view of the icon and more info. \ No newline at end of file +- [ ] Click on some icons and check that they give a bigger view of the icon and more info. diff --git a/doc/man/taurusconfigbrowser.1 b/doc/man/taurusconfigbrowser.1 index 2eb0910e7..a409d4426 100644 --- a/doc/man/taurusconfigbrowser.1 +++ b/doc/man/taurusconfigbrowser.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSCONFIGEDITOR "1" "July 2017" "taurusconfigeditor 4.1.0" "User Commands" +.TH TAURUSCONFIGEDITOR "1" "February 2018" "taurusconfigeditor 4.3.0" "User Commands" .SH NAME -taurusconfigeditor \- manual page for taurusconfigeditor 4.1.0 +taurusconfigeditor \- manual page for taurusconfigeditor 4.3.0 .SH SYNOPSIS .B taurusconfigbrowser [\fI\,options\/\fR] [\fI\,INIFILENAME\/\fR] @@ -37,3 +37,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/tauruscurve.1 b/doc/man/tauruscurve.1 index f0ac137a3..3e1afc260 100644 --- a/doc/man/tauruscurve.1 +++ b/doc/man/tauruscurve.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUS "1" "July 2017" "Taurus Curve Dialog 4.1.0" "User Commands" +.TH TAURUS "1" "February 2018" "Taurus Curve Dialog 4.3.0" "User Commands" .SH NAME -Taurus \- manual page for Taurus Curve Dialog 4.1.0 +Taurus \- manual page for Taurus Curve Dialog 4.3.0 .SH SYNOPSIS .B tauruscurve [\fI\,options\/\fR] [\fI\, \/\fR[\fI\,\/\fR] ...] @@ -43,3 +43,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusdemo.1 b/doc/man/taurusdemo.1 index 738adc42a..5d22067ad 100644 --- a/doc/man/taurusdemo.1 +++ b/doc/man/taurusdemo.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSDEMO "1" "July 2017" "taurusdemo 1.0" "User Commands" +.TH TAURUSDEMO "1" "February 2018" "taurusdemo 1.0" "User Commands" .SH NAME taurusdemo \- manual page for taurusdemo 1.0 .SH SYNOPSIS @@ -37,3 +37,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusdesigner.1 b/doc/man/taurusdesigner.1 index dae6a8575..da49b11f8 100644 --- a/doc/man/taurusdesigner.1 +++ b/doc/man/taurusdesigner.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSDESIGNER "1" "July 2017" "taurusdesigner 4.1.0" "User Commands" +.TH TAURUSDESIGNER "1" "February 2018" "taurusdesigner 4.3.0" "User Commands" .SH NAME -taurusdesigner \- manual page for taurusdesigner 4.1.0 +taurusdesigner \- manual page for taurusdesigner 4.3.0 .SH SYNOPSIS .B taurusdesigner [\fI\,options\/\fR] \fI\,\/\fR diff --git a/doc/man/taurusdevicepanel.1 b/doc/man/taurusdevicepanel.1 index 9bece2b35..a4cab30f3 100644 --- a/doc/man/taurusdevicepanel.1 +++ b/doc/man/taurusdevicepanel.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSDEVICEPANEL "1" "July 2017" "TaurusDevicePanel 4.1.0" "User Commands" +.TH TAURUSDEVICEPANEL "1" "February 2018" "TaurusDevicePanel 4.3.0" "User Commands" .SH NAME -TaurusDevicePanel \- manual page for TaurusDevicePanel 4.1.0 +TaurusDevicePanel \- manual page for TaurusDevicePanel 4.3.0 .SH SYNOPSIS .B taurusdevicepanel [\fI\,options\/\fR] [\fI\,devname \/\fR[\fI\,attrs\/\fR]] @@ -40,3 +40,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusform.1 b/doc/man/taurusform.1 index 971735dd0..cab9c121e 100644 --- a/doc/man/taurusform.1 +++ b/doc/man/taurusform.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSFORM "1" "July 2017" "taurusform 4.1.0" "User Commands" +.TH TAURUSFORM "1" "February 2018" "taurusform 4.3.0" "User Commands" .SH NAME -taurusform \- manual page for taurusform 4.1.0 +taurusform \- manual page for taurusform 4.3.0 .SH SYNOPSIS .B taurusform [\fI\,options\/\fR] [\fI\,model1 \/\fR[\fI\,model2 \/\fR...]] @@ -43,3 +43,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusgui.1 b/doc/man/taurusgui.1 index da209abe1..fb61f43bd 100644 --- a/doc/man/taurusgui.1 +++ b/doc/man/taurusgui.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSGUI "1" "July 2017" "taurusgui 4.1.0" "User Commands" +.TH TAURUSGUI "1" "February 2018" "taurusgui 4.3.0" "User Commands" .SH NAME -taurusgui \- manual page for taurusgui 4.1.0 +taurusgui \- manual page for taurusgui 4.3.0 .SH SYNOPSIS .B taurusgui [\fI\,options\/\fR] \fI\,confname\/\fR @@ -49,3 +49,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusiconcatalog.1 b/doc/man/taurusiconcatalog.1 index 275389f58..2a97ea087 100644 --- a/doc/man/taurusiconcatalog.1 +++ b/doc/man/taurusiconcatalog.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSICONCATALOG "1" "July 2017" "taurusiconcatalog 4.1.0" "User Commands" +.TH TAURUSICONCATALOG "1" "February 2018" "taurusiconcatalog 4.3.0" "User Commands" .SH NAME -taurusiconcatalog \- manual page for taurusiconcatalog 4.1.0 +taurusiconcatalog \- manual page for taurusiconcatalog 4.3.0 .SH SYNOPSIS .B taurusiconcatalog [\fI\,options\/\fR] @@ -35,3 +35,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusimage.1 b/doc/man/taurusimage.1 index 9220d1757..2b611af5c 100644 --- a/doc/man/taurusimage.1 +++ b/doc/man/taurusimage.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUS "1" "July 2017" "Taurus Image Dialog 4.1.0" "User Commands" +.TH TAURUS "1" "February 2018" "Taurus Image Dialog 4.3.0" "User Commands" .SH NAME -Taurus \- manual page for Taurus Image Dialog 4.1.0 +Taurus \- manual page for Taurus Image Dialog 4.3.0 .SH SYNOPSIS .B taurusimage [\fI\,options\/\fR] \fI\,\/\fR @@ -46,3 +46,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/tauruspanel.1 b/doc/man/tauruspanel.1 index 6b1048c06..9705988d6 100644 --- a/doc/man/tauruspanel.1 +++ b/doc/man/tauruspanel.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSPANEL "1" "July 2017" "tauruspanel 4.1.0" "User Commands" +.TH TAURUSPANEL "1" "February 2018" "tauruspanel 4.3.0" "User Commands" .SH NAME -tauruspanel \- manual page for tauruspanel 4.1.0 +tauruspanel \- manual page for tauruspanel 4.3.0 .SH SYNOPSIS .B tauruspanel [\fI\,options\/\fR] [\fI\,devname\/\fR] @@ -37,3 +37,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurusplot.1 b/doc/man/taurusplot.1 index 18f9669ac..e43fd6e45 100644 --- a/doc/man/taurusplot.1 +++ b/doc/man/taurusplot.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSPLOT "1" "July 2017" "taurusplot 4.1.0" "User Commands" +.TH TAURUSPLOT "1" "February 2018" "taurusplot 4.3.0" "User Commands" .SH NAME -taurusplot \- manual page for taurusplot 4.1.0 +taurusplot \- manual page for taurusplot 4.3.0 .SH SYNOPSIS .B taurusplot [\fI\,options\/\fR] [\fI\, \/\fR[\fI\,\/\fR] ...] @@ -20,6 +20,9 @@ as a synonim of n) \fB\-\-config\fR=\fI\,CONFIG_FILE\/\fR, \fB\-\-config\-file\fR=\fI\,CONFIG_FILE\/\fR use the given config file for initialization .TP +\fB\-\-import\-ascii\fR=\fI\,IMPORT_ASCII\/\fR +import the given ascii file into the plot +.TP \fB\-\-export\fR=\fI\,EXPORT_FILE\/\fR, \fB\-\-export\-file\fR=\fI\,EXPORT_FILE\/\fR use the given file to as output instead of showing the plot @@ -52,3 +55,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurustestsuite.1 b/doc/man/taurustestsuite.1 index b2d248c3b..0cc7b36d9 100644 --- a/doc/man/taurustestsuite.1 +++ b/doc/man/taurustestsuite.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSTESTSUITE "1" "July 2017" "taurustestsuite 4.1.0" "User Commands" +.TH TAURUSTESTSUITE "1" "February 2018" "taurustestsuite 4.3.0" "User Commands" .SH NAME -taurustestsuite \- manual page for taurustestsuite 4.1.0 +taurustestsuite \- manual page for taurustestsuite 4.3.0 .SH DESCRIPTION usage: taurustestsuite [\-h] [\-\-skip\-gui\-tests] [\-e EXCLUDE_PATTERN] .IP diff --git a/doc/man/taurustrend.1 b/doc/man/taurustrend.1 index df56fbbce..b34a92787 100644 --- a/doc/man/taurustrend.1 +++ b/doc/man/taurustrend.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUSTREND "1" "July 2017" "taurustrend 4.1.0" "User Commands" +.TH TAURUSTREND "1" "February 2018" "taurustrend 4.3.0" "User Commands" .SH NAME -taurustrend \- manual page for taurustrend 4.1.0 +taurustrend \- manual page for taurustrend 4.3.0 .SH SYNOPSIS .B taurustrend [\fI\,options\/\fR] [\fI\, \/\fR[\fI\,\/\fR] ...] @@ -62,3 +62,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurustrend1d.1 b/doc/man/taurustrend1d.1 index e2cadd49b..2cf81f6b2 100644 --- a/doc/man/taurustrend1d.1 +++ b/doc/man/taurustrend1d.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUS "1" "July 2017" "Taurus Trend 4.1.0" "User Commands" +.TH TAURUS "1" "February 2018" "Taurus Trend 4.3.0" "User Commands" .SH NAME -Taurus \- manual page for Taurus Trend 4.1.0 +Taurus \- manual page for Taurus Trend 4.3.0 .SH SYNOPSIS .B taurustrend1d [\fI\,options\/\fR] \fI\,\/\fR @@ -53,3 +53,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/man/taurustrend2d.1 b/doc/man/taurustrend2d.1 index 2a9b7c2ab..f1a2a1ec5 100644 --- a/doc/man/taurustrend2d.1 +++ b/doc/man/taurustrend2d.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH TAURUS "1" "July 2017" "Taurus Trend 2D 4.1.0" "User Commands" +.TH TAURUS "1" "February 2018" "Taurus Trend 2D 4.3.0" "User Commands" .SH NAME -Taurus \- manual page for Taurus Trend 2D 4.1.0 +Taurus \- manual page for Taurus Trend 2D 4.3.0 .SH SYNOPSIS .B taurustrend2d [\fI\,options\/\fR] \fI\,\/\fR @@ -53,3 +53,6 @@ e.g. tango://foo:1234) .TP \fB\-\-remote\-console\-port\fR=\fI\,PORT\/\fR enables remote debugging using the given port +.TP +\fB\-\-default\-formatter\fR=\fI\,FORMATTER\/\fR +Override the default formatter diff --git a/doc/source/devel/core_tutorial.rst b/doc/source/devel/core_tutorial.rst index 4398e1c9b..83e0d4e03 100644 --- a/doc/source/devel/core_tutorial.rst +++ b/doc/source/devel/core_tutorial.rst @@ -136,8 +136,8 @@ model name: - :func:`taurus.Attribute` - :func:`taurus.Object` -The first four helpers require you to know which type of Element (e.g., -Attribute, Device,...) is represented by the model name. If you do not know that +The first three helpers require you to know which type of Element (i.e., +Attribute, Device or Authority) is represented by the model name. If you do not know that beforehand, you can use :meth:`taurus.Object` which will automatically find the type and provide you with the corresponding model object (but of course this is slightly less efficient than using one of the first three helpers). @@ -237,4 +237,4 @@ the most important ones. .. _Tango: http://www.tango-controls.org/ .. _PyTango: http://packages.python.org/PyTango/ .. _EPICS: http://www.aps.anl.gov/epics/ -.. _RFC3986: https://tools.ietf.org/html/rfc3986 \ No newline at end of file +.. _RFC3986: https://tools.ietf.org/html/rfc3986 diff --git a/doc/source/devel/tauruscustomsettings.rst b/doc/source/devel/tauruscustomsettings.rst index 274d0cbaf..8247a8c7c 100644 --- a/doc/source/devel/tauruscustomsettings.rst +++ b/doc/source/devel/tauruscustomsettings.rst @@ -7,7 +7,10 @@ Taurus custom settings Taurus provides a module located at its root directory called -`tauruscustomsettings`. +`tauruscustomsettings` which stores global configuration options. +It can be modified permanently so that it affects all applications +(use with care), or accessed at run time for setting options for +the current execution. .. automodule:: taurus.tauruscustomsettings :members: diff --git a/doc/source/index.rst b/doc/source/index.rst index 337b645cc..f526eceb3 100755 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,12 +1,22 @@ +.. toctree:: + :hidden: + :titlesonly: + :maxdepth: 4 -Welcome to Taurus's Home Page! + Home Page + Project Page + Download from PyPI + docs + + +Welcome to Taurus Home Page! ============================================= |image1| Taurus is a python framework for control and data acquisition CLIs and GUIs in scientific/industrial environments. -It supports multiple control systems or data sources: Tango_, EPICS_, spec... +It supports multiple control systems or data sources: Tango_, EPICS_, ... New control system libraries can be integrated through plugins. For non-programmers: Taurus allows the creation of fully-featured GUI (with @@ -22,26 +32,14 @@ Of course, Taurus is Free Software (under LGPL). You can download it from PyPi_, access its Documentation_ or get support from its community and the latest code from the `project page `_. -Projects related to Taurus ---------------------------- - -- Taurus uses PyQt_ for the GUIs -- Tango_ is supported vis PyTango_ -- Taurus is part of the Sardana_ suite +See also the related Sardana_ project, which uses Taurus to build its user +interfaces. .. |image1| image:: _static/taurus_showcase01.png :align: middle :height: 180 -.. toctree:: - :hidden: - - Home Page - Project Page - Download from PyPI - docs - :Last Update: |today| :Release: |release| @@ -76,7 +74,6 @@ Projects related to Taurus .. _Tango: http://www.tango-controls.org/ -.. _PyTango: http://packages.python.org/PyTango/ .. _EPICS: http://www.aps.anl.gov/epics/ .. _PyQt: http://www.riverbankcomputing.co.uk/software/pyqt/ .. _Sardana: http://sardana-controls.org diff --git a/doc/source/tep/TEP14.md b/doc/source/tep/TEP14.md index 918aa6797..d5713ec5b 100644 --- a/doc/source/tep/TEP14.md +++ b/doc/source/tep/TEP14.md @@ -70,12 +70,12 @@ DataType | _0D | _1D | _2D ------------------- | ------------ | ------------------- | ---------------------- Boolean | bool | `ndarray` | `ndarray` Integer, Float | Quantity | Quantity | Quantity -String | str | seq | `seq>` +String | str | `seq` | `seq>` Bytes | bytes | | Some important remarks: -- units is no longer a common member of the TaurusAttribute. Each each physical value, being represented as a Quantity object, has its own units (e.g. the rvalue, wvalue or the range values units are not necessarily identical, although they should be compatible among them) +- units is no longer a common member of the TaurusAttribute. Each physical value, being represented as a Quantity object, has its own units (e.g. the rvalue, wvalue or the range values units are not necessarily identical, although they should be compatible among them) - The use of Quantity objects blurs the difference between integer and float numbers because unit transformations may transform a quantity whose magnitude initially was an integer into a "float"-based quantity. But this is not essentially different from the implicit type conversion between int and float objects. - while numpy arrays are the only allowed types for representing non-scalar booleans (and the same for integers and float within Quantities), we do **not** require numpy arrays for arrays of strings because `numpy.ndarray` imposes fixed lengths of its items. Instead, for strings we recomend using (nested) lists of `str`. diff --git a/doc/source/users/introduction.rst b/doc/source/users/introduction.rst index 3523dcdaf..a8526ff31 100644 --- a/doc/source/users/introduction.rst +++ b/doc/source/users/introduction.rst @@ -13,7 +13,7 @@ systems (such as EPICS_) and data sources. Tango-related examples. We intend to gradually introduce more non-Tango examples -Taurus was developed within the Sardana_ project, but since it has being found +Taurus was developed within the Sardana_ project, but since it has been found useful for other projects not related to Sardana, it has been moved to a separate project (although both projects are kept in sync and share most of their developers). diff --git a/doc/source/users/ui/arrayeditor.rst b/doc/source/users/ui/arrayeditor.rst index 0bd3c5ce5..40d972e0c 100644 --- a/doc/source/users/ui/arrayeditor.rst +++ b/doc/source/users/ui/arrayeditor.rst @@ -14,7 +14,7 @@ The :class:`ArrayEditor` is a widget for graphically editing a spectrum. It consists of two :ref:`plots ` and a *control point area*. The plot on top shows the current and modified spectrum. The other plot shows the difference between the current and the modified spectrum. The Control point -area shows details on the conrol points that have been defined. +area shows details on the control points that have been defined. The spectrum can be modified by adding control points and moving them along the vertical axis, either by setting the value in the controls area or by @@ -29,4 +29,4 @@ controls area. The arrow buttons in the controls area will help in propagating the related value to the other control points to the left or to the right of the selected -one. \ No newline at end of file +one. diff --git a/doc/source/users/ui/index.rst b/doc/source/users/ui/index.rst index 02dde868f..b571d3f4b 100644 --- a/doc/source/users/ui/index.rst +++ b/doc/source/users/ui/index.rst @@ -4,6 +4,11 @@ User's Interface ================ +This section explains some features that are common to most applications built +with taurus. This is done from the GUI user point of view (not for developers). + +For a detailed list of features of each widget, please refer to the +:ref:`developers-guide-index` .. toctree:: :maxdepth: 2 @@ -21,12 +26,4 @@ User's Interface TaurusDemo Logs Configurations - - - -This section explains some features that are common to most applications built -with taurus. This is done from the GUI user point of view (not for developers). - -For a detailed list of features of each widget, please refer to the -:ref:`developers-guide-index` diff --git a/doc/source/users/ui/plot.rst b/doc/source/users/ui/plot.rst index 0bacef699..53aafe6d8 100644 --- a/doc/source/users/ui/plot.rst +++ b/doc/source/users/ui/plot.rst @@ -148,6 +148,8 @@ where you can add data sets to the plot from the following sources: You can use `x` as a dependent variable, which you can set as regular steps or using an arbitrary expression. +Note that there is actually no way to remove RawData curve from the GUI. + .. figure:: /_static/taurusplot-inputdata02.png :align: center @@ -157,7 +159,7 @@ Storing and recovering current configuration Once you have customized the way the plot looks (see the `Plot Configuration dialog`_ section), you may want to save the settings for -later use. This can be done using the `Store current settings` option from the +later use. This can be done using the `Save current settings` option from the `TaurusPlot context menu`_. This will save which curves should be plotted and how they should look. @@ -192,7 +194,7 @@ features can be useful: - Peak locator: :class:`TaurusPlot` can locate and put a mark at the maximum and/or minimum points in the plotted data. You switch this option on and off using the - `Show Peaks` option from the `TaurusPlot context menu`_ or use from the + `Show min` and `Show max` option from the `TaurusPlot context menu`_ or use from the `Peak Markers` option in the `Plot Configuration dialog`_ .. image:: /_static/taurusplot-datainfo03.png @@ -266,9 +268,8 @@ Here are some tips for entering valid date/time values: - The date can be written in various formats. ISO format is recommended (e.g. "1917-10-25"), although others like, e.g. "25/10/1917" are also accepted. - - The time is given in 24 hours format (e.g. "21:45") and may include - (e.g. "21:45:01") secondshours:minutes , if given, may optionally - include seconds: e.g., is + - The time is given in 24 hours format (e.g. "21:45") and may optionnaly + include seconds if given (e.g. "21:45:01") - Date is mandatory while time is optional. If time is given, it must be separated from the date with a single space (e.g. "1917-10-25 21:45:01") diff --git a/doc/source/users/ui/taurusgui.rst b/doc/source/users/ui/taurusgui.rst index d678ef9b8..e75cea59f 100644 --- a/doc/source/users/ui/taurusgui.rst +++ b/doc/source/users/ui/taurusgui.rst @@ -151,10 +151,14 @@ want to extend the application by adding custom panels to provide more features (e.g., to add an extra plot panel, or a new form). You can add a new panel by clicking in the "New Panel" button of the main tool -bar (or selecting `View->Panel->New Panel...`). This will open a dialog offering +bar (or selecting `Panels->New Panel...`). This will open a dialog offering a catalog of different panel types and options for your new panel. Once accepted, the new panel will appear floating, ready to be docked to the main window. +.. tip:: if you are interested in creating a panel different from those offered in + the catalog, you can do so by using the "other..." button and manually + selecting the module and widget class of your choice. + .. figure:: /_static/taurusgui-newpanel01.png :align: center @@ -174,10 +178,10 @@ future use and which are only meant for the current session. The dialog for selection which custom panels are to be stored permanently -You can also open this dialog from the `Tools->Select panel Configuration` option. +You can also open this dialog from the `Panels->Switch temporary/permanent status...` option. .. tip:: if you want to remove a custom panel from an application, just hide it - and use the `Select panel Configuration` option for making it + and use the `Switch temporary/permanent status...` option for making it "temporary" so that it is not stored for future sessions. .. _perspectives: @@ -203,9 +207,9 @@ functionalities from the `View` menu). shows all available perspectives. The button on the right allows you to save the current arrangement as a perspective. -Switching to another perspective can be done using the `Load perspectives` drop- -down button in the perspectives toolbar (or using the `View->Load perspective` -option). +Switching to another perspective can be done using the `Load perspectives` +drop-down button in the perspectives toolbar (or using the +`View->Load perspective` option). Apart from the pre-defined perspectives, you can always re-arrange panels and store your preferred arrangement as a perspective using the @@ -214,8 +218,8 @@ perspective` option). .. tip:: If you want to backup your current perspectives, or you want to use some perspectives you created in one computer in another computer (or another - user of the same computer) you can do so by using the `File->Export Settings` - option. Similarly, use the `File->Import Settings` option to update the application + user of the same computer) you can do so by using the `File->Export Settings ...` + option. Similarly, use the `File->Import Settings ...` option to update the application perspectives with those contained in the imported file. .. _synopticpanels: @@ -223,7 +227,7 @@ perspective` option). Synoptic panels --------------- -An special type of panel present in some TaurusGui-based applications is the +A special type of panel present in some TaurusGui-based applications is the Synoptics. Synoptics panels are typically used for providing a visual representation of the hardware that is controlled by the GUI. Some elements or areas of the synoptic panel may be *active*, meaning that they can be selected @@ -236,6 +240,11 @@ element in the synoptic will be highlighted). This is very useful because synoptic panels can be used as a sort of quick index or browser to navigate in panel-crowded applications. +To add a Synoptic panel to a taurusgui after the creation of the taurusgui, +use the "Add Panel" button (or menu), select the "TaurusJDrawSynopticsView", +enter "Advanced settings..." to enter the full path of the JDraw file +into the "Model" field. + Also note that you can find a button in the application toolbar for showing/hiding each synoptic panel. @@ -308,7 +317,7 @@ Also, some other temporary panels may be dynamically created depending on the ex In most specific GUIs the macroserver and door name to use are pre-configured and the user does not need to change them. Sometimes though, you may want to alter it. -You can do so by using the `Tools->Macro execution configuration` option. +You can do so by using the `Taurus->Macro execution configuration...` option. Automatic creation of Instrument panels from Pool info '''''''''''''''''''''''''''''''''''''''''''''''''''''' @@ -355,4 +364,4 @@ width or height in any of the available "slots". Try to make room by hiding some other panel, or tabifying other panels together, or increasing the main window size. -.. _Sardana: http://www.sardana-controls.org/ \ No newline at end of file +.. _Sardana: http://www.sardana-controls.org/ diff --git a/lib/taurus/core/epics/epicsfactory.py b/lib/taurus/core/epics/epicsfactory.py index 5b01b0f88..aeda48f34 100644 --- a/lib/taurus/core/epics/epicsfactory.py +++ b/lib/taurus/core/epics/epicsfactory.py @@ -57,7 +57,7 @@ class EpicsFactory(Singleton, TaurusFactory, Logger): schemes = ("ca", "epics",) DEFAULT_DEVICE = 'ca:' DEFAULT_AUTHORITY = 'ca://' - caseSensitive = False + caseSensitive = True elementTypesMap = {TaurusElementType.Authority: EpicsAuthority, TaurusElementType.Device: EpicsDevice, TaurusElementType.Attribute: EpicsAttribute diff --git a/lib/taurus/core/epics/test/test_epicsattribute.py b/lib/taurus/core/epics/test/test_epicsattribute.py index 463037976..06caa0cc1 100755 --- a/lib/taurus/core/epics/test/test_epicsattribute.py +++ b/lib/taurus/core/epics/test/test_epicsattribute.py @@ -54,9 +54,7 @@ wvalue=Quantity('1m'), quality=AttrQuality.ATTR_VALID, error=None, - ), - test_skip='There are troubles in the docker container. ' - 'This test will be skipped till we fix it' + ) ) @unittest.skipIf(sys.modules.has_key('epics') is False, "epics module is not available") diff --git a/lib/taurus/core/evaluation/__init__.py b/lib/taurus/core/evaluation/__init__.py index 1b4acce28..7dad996f0 100644 --- a/lib/taurus/core/evaluation/__init__.py +++ b/lib/taurus/core/evaluation/__init__.py @@ -53,7 +53,7 @@ where: - The `` segment is optional (except when referring to an - EvaluationDatabase). At this point, only `//localhost` is supported. + EvaluationAuthority). At this point, only `//localhost` is supported. - The `@` is optional (except when referring to an EvaluationDevice). If not given, it defaults to `DefaultEvaluator`. See @@ -172,7 +172,7 @@ class MyClass(object): .. note:: Previous to SEP3, a RFC3986 non-compliant syntax was used for the evaluation scheme (e.g., allowing names such as - ``tango://db=foo;dev=bar;a*b?k=2;a={tango:a/b/c/d}``). + ``eval://db=foo;dev=bar;a*b?k=2;a={tango:a/b/c/d}``). This syntax is now deprecated and should not be used. Taurus will issue warnings if detected. """ diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 84f7cd36f..31baabfff 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -48,7 +48,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.1.0' +version = '4.3.0' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: diff --git a/lib/taurus/core/tango/enums.py b/lib/taurus/core/tango/enums.py index 1d2755319..4f3e43376 100644 --- a/lib/taurus/core/tango/enums.py +++ b/lib/taurus/core/tango/enums.py @@ -70,6 +70,7 @@ # 'API_BadConfigurationProperty') FROM_TANGO_TO_NUMPY_TYPE = { + PyTango.DevVoid: None, PyTango.DevBoolean: numpy.bool8, PyTango.DevUChar: numpy.uint8, PyTango.DevShort: numpy.short, @@ -84,6 +85,7 @@ } FROM_TANGO_TO_STR_TYPE = { + PyTango.DevVoid: None, PyTango.DevBoolean: 'bool8', PyTango.DevUChar: 'uint8', PyTango.DevShort: 'short', diff --git a/lib/taurus/core/tango/search.py b/lib/taurus/core/tango/search.py index 68522b834..8a677e7f3 100644 --- a/lib/taurus/core/tango/search.py +++ b/lib/taurus/core/tango/search.py @@ -1,146 +1,6 @@ -#!/usr/bin/env python +from taurus.core.util.log import deprecated as __deprecated -############################################################################# -## -# This file is part of Taurus -## -# http://taurus-scada.org -## -# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain -## -# Taurus is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -## -# Taurus is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -## -# You should have received a copy of the GNU Lesser General Public License -# along with Taurus. If not, see . -## -############################################################################# +__deprecated(dep='taurus.core.tango.search', + alt='taurus.core.util.fandango_search', rel='4.1.2') -""" -search.py: methods for getting matching device/attribute/alias names from Tango database - -These methods have been borrowed from fandango modules. -""" - -import re -import taurus - -############################################################################### -# Utils - - -def searchCl(regexp, target): - return re.search(extend_regexp(regexp).lower(), target.lower()) - - -def matchCl(regexp, target): - return re.match(extend_regexp(regexp).lower(), target.lower()) - - -def is_regexp(s): - return any(c in s for c in '.*[]()+?') - - -def extend_regexp(s): - s = str(s).strip() - if '.*' not in s: - s = s.replace('*', '.*') - if '.*' not in s: - if ' ' in s: - s = s.replace(' ', '.*') - if '/' not in s: - s = '.*' + s + '.*' - else: - if not s.startswith('^'): - s = '^' + s - if not s.endswith('$'): - s = s + '$' - return s - - -def isString(s): - typ = s.__class__.__name__.lower() - return not hasattr(s, '__iter__') and 'str' in typ and 'list' not in typ - - -def isCallable(obj): - return hasattr(obj, '__call__') - - -def isMap(obj): - return hasattr(obj, 'has_key') or hasattr(obj, 'items') - - -def isDictionary(obj): - return isMap(obj) - - -def isSequence(obj): - typ = obj.__class__.__name__.lower() - return (hasattr(obj, '__iter__') or 'list' in typ) and not isString(obj) and not isMap(obj) - - -def split_model_list(modelNames): - '''convert str to list if needed (commas and whitespace are considered as separators)''' - if isString(modelNames): # isinstance(modelNames,(basestring,Qt.QString)): - modelNames = str(modelNames).replace(',', ' ') - modelNames = modelNames.split() - if isSequence(modelNames): # isinstance(modelNames,(list.Qt.QStringList)): - modelNames = [str(s) for s in modelNames] - return modelNames - - -def get_matching_devices(expressions, limit=0, exported=False): - """ - Searches for devices matching expressions, if exported is True only running devices are returned - """ - db = taurus.Authority() - all_devs = [s.lower() for s in db.get_device_name('*', '*')] - # This code is used to get data from multiples hosts - #if any(not fun.matchCl(rehost,expr) for expr in expressions): all_devs.extend(get_all_devices(exported)) - # for expr in expressions: - #m = fun.matchCl(rehost,expr) - # if m: - #host = m.groups()[0] - # print 'get_matching_devices(%s): getting %s devices ...'%(expr,host) - #odb = PyTango.Database(*host.split(':')) - #all_devs.extend('%s/%s'%(host,d) for d in odb.get_device_name('*','*')) - result = [e for e in expressions if e.lower() in all_devs] - expressions = [extend_regexp(e) for e in expressions if e not in result] - result.extend(filter(lambda d: any(matchCl(extend_regexp(e), d) - for e in expressions), all_devs)) - return result - - -def get_device_for_alias(alias): - db = taurus.Authority() - try: - return db.get_device_alias(alias) - except Exception, e: - if 'no device found' in str(e).lower(): - return None - return None # raise e - - -def get_alias_for_device(dev): - db = taurus.Authority() - try: - # .get_database_device().DbGetDeviceAlias(dev) - result = db.get_alias(dev) - return result - except Exception, e: - if 'no alias found' in str(e).lower(): - return None - return None # raise e - - -def get_alias_dict(exp='*'): - tango = taurus.Authority() - return dict((k, tango.get_device_alias(k)) for k in tango.get_device_alias_list(exp)) +from taurus.core.util.fandango_search import * diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 045a92836..deed0369d 100755 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -41,6 +41,7 @@ from taurus import Manager from taurus.external.pint import Quantity, UR, UndefinedUnitError +from taurus import tauruscustomsettings from taurus.core.taurusattribute import TaurusAttribute from taurus.core.taurusbasetypes import (TaurusEventType, TaurusSerializationMode, @@ -48,6 +49,10 @@ DataFormat, DataType) from taurus.core.taurusoperation import WriteAttrOperation from taurus.core.util.event import EventListener +# ------------------------------------------------------------------------- +# TODO: remove this when PyTango's bug 185 is fixed +from taurus.core.util.event import _BoundMethodWeakrefWithCall +# ------------------------------------------------------------------------- from taurus.core.util.log import (debug, taurus4_deprecation, deprecation_decorator) @@ -90,10 +95,8 @@ def __init__(self, attr=None, pytango_dev_attr=None, config=None): if self._attrRef is None: return - numerical = (PyTango.is_numerical_type(self._attrRef._tango_data_type, - inc_array=True) or - p.type == PyTango.CmdArgType.DevUChar - ) + numerical = PyTango.is_numerical_type(self._attrRef._tango_data_type, + inc_array=True) if p.has_failed: self.error = PyTango.DevFailed(*p.get_err_stack()) @@ -150,7 +153,6 @@ def __getattr__(self, name): @taurus4_deprecation(alt='.rvalue') def _get_value(self): """for backwards compat with taurus < 4""" - debug(repr(self)) try: return self.__fix_int(self.rvalue.magnitude) except AttributeError: @@ -246,13 +248,16 @@ class TangoAttribute(TaurusAttribute): _description = 'A Tango Attribute' def __init__(self, name, parent, **kwargs): - # the last attribute value self.__attr_value = None # the last attribute error self.__attr_err = None + # Lock for protecting the critical read region + # where __attr_value and __attr_err are updated + self.__read_lock = threading.RLock() + # the change event identifier self.__chg_evt_id = None @@ -289,7 +294,11 @@ def __init__(self, name, parent, **kwargs): # subscribe to configuration events (unsubscription done at cleanup) self.__cfg_evt_id = None - self._subscribeConfEvents() + if self.factory().is_tango_subscribe_enabled(): + self._subscribeConfEvents() + + def __del__(self): + self.cleanUp() def cleanUp(self): self.trace("[TangoAttribute] cleanUp") @@ -316,10 +325,6 @@ def getNewOperation(self, value): # PyTango connection #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - def isNumeric(self, inc_array=False): - tgtype = self._tango_data_type - return PyTango.is_numerical_type(tgtype, inc_array=inc_array) - def isInteger(self, inc_array=False): tgtype = self._tango_data_type return PyTango.is_int_type(tgtype, inc_array=inc_array) @@ -400,7 +405,9 @@ def encode(self, value): def decode(self, attr_value): """Decodes a value that was received from PyTango into the expected representation""" - # TODO decode of the configuration + if self._pytango_attrinfoex is None: + self.getAttributeInfoEx(cache=False) + self._decodeAttrInfoEx() value = TangoAttrValue(pytango_dev_attr=attr_value, attr=self) return value @@ -441,29 +448,43 @@ def write(self, value, with_read=True): self.error("[Tango] write failed: %s" % str(e)) raise e - def poll(self, **kwargs): + def poll(self, single=True, value=None, time=None, error=None): """ Notify listeners when the attribute has been polled""" - single = kwargs.get('single', True) - try: - if single: - self.read(cache=False) + with self.__read_lock: + try: + if single: + self.read(cache=False) + else: + value = self.decode(value) + self.__attr_err = error + if self.__attr_err: + raise self.__attr_err + # Avoid "valid-but-outdated" notifications + # if FILTER_OLD_TANGO_EVENTS is enabled + # and the given timestamp is older than the timestamp + # of the cached value + filter_old_event = getattr(tauruscustomsettings, + 'FILTER_OLD_TANGO_EVENTS', + False) + if (self.__attr_value is not None + and filter_old_event + and time is not None + and time < self.__attr_value.time.totime() + ): + return + self.__attr_value = value + except PyTango.DevFailed, df: + self.__subscription_event.set() + self.debug("Error polling: %s" % df[0].desc) + self.traceback() + self.fireEvent(TaurusEventType.Error, self.__attr_err) + except Exception, e: + self.__subscription_event.set() + self.debug("Error polling: %s" % str(e)) + self.fireEvent(TaurusEventType.Error, self.__attr_err) else: - self.__attr_value = self.decode(kwargs.get('value')) - self.__attr_err = kwargs.get('error') - if self.__attr_err: - raise self.__attr_err - except PyTango.DevFailed, df: - self.__subscription_event.set() - self.debug("Error polling: %s" % df[0].desc) - self.traceback() - self.fireEvent(TaurusEventType.Error, self.__attr_err) - except Exception, e: - self.__subscription_event.set() - self.debug("Error polling: %s" % str(e)) - self.fireEvent(TaurusEventType.Error, self.__attr_err) - else: - self.__subscription_event.set() - self.fireEvent(TaurusEventType.Periodic, self.__attr_value) + self.__subscription_event.set() + self.fireEvent(TaurusEventType.Periodic, self.__attr_value) def read(self, cache=True): """ Returns the current value of the attribute. @@ -471,7 +492,6 @@ def read(self, cache=True): active then it will return the local cached value. Otherwise it will read the attribute value from the tango device.""" curr_time = time.time() - if cache: try: attr_timestamp = self.__attr_value.time.totime() @@ -486,34 +506,43 @@ def read(self, cache=True): self.fullname, self.__attr_err) raise self.__attr_err - if not cache or (self.__subscription_state in (SubscriptionState.PendingSubscribe, SubscriptionState.Unsubscribed) and not self.isPollingActive()): - try: - dev = self.getParentObj() - v = dev.read_attribute(self.getSimpleName()) - self.__attr_value, self.__attr_err = self.decode(v), None - return self.__attr_value - except PyTango.DevFailed, df: - self.__attr_value, self.__attr_err = None, df - err = df[0] - self.debug("[Tango] read failed (%s): %s", - err.reason, err.desc) - raise df - except Exception, e: - self.__attr_value, self.__attr_err = None, e - self.debug("[Tango] read failed: %s", e) - raise e - elif self.__subscription_state in (SubscriptionState.Subscribing, SubscriptionState.PendingSubscribe): + if not cache or (self.__subscription_state in + (SubscriptionState.PendingSubscribe, + SubscriptionState.Unsubscribed) + and not self.isPollingActive()): + with self.__read_lock: + try: + dev = self.getParentObj() + v = dev.read_attribute(self.getSimpleName()) + self.__attr_value = self.decode(v) + self.__attr_err = None + return self.__attr_value + except PyTango.DevFailed as df: + self.__attr_value = None + self.__attr_err = df + err = df[0] + self.debug("[Tango] read failed (%s): %s", + err.reason, err.desc) + raise df + except Exception as e: + self.__attr_value = None + self.__attr_err = e + self.debug("[Tango] read failed: %s", e) + raise e + elif self.__subscription_state in (SubscriptionState.Subscribing, + SubscriptionState.PendingSubscribe): self.__subscription_event.wait() if self.__attr_err is not None: raise self.__attr_err return self.__attr_value - + + def getAttributeProxy(self): """Convenience method that creates and returns a PyTango.AttributeProxy object""" return PyTango.AttributeProxy(self.getFullName()) - + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # API for listeners #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- @@ -588,6 +617,9 @@ def setConfigEx(self, config): def isUsingEvents(self): return self.__subscription_state == SubscriptionState.Subscribed + + def getSubscriptionState(self): + return self.__subscription_state def _process_event_exception(self, ex): pass @@ -595,6 +627,11 @@ def _process_event_exception(self, ex): def _subscribeEvents(self): """ Enable subscription to the attribute events. If change events are not supported polling is activated """ + + if self.__chg_evt_id is not None: + self.warning("chg events already subscribed (id=%s)" + %self.__chg_evt_id) + return if self.__dev_hw_obj is None: dev = self.getParentObj() @@ -606,26 +643,43 @@ def _subscribeEvents(self): self.debug("failed to subscribe to chg events: HW is None") return - self.__subscription_event = threading.Event() - attr_name = self.getSimpleName() + if not self.factory().is_tango_subscribe_enabled(): + self.enablePolling(True) + return try: self.__subscription_state = SubscriptionState.Subscribing - self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( - attr_name, PyTango.EventType.CHANGE_EVENT, - self, []) # connects to self.push_event callback - + self._call_dev_hw_subscribe_event(False) except: self.__subscription_state = SubscriptionState.PendingSubscribe self._activatePolling() - self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( - attr_name, PyTango.EventType.CHANGE_EVENT, - self, [], True) # connects to self.push_event callback + self._call_dev_hw_subscribe_event(True) + + def _call_dev_hw_subscribe_event(self, stateless=True): + """ Executes event subscription on parent TangoDevice objectName + """ + + if self.__chg_evt_id is not None: + self.warning("chg events already subscribed (id=%s)", + self.__chg_evt_id) + return + + attr_name = self.getSimpleName() + # connects to self.push_event callback + # TODO: _BoundMethodWeakrefWithCall is used as workaround for + # PyTango #185 issue + self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( + attr_name, PyTango.EventType.CHANGE_EVENT, + _BoundMethodWeakrefWithCall(self.push_event), [], stateless) + + return self.__chg_evt_id + def _unsubscribeEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object + if self.__dev_hw_obj is not None and self.__chg_evt_id is not None: self.trace("Unsubscribing to change events (ID=%d)", self.__chg_evt_id) @@ -646,6 +700,12 @@ def _unsubscribeEvents(self): def _subscribeConfEvents(self): """ Enable subscription to the attribute configuration events.""" self.trace("Subscribing to configuration events...") + + if self.__cfg_evt_id is not None: + self.warning("cfg events already subscribed (id=%s)" + %self.__cfg_evt_id) + return + if self.__dev_hw_obj is None: dev = self.getParentObj() if dev is None: @@ -658,11 +718,14 @@ def _subscribeConfEvents(self): attr_name = self.getSimpleName() try: + # connects to self.push_event callback + # TODO: _BoundMethodWeakrefWithCall is used as workaround for + # PyTango #185 issue self.__cfg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.ATTR_CONF_EVENT, - self, [], True) # connects to self.push_event callback - except PyTango.DevFailed, e: + _BoundMethodWeakrefWithCall(self.push_event), [], True) + except PyTango.DevFailed as e: self.debug("Error trying to subscribe to CONFIGURATION events.") self.traceback() # Subscription failed either because event mechanism is not available @@ -676,12 +739,13 @@ def _subscribeConfEvents(self): except: self.debug("Error getting attribute configuration") self.traceback() - + def _unsubscribeConfEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object - if self.__cfg_evt_id and not self.__dev_hw_obj is None: + + if self.__cfg_evt_id is not None and self.__dev_hw_obj is not None: self.trace("Unsubscribing to configuration events (ID=%s)", str(self.__cfg_evt_id)) try: @@ -690,30 +754,40 @@ def _unsubscribeConfEvents(self): except PyTango.DevFailed, e: self.debug("Error trying to unsubscribe configuration events") self.trace(str(e)) + + def subscribePendingEvents(self): + """ Execute delayed event subscription + """ + if (self.__subscription_state == SubscriptionState.Unsubscribed + or self.isPollingActive()): + self.__subscription_state = SubscriptionState.PendingSubscribe + self._subscribeConfEvents() + self._call_dev_hw_subscribe_event(True) def push_event(self, event): """Method invoked by the PyTango layer when an event occurs. It propagates the event to listeners and delegates other tasks to specific handlers for different event types. """ - # if it is a configuration event - if isinstance(event, PyTango.AttrConfEventData): - etype, evalue = self._pushConfEvent(event) - # if it is an attribute event - else: - etype, evalue = self._pushAttrEvent(event) + with self.__read_lock: + # if it is a configuration event + if isinstance(event, PyTango.AttrConfEventData): + etype, evalue = self._pushConfEvent(event) + # if it is an attribute event + else: + etype, evalue = self._pushAttrEvent(event) - # notify the listeners if required (i.e, if etype is not None) - if etype is None: - return - manager = Manager() - sm = self.getSerializationMode() - listeners = tuple(self._listeners) - if sm == TaurusSerializationMode.Concurrent: - manager.addJob(self.fireEvent, None, etype, evalue, - listeners=listeners) - else: - self.fireEvent(etype, evalue, listeners=listeners) + # notify the listeners if required (i.e, if etype is not None) + if etype is None: + return + manager = Manager() + sm = self.getSerializationMode() + listeners = tuple(self._listeners) + if sm == TaurusSerializationMode.Concurrent: + manager.addJob(self.fireEvent, None, etype, evalue, + listeners=listeners) + else: + self.fireEvent(etype, evalue, listeners=listeners) def _pushAttrEvent(self, event): """Handler of (non-configuration) events from the PyTango layer. @@ -727,8 +801,22 @@ def _pushAttrEvent(self, event): evt_value is a TaurusValue, an Exception, or None. """ if not event.err: - self.__attr_value, self.__attr_err = self.decode( - event.attr_value), None + attr_value = self.decode(event.attr_value) + filter_old_event = getattr(tauruscustomsettings, + 'FILTER_OLD_TANGO_EVENTS', False) + time = event.attr_value.time.totime() + + # Discard "valid" events if the attribute value is not None + # and FILTER_OLD_TANGO_EVENTS is enabled + # and the given timestamp is older than the timestamp + # of the cache value + if (self.__attr_value is not None + and filter_old_event + and time < self.__attr_value.time.totime()): + return [None, None] + + self.__attr_value = attr_value + self.__attr_err = None self.__subscription_state = SubscriptionState.Subscribed self.__subscription_event.set() if not self.isPollingForced(): @@ -741,7 +829,7 @@ def _pushAttrEvent(self, event): event.errors[0].reason) self.__subscription_state = SubscriptionState.PendingSubscribe self._activatePolling() - return None, None + return [None, None] else: self.__attr_value, self.__attr_err = None, PyTango.DevFailed( @@ -815,7 +903,7 @@ def setLimits(self, low, high): def setLabel(self, lbl): TaurusAttribute.setLabel(self, lbl) - infoex = self._pytango_attrinfoex + infoex = self._pytango_attrinfoex or PyTango.AttributeInfoEx() infoex.label = lbl self._applyConfig() @@ -830,7 +918,7 @@ def setRange(self, *limits): if high.unitless: high = Quantity(high.magnitude, self._units) TaurusAttribute.setRange(self, [low, high]) - infoex = self._pytango_attrinfoex + infoex = self._pytango_attrinfoex or PyTango.AttributeInfoEx() if low.magnitude != float('-inf'): infoex.min_value = str(low.to(self._units).magnitude) else: @@ -852,7 +940,7 @@ def setWarnings(self, *limits): if high.unitless: high = Quantity(high.magnitude, self._units) TaurusAttribute.setWarnings(self, [low, high]) - infoex = self._pytango_attrinfoex + infoex = self._pytango_attrinfoex or PyTango.AttributeInfoEx() if low.magnitude != float('-inf'): infoex.alarms.min_warning = str(low.to(self._units).magnitude) else: @@ -874,7 +962,7 @@ def setAlarms(self, *limits): if high.unitless: high = Quantity(high.magnitude, self._units) TaurusAttribute.setAlarms(self, [low, high]) - infoex = self._pytango_attrinfoex + infoex = self._pytango_attrinfoex or PyTango.AttributeInfoEx() if low.magnitude != float('-inf'): infoex.alarms.min_alarm = str(low.to(self._units).magnitude) else: @@ -886,80 +974,91 @@ def setAlarms(self, *limits): self._applyConfig() def _applyConfig(self): - config = self._pytango_attrinfoex + config = self._pytango_attrinfoex or PyTango.AttributeInfoEx() self.setConfigEx(config) def _decodeAttrInfoEx(self, pytango_attrinfoex=None): if pytango_attrinfoex is None: - self._pytango_attrinfoex = PyTango.AttributeInfoEx() - else: - self._pytango_attrinfoex = i = pytango_attrinfoex - - self.writable = i.writable != PyTango.AttrWriteType.READ - self._label = i.label - self.data_format = data_format_from_tango(i.data_format) - desc = description_from_tango(i.description) - if desc != "": - self._description = desc - self.type = data_type_from_tango(i.data_type) - ############################################################### - # changed in taurus4: range, alarm and warning now return - # quantities if appropriate + return + + self._pytango_attrinfoex = i = pytango_attrinfoex + + self.writable = i.writable != PyTango.AttrWriteType.READ + self._label = i.label + self.data_format = data_format_from_tango(i.data_format) + desc = description_from_tango(i.description) + if desc != "": + self._description = desc + self.type = data_type_from_tango(i.data_type) + ############################################################### + # changed in taurus4: range, alarm and warning now return + # quantities if appropriate + if self.isNumeric(): units = self._unit_from_tango(i.unit) - if PyTango.is_numerical_type(i.data_type, inc_array=True): - Q_ = partial(quantity_from_tango_str, units=units, - dtype=i.data_type) - ninf, inf = float('-inf'), float('inf') - min_value = Q_(i.min_value) or Quantity(ninf, units) - max_value = Q_(i.max_value) or Quantity(inf, units) - min_alarm = Q_(i.alarms.min_alarm) or Quantity(ninf, units) - max_alarm = Q_(i.alarms.max_alarm) or Quantity(inf, units) - min_warning = Q_(i.alarms.min_warning) or Quantity(ninf, units) - max_warning = Q_(i.alarms.max_warning) or Quantity(inf, units) - self._range = [min_value, max_value] - self._warning = [min_warning, max_warning] - self._alarm = [min_alarm, max_alarm] - - ############################################################### - # The following members will be accessed via __getattr__ - # self.standard_unit - # self.display_unit - # self.disp_level - ############################################################### - # Tango-specific extension of TaurusConfigValue - self.display_level = display_level_from_tango(i.disp_level) - self.tango_writable = i.writable - self.max_dim = i.max_dim_x, i.max_dim_y - ############################################################### - fmt = standard_display_format_from_tango(i.data_type, i.format) - self.format_spec = fmt.lstrip('%') # format specifier - match = re.search("[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) - if match: - self.precision = int(match.group(1)) - # self._units and self._display_format is to be used by - # TangoAttrValue for performance reasons. Do not rely on it in other - # code - self._units = units + else: + units = UR.parse_units(None) + + if PyTango.is_numerical_type(i.data_type, inc_array=True): + Q_ = partial(quantity_from_tango_str, units=units, + dtype=i.data_type) + ninf, inf = float('-inf'), float('inf') + min_value = Q_(i.min_value) or Quantity(ninf, units) + max_value = Q_(i.max_value) or Quantity(inf, units) + min_alarm = Q_(i.alarms.min_alarm) or Quantity(ninf, units) + max_alarm = Q_(i.alarms.max_alarm) or Quantity(inf, units) + min_warning = Q_(i.alarms.min_warning) or Quantity(ninf, units) + max_warning = Q_(i.alarms.max_warning) or Quantity(inf, units) + self._range = [min_value, max_value] + self._warning = [min_warning, max_warning] + self._alarm = [min_alarm, max_alarm] + + ############################################################### + # The following members will be accessed via __getattr__ + # self.standard_unit + # self.display_unit + # self.disp_level + ############################################################### + # Tango-specific extension of TaurusConfigValue + self.display_level = display_level_from_tango(i.disp_level) + self.tango_writable = i.writable + self.max_dim = i.max_dim_x, i.max_dim_y + ############################################################### + fmt = standard_display_format_from_tango(i.data_type, i.format) + self.format_spec = fmt.lstrip('%') # format specifier + match = re.search("[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) + if match: + self.precision = int(match.group(1)) + # self._units and self._display_format is to be used by + # TangoAttrValue for performance reasons. Do not rely on it in other + # code + self._units = units @property @deprecation_decorator(alt='format_spec or precision', rel='4.0.4') def format(self): - i = self._pytango_attrinfoex + i = self._pytango_attrinfoex or PyTango.AttributeInfoEx() return standard_display_format_from_tango(i.data_type, i.format) @property def _tango_data_type(self): '''returns the *tango* (not Taurus) data type''' - return self._pytango_attrinfoex.data_type + i = self._pytango_attrinfoex or PyTango.AttributeInfoEx() + return i.data_type def _unit_from_tango(self, unit): - if unit == PyTango.constants.UnitNotSpec: + # silently treat unit-not-defined as unitless + # TODO: consider logging that unit-not-defined is treated as unitless + # TODO: See https://github.com/taurus-org/taurus/issues/584 and + # https://github.com/taurus-org/taurus/pull/662 + # The extra comparison to "No unit" is necessary where + # server/database runs Tango 7 or 8 and client runs higher versions. + if unit == PyTango.constants.UnitNotSpec or unit == "No unit": unit = None try: return UR.parse_units(unit) except Exception as e: # TODO: Maybe we could dynamically register the unit in the UR - msg = 'Unknown unit "%s (will be treated as unitless)"' + msg = 'Unknown unit "%s" (will be treated as unitless)' if self.__already_warned_unit == unit: self.debug(msg, unit) else: @@ -1078,7 +1177,16 @@ def getConfig(self): """Returns the current configuration of the attribute.""" return weakref.proxy(self) - def getAttributeInfoEx(self): + def getAttributeInfoEx(self, cache=True): + if not cache: + try: + attr_name = self.getSimpleName() + attrinfoex = self.__dev_hw_obj.attribute_query(attr_name) + self._decodeAttrInfoEx(attrinfoex) + except Exception as e: + self.debug("Error getting attribute configuration: %s", e) + self.traceback() + return self._pytango_attrinfoex @taurus4_deprecation(alt='.rvalue.units') diff --git a/lib/taurus/core/tango/tangofactory.py b/lib/taurus/core/tango/tangofactory.py index b90725daa..e3fa9c994 100644 --- a/lib/taurus/core/tango/tangofactory.py +++ b/lib/taurus/core/tango/tangofactory.py @@ -109,6 +109,7 @@ def init(self, *args, **kwargs): def reInit(self): """Reinitialize the singleton""" self._default_tango_host = None + self._tango_subscribe_enabled = True self.dft_db = None self.tango_db = CaselessWeakValueDict() self.tango_db_queries = CaselessWeakValueDict() @@ -182,6 +183,26 @@ def get_default_tango_host(self): """Retruns the current default tango host """ return self._default_tango_host + + def set_tango_subscribe_enabled(self,value): + """ If True, enables event subscribing on TangoAttribute objects + + .. warning:: This method belongs to a "Delayed Event Subscription" API + added in v.4.2.1-alpha as an *experimental* feature. This API + may not be stable and/or it may be removed in a future release + (even on a minor version change) + """ + self._tango_subscribe_enabled = value + + def is_tango_subscribe_enabled(self): + """ Returns the current tango_subscribe_enabled status + + .. warning:: This method belongs to a "Delayed Event Subscription" API + added in v.4.2.1-alpha as an *experimental* feature. This API + may not be stable and/or it may be removed in a future release + (even on a minor version change) + """ + return self._tango_subscribe_enabled def registerAttributeClass(self, attr_name, attr_klass): """Registers a new attribute class for the attribute name. diff --git a/lib/taurus/core/tango/tangovalidator.py b/lib/taurus/core/tango/tangovalidator.py index d3bcfa6f9..81d2eaece 100644 --- a/lib/taurus/core/tango/tangovalidator.py +++ b/lib/taurus/core/tango/tangovalidator.py @@ -30,7 +30,7 @@ __docformat__ = "restructuredtext" - +import socket from taurus.core.taurusvalidator import (TaurusAttributeNameValidator, TaurusDeviceNameValidator, TaurusAuthorityNameValidator) @@ -57,6 +57,17 @@ class TangoAuthorityNameValidator(TaurusAuthorityNameValidator): query = '(?!)' fragment = '(?!)' + def getUriGroups(self, name, strict=None): + '''Reimplementation of getUriGroups to fix the host and authority + name using fully qualified domain name for the host. + ''' + ret = TaurusAuthorityNameValidator.getUriGroups(self, name, strict) + if ret is not None: + fqdn = socket.getfqdn(ret["host"]) + ret["host"] = fqdn + ret["authority"] = "//{host}:{port}".format(**ret) + return ret + class TangoDeviceNameValidator(TaurusDeviceNameValidator): '''Validator for Tango device names. Apart from the standard named @@ -80,6 +91,17 @@ class TangoDeviceNameValidator(TaurusDeviceNameValidator): query = '(?!)' fragment = '(?!)' + def getUriGroups(self, name, strict=None): + '''Reimplementation of getUriGroups to fix the host and authority + name using fully qualified domain name for the host. + ''' + ret = TaurusDeviceNameValidator.getUriGroups(self, name, strict) + if ret is not None and ret.get("host", None) is not None: + fqdn = socket.getfqdn(ret["host"]) + ret["host"] = fqdn + ret["authority"] = "//{host}:{port}".format(**ret) + return ret + def getNames(self, fullname, factory=None, queryAuth=True): '''reimplemented from :class:`TaurusDeviceNameValidator`. It accepts an extra keyword arg `queryAuth` which, if set to False, will prevent the @@ -98,7 +120,10 @@ def getNames(self, fullname, factory=None, queryAuth=True): if default_authority is None: import PyTango - default_authority = "//" + PyTango.ApiUtil.get_env_var('TANGO_HOST') + host, port = PyTango.ApiUtil.get_env_var('TANGO_HOST').split(":") + # Get the fully qualified domain name + host = socket.getfqdn(host) + default_authority = "//{0}:{1}".format(host, port) authority = groups.get('authority') if authority is None: @@ -151,7 +176,20 @@ def nonStrictNamePattern(self): '''In non-strict mode, allow double-slash even if there is no Authority. (e.g., "tango://a/b/c" passes this non-strict form) ''' - return self.namePattern.replace('tango):(', 'tango)://(') + pattern = r'^((?P%(scheme)s)://)?' + \ + r'((?P%(authority)s)(?=/))?' + \ + r'(?P%(path)s)' + \ + r'(\?(?P%(query)s))?' + \ + r'(#%(fragment)s)?$' + authority = '(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' + path = '/?(?P((?P<_devalias>([^/?#:]+))|' + \ + '(?P<_devslashname>[^/?#:]+/[^/?#:]+/[^/?#:]+)))' + + return pattern % dict(scheme=self.scheme, + authority=authority, + path=path, + query='(?!)', + fragment='(?!)') class TangoAttributeNameValidator(TaurusAttributeNameValidator): @@ -178,6 +216,17 @@ class TangoAttributeNameValidator(TaurusAttributeNameValidator): query = '(?!)' fragment = '(?P[^# ]*)' + def getUriGroups(self, name, strict=None): + '''Reimplementation of getUriGroups to fix the host and authority + name using fully qualified domain name for the host. + ''' + ret = TaurusAttributeNameValidator.getUriGroups(self, name, strict) + if ret is not None and ret.get("host", None) is not None: + fqdn = socket.getfqdn(ret["host"]) + ret["host"] = fqdn + ret["authority"] = "//{host}:{port}".format(**ret) + return ret + def getNames(self, fullname, factory=None, queryAuth=True, fragment=False): """Returns the complete and short names""" @@ -213,15 +262,16 @@ def nonStrictNamePattern(self): non-strict form, and the named group "fragment" will contain "label" """ - # allow for *optional* double-slashes and *optional* ?configuration... - pattern = r'^(?P%(scheme)s):(//)?' + \ + pattern = r'^((?P%(scheme)s)://)?' + \ r'((?P%(authority)s)(?=/))?' + \ r'(?P%(path)s)' + \ r'(\?(?P%(query)s))?' + \ r'(#%(fragment)s)?$' + authority = '(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' + query = 'configuration(=(?P(?P[^# ]+)))?' return pattern % dict(scheme=self.scheme, - authority='(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})', + authority=authority, path=self.path, - query='configuration(=(?P(?P[^# ]+)))?', + query=query, fragment='(?!)') diff --git a/lib/taurus/core/tango/test/res/TangoSchemeTest b/lib/taurus/core/tango/test/res/TangoSchemeTest index 9bdfdf7e6..8deda674a 100755 --- a/lib/taurus/core/tango/test/res/TangoSchemeTest +++ b/lib/taurus/core/tango/test/res/TangoSchemeTest @@ -23,11 +23,13 @@ ## ############################################################################# import numpy -from time import time +from time import (time, sleep) from PyTango import DeviceProxy, AttrWriteType, DevState, AttrQuality from PyTango.server import Device, DeviceMeta, attribute, run, command +from threading import Thread + class TangoSchemeTest(Device): """ @@ -79,7 +81,6 @@ class TangoSchemeTest(Device): 'string': 'hello world', 'uchar': 1, } - default_unit = {'int': "mm", 'float': "mm"} # x10 default_rvalue @@ -100,13 +101,15 @@ class TangoSchemeTest(Device): 'float': [-default_rvalue["float"] * 3, default_rvalue["float"] * 3] } - attrs = {'bool_scalar': dict(dtype=bool), 'short_scalar': dict(unit=default_unit["int"], dtype=numpy.int16), 'short_scalar_nu': dict(dtype=numpy.int16), 'float_scalar': dict(unit=default_unit["float"], dtype=numpy.float32), + 'float_scalar_poll': dict(unit=default_unit["float"], + dtype=numpy.float32, + polling_period=500), 'double_scalar': dict(unit=default_unit["float"], dtype=numpy.float64), 'string_scalar': dict(dtype=str), @@ -142,8 +145,8 @@ class TangoSchemeTest(Device): 'uchar_image': dict(unit=default_unit["int"], dtype=[(numpy.uint8,)], max_dim_x=MAXDIMX, max_dim_y=MAXDIMY), + 'MIXEDcase': dict(dtype=str) } - extra_cfg = {'short_scalar': dict(min_value=default_ranges['int'][0], max_value=default_ranges['int'][1], min_alarm=default_alarms['int'][0], @@ -181,7 +184,8 @@ class TangoSchemeTest(Device): **attrs['string_scalar']) uchar_scalar_ro = attribute(access=AttrWriteType.READ, **attrs['uchar_scalar']) - # SPECTRUMS + + # SPECTRA boolean_spectrum_ro = attribute(access=AttrWriteType.READ, **attrs['bool_spectrum']) short_spectrum_ro = attribute(access=AttrWriteType.READ, @@ -190,6 +194,7 @@ class TangoSchemeTest(Device): **attrs['float_spectrum']) string_spectrum_ro = attribute(access=AttrWriteType.READ, **attrs['string_spectrum']) + # IMAGES uchar_image_ro = attribute(access=AttrWriteType.READ, **attrs['uchar_image']) @@ -201,6 +206,8 @@ class TangoSchemeTest(Device): **attrs['float_image']) string_image_ro = attribute(access=AttrWriteType.READ, **attrs['string_image']) + MIXEDcase = attribute(access=AttrWriteType.READ, + **attrs['MIXEDcase']) # READ/WRITE ATTRIBUTES # SCALARS @@ -224,7 +231,8 @@ class TangoSchemeTest(Device): **attrs['string_scalar']) uchar_scalar = attribute(access=AttrWriteType.READ_WRITE, **attrs['uchar_scalar']) - # SPECTRUM + + # SPECTRA boolean_spectrum = attribute(access=AttrWriteType.READ_WRITE, **attrs['bool_spectrum']) short_spectrum = attribute(access=AttrWriteType.READ_WRITE, @@ -237,6 +245,7 @@ class TangoSchemeTest(Device): **attrs['string_spectrum']) uchar_spectrum = attribute(access=AttrWriteType.READ_WRITE, **attrs['uchar_spectrum']) + # IMAGES boolean_image = attribute(access=AttrWriteType.READ_WRITE, **attrs['bool_image']) @@ -251,8 +260,34 @@ class TangoSchemeTest(Device): uchar_image = attribute(access=AttrWriteType.READ_WRITE, **attrs['uchar_image']) + # EVENTS + float_scalar_poll = attribute(access=AttrWriteType.READ, + **attrs['float_scalar_poll']) + float_scalar_evt = attribute(access=AttrWriteType.READ_WRITE, + **attrs['float_scalar']) + + _float_scalar_evt = default_rvalue['float'] + _float_scalar_poll = default_rvalue['float'] + # READ ONLY METHODS # SCALARS + def read_float_scalar_poll(self): + self._float_scalar_poll *= -1 + + def read_float_scalar_evt(self): + return self._float_scalar_evt + + def write_float_scalar_evt(self, value): + self._float_scalar_evt = value + self.push_change_event('float_scalar_evt', self._float_scalar_evt) + + @command(dtype_in=numpy.float32) + def PushEvent(self, data): + # push_change_event (self, attr_name, data, time_stamp, quality, + # dim_x = 1, dim_y = 0) + self.push_change_event('float_scalar_evt', data, time.time(), + AttrQuality.ATTR_VALID, 1, 0) + def read_boolean_scalar_ro(self): return self.default_rvalue['bool'] @@ -274,7 +309,7 @@ class TangoSchemeTest(Device): def read_uchar_scalar_ro(self): return self.default_rvalue['uchar'] - # SPECTRUMS + # SPECTRA def read_boolean_spectrum_ro(self): return [self.default_rvalue['bool']] * self.DIMX @@ -309,6 +344,9 @@ class TangoSchemeTest(Device): def read_uchar_image_ro(self): return [[self.default_rvalue['uchar']] * self.DIMX] * self.DIMY + def read_MIXEDcase(self): + return "MIXEDcase" + # READ/WRITE METHODS # SCALARS def read_boolean_scalar(self): @@ -353,7 +391,7 @@ class TangoSchemeTest(Device): def write_uchar_scalar(self, v): self._uchar_scalar = v - # SPECTRUMS + # SPECTRA def read_boolean_spectrum(self): return getattr(self, '_boolean_spectrum', [self.default_rvalue['bool']] * self.DIMX) @@ -441,11 +479,72 @@ class TangoSchemeTest(Device): def write_uchar_image(self, v): self._uchar_image = v + def _update_loop(self): + while True: + sleep(0.1) + try: + if not self.use_fixed_time: + self.sleeptime *= 2 + if self.sleeptime > 5: + self.sleeptime = 0.1 + + self._float_scalar_evt *= -1 + + self.push_change_event('float_scalar_evt', + self._float_scalar_evt, time(), + AttrQuality.ATTR_VALID) + sleep(self.sleeptime) + except Exception as e: + print 'Exception in update loop', e + sleep(1) + def init_device(self): # To setUp the state Device.init_device(self) self.set_state(DevState.ON) self._quality = AttrQuality.ATTR_VALID + self.set_change_event('float_scalar_evt', True, False) + # event thread + self.use_fixed_time = False + self.sleeptime = None + self.t = Thread(target=self._update_loop) + self.t.setDaemon(True) + self.t.start() + + @command(dtype_in=int) + def ChangePollingPeriod(self, period): + """ Change the polling period of float_scalar_poll attribute""" + self.poll_attribute('float_scalar_poll', period) + + @command(dtype_in=float) + def SetEventPeriod(self, period): + """ Set a fixed event period for float_scalar_evt attribute. + If period is 0, the event period will vary from 0.1 to 5 + """ + if period == 0: + self.use_fixed_time = False + else: + self.use_fixed_time = True + self.sleeptime = period + + @command(dtype_in=str) + def ChangeState(self, state): + tango_state = {"alarm": DevState.ALARM, + "fault": DevState.FAULT, + "off": DevState.OFF, + "standby": DevState.STANDBY, + "close": DevState.CLOSE, + "init": DevState.INIT, + "on": DevState.ON, + "unknown": DevState.UNKNOWN, + "disable": DevState.DISABLE, + "insert": DevState.INSERT, + "open": DevState.OPEN, + "extract": DevState.EXTRACT, + "moving": DevState.MOVING, + "running": DevState.RUNNING + } + self.set_state(tango_state.get(state.lower(), DevState.UNKNOWN)) @command(dtype_in=str) def ChangeShortScalarROQuality(self, quality): diff --git a/lib/taurus/core/tango/test/test_tangoattribute.py b/lib/taurus/core/tango/test/test_tangoattribute.py index d5a1350fc..6580b355b 100644 --- a/lib/taurus/core/tango/test/test_tangoattribute.py +++ b/lib/taurus/core/tango/test/test_tangoattribute.py @@ -155,90 +155,90 @@ # Test encode-decode of strings, booleans and uchars @insertTest(helper_name='write_read_attr', attrname='uchar_image', - setvalue=Quantity(_UINT8_IMG, 'mm'), - expected=dict(rvalue=Quantity(_UINT8_IMG, 'mm'), - wvalue=Quantity(_UINT8_IMG, 'mm'), - type=DataType.Integer, + setvalue=_UINT8_IMG, + expected=dict(rvalue=_UINT8_IMG, + wvalue=_UINT8_IMG, + type=DataType.Bytes, label='uchar_image', writable=True, ), - expected_attrv=dict(rvalue=Quantity(_UINT8_IMG, 'mm'), + expected_attrv=dict(rvalue=_UINT8_IMG, value=_UINT8_IMG, - wvalue=Quantity(_UINT8_IMG, 'mm'), + wvalue=_UINT8_IMG, w_value=_UINT8_IMG, quality=AttrQuality.ATTR_VALID ) ) @insertTest(helper_name='write_read_attr', attrname='uchar_spectrum', - setvalue=Quantity(_UINT8_SPE, 'mm'), - expected=dict(rvalue=Quantity(_UINT8_SPE, 'mm'), - wvalue=Quantity(_UINT8_SPE, 'mm'), - type=DataType.Integer, + setvalue=_UINT8_SPE, + expected=dict(rvalue=_UINT8_SPE, + wvalue=_UINT8_SPE, + type=DataType.Bytes, writable=True, ), - expected_attrv=dict(rvalue=Quantity(_UINT8_SPE, 'mm'), + expected_attrv=dict(rvalue=_UINT8_SPE, value=_UINT8_SPE, - wvalue=Quantity(_UINT8_SPE, 'mm'), + wvalue=_UINT8_SPE, w_value=_UINT8_SPE, quality=AttrQuality.ATTR_VALID ) ) @insertTest(helper_name='write_read_attr', attrname='uchar_scalar', - setvalue=Quantity(12, 'mm'), - expected=dict(rvalue=Quantity(12, 'mm'), - wvalue=Quantity(12, 'mm'), - type=DataType.Integer, + setvalue=12, + expected=dict(rvalue=12, + wvalue=12, + type=DataType.Bytes, writable=True, range=[None, None], alarms=[None, None], warnings=[None, None] ), - expected_attrv=dict(rvalue=Quantity(12, 'mm'), + expected_attrv=dict(rvalue=12, value=12, - wvalue=Quantity(12, 'mm'), + wvalue=12, w_value=12, quality=AttrQuality.ATTR_VALID ) ) @insertTest(helper_name='write_read_attr', attrname='uchar_image', - setvalue=Quantity(_UINT8_IMG, 'mm'), - expected=dict(rvalue=Quantity(_UINT8_IMG, 'mm'), - wvalue=Quantity(_UINT8_IMG, 'mm'), - type=DataType.Integer, + setvalue=_UINT8_IMG, + expected=dict(rvalue=_UINT8_IMG, + wvalue=_UINT8_IMG, + type=DataType.Bytes, label='uchar_image', writable=True, ), - expected_attrv=dict(rvalue=Quantity(_UINT8_IMG, 'mm'), + expected_attrv=dict(rvalue=_UINT8_IMG, value=_UINT8_IMG, - wvalue=Quantity(_UINT8_IMG, 'mm'), + wvalue=_UINT8_IMG, w_value=_UINT8_IMG, quality=AttrQuality.ATTR_VALID ) ) @insertTest(helper_name='write_read_attr', attrname='uchar_spectrum', - setvalue=Quantity(_UINT8_SPE, 'mm'), - expected=dict(rvalue=Quantity(_UINT8_SPE, 'mm'), - wvalue=Quantity(_UINT8_SPE, 'mm'), - type=DataType.Integer, + setvalue=_UINT8_SPE, + expected=dict(rvalue=_UINT8_SPE, + wvalue=_UINT8_SPE, + type=DataType.Bytes, writable=True, ), - expected_attrv=dict(rvalue=Quantity(_UINT8_SPE, 'mm'), + expected_attrv=dict(rvalue=_UINT8_SPE, value=_UINT8_SPE, - wvalue=Quantity(_UINT8_SPE, 'mm'), + wvalue=_UINT8_SPE, w_value=_UINT8_SPE, quality=AttrQuality.ATTR_VALID ) ) @insertTest(helper_name='write_read_attr', attrname='uchar_scalar', - setvalue=Quantity(12, 'mm'), - expected=dict(rvalue=Quantity(12, 'mm'), - wvalue=Quantity(12, 'mm'), - type=DataType.Integer, + setvalue=12, + expected=dict(rvalue=12, + wvalue=12, + type=DataType.Bytes, writable=True, range=[None, None], alarms=[None, None], @@ -531,7 +531,7 @@ attrname='uchar_image_ro', expected=dict(rvalue=Quantity([[1] * 3] * 3, 'mm'), wvalue=None, - type=DataType.Integer + type=DataType.Bytes ), expected_attrv=dict(value=[[1] * 3] * 3, w_value=None, @@ -542,9 +542,9 @@ @insertTest(helper_name='write_read_attr', attrname='uchar_scalar_ro', - expected=dict(rvalue=Quantity(1, 'mm'), + expected=dict(rvalue=1, wvalue=None, - type=DataType.Integer, + type=DataType.Bytes, data_format=DataFormat._0D, writable=False, range=[None, None], diff --git a/lib/taurus/core/tango/test/test_tangovalidator.py b/lib/taurus/core/tango/test/test_tangovalidator.py index 3115859f0..919d9c1a3 100644 --- a/lib/taurus/core/tango/test/test_tangovalidator.py +++ b/lib/taurus/core/tango/test/test_tangovalidator.py @@ -38,8 +38,11 @@ TangoAttributeNameValidator) import PyTango -__GETENV = PyTango.ApiUtil.get_env_var +import socket +__PY_TANGO_HOST = PyTango.ApiUtil.get_env_var("TANGO_HOST") +host, port = __PY_TANGO_HOST.split(':') +__TANGO_HOST = "{0}:{1}".format(socket.getfqdn(host), port) #========================================================================= # Tests for Tango Authority name validation @@ -75,16 +78,23 @@ class TangoAuthValidatorTestCase(AbstractNameValidatorTestCase, @valid(name='tango:a/b/ c', groups={'devname': 'a/b/ c'}) @invalid(name='tango:/a/b/c?') @valid(name='tango://a/b/c', strict=False) +@valid(name='tango:alias') +@valid(name='tango://alias', strict=False) +@valid(name='tango://a/b/c', strict=False) +@invalid(name='tango:foo:1234/alias', strict=False) +@invalid(name='tango:foo:1234/a/b/c', strict=False) +@valid(name='foo:1234/alias', strict=False) # Implicit scheme +@valid(name='foo:1234/a/b/c', strict=False) # Implicit scheme @invalid(name='tango://a/b/c', strict=True) @invalid(name='tango://devalias') @names(name='tango://foo:123/a/b/c', out=('tango://foo:123/a/b/c', '//foo:123/a/b/c', 'a/b/c')) @names(name='tango:sys/tg_test/1', - out=('tango://%s/sys/tg_test/1' % __GETENV("TANGO_HOST"), + out=('tango://%s/sys/tg_test/1' % __TANGO_HOST, 'sys/tg_test/1', 'sys/tg_test/1')) @names(name='tango:alias', out=(None, None, 'alias')) # @names(name = 'tango:mot49', # commented out because it assumes mot49 exists -# out=('tango://%s/motor/motctrl13/1'% __GETENV("TANGO_HOST"), +# out=('tango://%s/motor/motctrl13/1'% __TANGO_HOST, # 'motor/motctrl13/1', 'mot49')) class TangoDevValidatorTestCase(AbstractNameValidatorTestCase, unittest.TestCase): @@ -94,6 +104,8 @@ class TangoDevValidatorTestCase(AbstractNameValidatorTestCase, #========================================================================= # Tests for Tango Attribute name validation (without fragment) #========================================================================= +@valid(name='foo:10000/a/b/c/d', strict=False) +@valid(name='mot/position', strict=False) @valid(name='tango:a/b/c/d', groups={'devname': 'a/b/c', 'attrname': 'a/b/c/d', '_shortattrname': 'd'}) @@ -112,10 +124,10 @@ class TangoDevValidatorTestCase(AbstractNameValidatorTestCase, @names(name='tango://foo:123/a/b/c/d', out=('tango://foo:123/a/b/c/d', '//foo:123/a/b/c/d', 'd')) @names(name='tango:sys/tg_test/1/float_scalar', - out=('tango://%s/sys/tg_test/1/float_scalar' % __GETENV("TANGO_HOST"), + out=('tango://%s/sys/tg_test/1/float_scalar' % __TANGO_HOST, 'sys/tg_test/1/float_scalar', 'float_scalar')) # @names(name = 'tango:mot49/position', # commented out because it assumes mot49 -# out=('tango://%s/motor/motctrl13/1/position'% __GETENV("TANGO_HOST"), +# out=('tango://%s/motor/motctrl13/1/position'% __TANGO_HOST, # 'motor/motctrl13/1/position', 'position')) #========================================================================= # Tests for validation of Attribute name with fragment @@ -147,8 +159,7 @@ class TangoDevValidatorTestCase(AbstractNameValidatorTestCase, out=('tango://foo:123/a/b/c/d', '//foo:123/a/b/c/d', 'd', 'label')) @names(name='tango:sys/tg_test/1/float_scalar#', - out=('tango://%s/sys/tg_test/1/float_scalar' % - __GETENV("TANGO_HOST"), + out=('tango://%s/sys/tg_test/1/float_scalar' % __TANGO_HOST, 'sys/tg_test/1/float_scalar', 'float_scalar', '')) @names(name='tango://foo:123/a/b/c/d?configuration=label', out=('tango://foo:123/a/b/c/d', diff --git a/lib/taurus/core/tango/util/tango_taurus.py b/lib/taurus/core/tango/util/tango_taurus.py index 3b8116a1f..5dcf17913 100644 --- a/lib/taurus/core/tango/util/tango_taurus.py +++ b/lib/taurus/core/tango/util/tango_taurus.py @@ -47,7 +47,7 @@ PyTango.CmdArgType.DevUShort: DataType.Integer, PyTango.CmdArgType.DevULong: DataType.Integer, PyTango.CmdArgType.DevString: DataType.String, - PyTango.CmdArgType.DevVarCharArray: DataType.Integer, + PyTango.CmdArgType.DevVarCharArray: DataType.Bytes, PyTango.CmdArgType.DevVarShortArray: DataType.Integer, PyTango.CmdArgType.DevVarLongArray: DataType.Integer, PyTango.CmdArgType.DevVarFloatArray: DataType.Float, @@ -60,7 +60,7 @@ PyTango.CmdArgType.DevState: DataType.DevState, PyTango.CmdArgType.ConstDevString: DataType.String, PyTango.CmdArgType.DevVarBooleanArray: DataType.Boolean, - PyTango.CmdArgType.DevUChar: DataType.Integer, + PyTango.CmdArgType.DevUChar: DataType.Bytes, PyTango.CmdArgType.DevLong64: DataType.Integer, PyTango.CmdArgType.DevULong64: DataType.Integer, PyTango.CmdArgType.DevVarLong64Array: DataType.Integer, @@ -117,14 +117,14 @@ def unit_from_tango(unit): from taurus import deprecated deprecated(dep='unit_from_tango', rel='4.0.4', alt="pint's parse_units") - if unit == PyTango.constants.UnitNotSpec: + if unit == PyTango.constants.UnitNotSpec or unit == "No unit": unit = None try: return UR.parse_units(unit) except (UndefinedUnitError, UnicodeDecodeError): # TODO: Maybe we could dynamically register the unit in the UR from taurus import warning - warning('Unknown unit "%s (will be treated as unitless)"', unit) + warning('Unknown unit "%s" (will be treated as unitless)', unit) return UR.parse_units(None) diff --git a/lib/taurus/core/taurusattribute.py b/lib/taurus/core/taurusattribute.py index 9ca970276..c59aeb968 100644 --- a/lib/taurus/core/taurusattribute.py +++ b/lib/taurus/core/taurusattribute.py @@ -290,7 +290,8 @@ def getDisplayDescription(self, cache=True): def getDisplayDescrObj(self, cache=True): name = self.getLabel(cache=cache) - obj = [('name', name)] + obj = [('name', name), + ('model', self.getFullName() or '')] descr = self.description if descr: _descr = descr.replace("<", "<").replace(">", ">") diff --git a/lib/taurus/core/taurusfactory.py b/lib/taurus/core/taurusfactory.py index b140dfceb..db71cce0b 100644 --- a/lib/taurus/core/taurusfactory.py +++ b/lib/taurus/core/taurusfactory.py @@ -147,7 +147,7 @@ def getAuthority(self, name=None): raise TaurusException(msg) fullname, _, _ = v.getNames(name) - auth = self._devs.get(fullname) + auth = self._auths.get(fullname) if auth is not None: return auth @@ -271,7 +271,7 @@ def supportsScheme(self, scheme): :return: (bool) True if the scheme is supported (False otherwise) """ - return scheme in self.shemes + return scheme in self.schemes def findObject(self, absolute_name): """ Must give an absolute name""" diff --git a/lib/taurus/core/taurushelper.py b/lib/taurus/core/taurushelper.py index 9c7e1f6d2..78170fe92 100644 --- a/lib/taurus/core/taurushelper.py +++ b/lib/taurus/core/taurushelper.py @@ -90,7 +90,8 @@ def check_dependencies(): # requirements from PyPI for r in d.requires(extras=[extra]): try: - pkg_resources.require(str(r)) + r = str(r).split(';')[0] # remove marker if present (see #612) + pkg_resources.require(r) print '\t[*]', except Exception: print '\t[ ]', diff --git a/lib/taurus/core/taurusmodel.py b/lib/taurus/core/taurusmodel.py index 63a7f6e24..5546de9ac 100644 --- a/lib/taurus/core/taurusmodel.py +++ b/lib/taurus/core/taurusmodel.py @@ -34,7 +34,9 @@ import threading from .util.log import Logger -from .util.event import CallableRef, BoundMethodWeakref +from .util.event import (CallableRef, + BoundMethodWeakref, + _BoundMethodWeakrefWithCall) from .taurusbasetypes import TaurusEventType, MatchLevel from .taurushelper import Factory @@ -210,8 +212,8 @@ def _listenerDied(self, weak_listener): return try: self._listeners.remove(weak_listener) - except Exception, e: - pass + except Exception as e: + self.debug("Problem removing listener: %r", e) def _getCallableRef(self, listener, cb=None): # return weakref.ref(listener, self._listenerDied) @@ -225,7 +227,11 @@ def addListener(self, listener): if self._listeners is None or listener is None: return False - weak_listener = self._getCallableRef(listener, self._listenerDied) + # TODO: _BoundMethodWeakrefWithCall is used as workaround for + # PyTango #185 issue + weak_listener = self._getCallableRef( + listener, _BoundMethodWeakrefWithCall(self._listenerDied)) + # listener, self._listenerDied) if weak_listener in self._listeners: return False self._listeners.append(weak_listener) @@ -237,7 +243,7 @@ def removeListener(self, listener): weak_listener = self._getCallableRef(listener) try: self._listeners.remove(weak_listener) - except Exception, e: + except Exception as e: return False return True diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index 3cf50c150..c34b2976c 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -30,10 +30,11 @@ __docformat__ = "restructuredtext" import time +import weakref import threading from .util.log import Logger, DebugIt -from .util.containers import CaselessDict +from .util.containers import CaselessWeakValueDict from .util.timer import Timer @@ -97,9 +98,9 @@ def addAttribute(self, attribute, auto_start=True): attr_dict = self.dev_dict.get(dev) if attr_dict is None: if attribute.factory().caseSensitive: - self.dev_dict[dev] = attr_dict = {} + self.dev_dict[dev] = attr_dict = weakref.WeakValueDictionary() else: - self.dev_dict[dev] = attr_dict = CaselessDict() + self.dev_dict[dev] = attr_dict = CaselessWeakValueDict() if attr_name not in attr_dict: attr_dict[attr_name] = attribute self.attr_nb += 1 diff --git a/lib/taurus/core/test/modulemanager.py b/lib/taurus/core/test/modulemanager.py deleted file mode 100644 index cdcb770cd..000000000 --- a/lib/taurus/core/test/modulemanager.py +++ /dev/null @@ -1,150 +0,0 @@ -import imp -import importlib -from sys import modules -from taurus.core.util.singleton import Singleton - - -class ModuleManager(Singleton): - '''ModuleManager class is an helper to manager the python modules. - This class has methods to import, reload, and block python modules. - ''' - - def __init__(self): - self._modules = {} - - def _deleteModule(self, modname): - ''' Remove the given module name from the exported python modules - - :param modname: Module name - :type modname: str - ''' - try: - thismod = modules[modname] - except KeyError: - # This module is not imported - raise ValueError(modname) - these_symbols = dir(thismod) - - del modules[modname] - for mod in modules.values(): - try: - delattr(mod, modname) - except AttributeError: - pass - - def blockModule(self, modname): - ''' Remplace the given module name by None. inhibits the module. - - :param modname: Module name - :type modname: str - ''' - if self._modules.get(modname) is None: - self.importModule(modname) - - if modules[modname] is not None: - _mod = {} - _mod['mod'] = modules[modname] - _mod['submod'] = [] - # Delete and block submodules - for mod in modules.keys(): - if mod.find(modname) != -1: - _mod['submod'].append((mod, modules[mod])) - self._deleteModule(mod) - modules[mod] = None - # Delete and block the module - self._deleteModule(modname) - modules[modname] = None - self._modules[modname] = _mod - - def reloadModule(self, modname): - '''Reload the given module name. - - :param modname: Module name - :type modname: str - ''' - if self._modules.get(modname) is None: - msg = 'ModuleManager: Trying to reload a not imported module, %s'\ - % (modname) - print msg - return - # Reload the submodules - for subname, submod in self._modules[modname]['submod']: - modules[subname] = submod - # Reload the module - modules[modname] = self._modules[modname]['mod'] - imp.reload(self._modules[modname]['mod']) - - def importModule(self, modname): - ''' Import the given module name. - - :param modname: Module name - :type modname: str - ''' - try: - mod = __import__(modname) - modules[modname] = mod - _mod = {} - _mod['mod'] = mod - _mod['submod'] = [] - self._modules[modname] = _mod - except ImportError: - print 'Imposible to import the module %s' % (modname) - - def _getModuleDict(self, modname): - ''' Return a dictionary with the given module name and its submodels - if exists or None. - - :param modname: Module name - :type modname: str - - :return dictionary - ''' - return self._modules.get(modname) - - def getModule(self, modname): - ''' Return a module of the given module name if exists or None. - - :param modname: Module name - :type modname: str - - :return python module - ''' - d = self._getModuleDict(modname) - if d is None: - return None - return d['mod'] - -#---------------------Just 4 Test----------------------------------------- - - -def blockPyTango(modmanager): - import taurus - from taurus.core.taurusmanager import TaurusManager - tm = TaurusManager() - print '\n-*-*-*-*-*- Block PyTango' - modmanager.blockModule('PyTango') - taurus.check_dependencies() - print '\n -- Taurus plugins' - print tm.buildPlugins() - - -def reloadPyTango(modmanager): - import taurus - from taurus.core.taurusmanager import TaurusManager - tm = TaurusManager() - print '\n-*-*-*-*-*- Reload PyTango' - modmanager.reloadModule('PyTango') - taurus.check_dependencies() - print '\n -- Taurus plugins' - print tm.buildPlugins() - - -def test(): - modmanager = ModuleManager() - blockPyTango(modmanager) - reloadPyTango(modmanager) - blockPyTango(modmanager) - - -if __name__ == '__main__': - test() diff --git a/lib/taurus/core/test/test_tauruscoreindependent.py b/lib/taurus/core/test/test_tauruscoreindependent.py deleted file mode 100644 index 8e755a215..000000000 --- a/lib/taurus/core/test/test_tauruscoreindependent.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python - -############################################################################# -## -# This file is part of Taurus -## -# http://taurus-scada.org -## -# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain -## -# Taurus is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -## -# Taurus is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -## -# You should have received a copy of the GNU Lesser General Public License -# along with Taurus. If not, see . -## -############################################################################# - -"""Test for taurus.core being PyTango Independent""" - -__docformat__ = 'restructuredtext' - -from taurus.external import unittest -# import functools -# from taurus.test import insertTest -# from modulemanager import ModuleManager -# -# # Decorator -# isTangoAvailable = functools.partial(insertTest, helper_name='isTangoAvailable', -# blockPytango=False) -# -# -# *TODO -#@isTangoAvailable(blockPytango=True) -#@isTangoAvailable() # Default False -#@isTangoAvailable(blockPytango=True) - - -class CoreTangoIndependentTestCase(unittest.TestCase): - '''Test the Tango-independent core. As part of the SEP3 specification - Taurus's core must be functional without PyTango. - This test checks that you can import taurus without PyTango. - ''' - - def _importTaurus(self): - ''' Helper method''' - try: - import taurus - except ImportError: - raise ImportError("Cannot import Taurus") - - def test_basicImport(self): - '''Check if Taurus can be imported without PyTango - ''' - # skip if PyTango is available - try: - import PyTango - msg = 'Cannot test Tango-independence if PyTango can be imported' - self.skipTest(msg) - except ImportError: - pass - # check that taurus can be imported - self.assertRaises(ImportError, self._importTaurus()) - -# _modmanager = ModuleManager() -# def _basicImportWithoutPyTango(self): -# '''Basic test just try to import taurus (without PyTango) -# ''' -# # TODO: -# # _basicImportWithoutPyTango -> test_basicImportWithoutPyTango -# # We have problems blocking modules so this test failed -# self._modmanager.blockModule('PyTango') -# self.assertRaises(ImportError, self._importTaurus()) -# -# def isTangoAvailable(self, blockPytango=False): -# '''Test to check is Tango module is available for Taurus -# ''' -# # TODO: -# # We have problems blocking modules so this test failed because that -# if blockPytango: -# self._modmanager.blockModule('PyTango') -# -# from taurus.core.taurusmanager import TaurusManager -# plugins = TaurusManager().buildPlugins() -# -# if blockPytango: -# print '\t***', plugins -# msg = 'Taurus is using Tango scheme, but you are blocking PyTango' -# self.assertFalse('tango' in plugins, msg) -# else: -# msg = 'Taurus can not use Tango scheme, maybe you have not' +\ -# ' installed PyTango' -# self.assertTrue('tango' in plugins, msg) -# -# -# def tearDown(self): -# ''' Restore the original PyTango module''' -# # *TODO -# #self._modmanager.reloadModule('PyTango') -# pass -# diff --git a/lib/taurus/core/util/argparse/taurusargparse.py b/lib/taurus/core/util/argparse/taurusargparse.py index eb7eea3bf..14f129c7d 100644 --- a/lib/taurus/core/util/argparse/taurusargparse.py +++ b/lib/taurus/core/util/argparse/taurusargparse.py @@ -108,6 +108,7 @@ def get_taurus_parser(parser=None): help_taurusserial = "taurus serialization mode. Allowed values are (case insensitive): "\ "serial, concurrent (default)" help_rcport = "enables remote debugging using the given port" + help_formatter = "Override the default formatter" group.add_option("--taurus-log-level", dest="taurus_log_level", metavar="LEVEL", help=help_tauruslog, type="str", default="info") group.add_option("--taurus-polling-period", dest="taurus_polling_period", metavar="MILLISEC", @@ -118,6 +119,9 @@ def get_taurus_parser(parser=None): help=help_tangohost, type="str", default=None) group.add_option("--remote-console-port", dest="remote_console_port", metavar="PORT", help=help_rcport, type="int", default=None) + group.add_option("--default-formatter", dest="default_formatter", + metavar="FORMATTER", help=help_formatter, type="str", + default=None) parser.add_option_group(group) return parser @@ -197,6 +201,12 @@ def init_taurus_args(parser=None, args=None, values=None): except Exception, e: taurus.warning("Cannot spawn debugger. Reason: %s", str(e)) + # initialize default formatter + if options.default_formatter is not None: + from taurus import tauruscustomsettings + setattr(tauruscustomsettings, 'DEFAULT_FORMATTER', + options.default_formatter) + return parser, options, args diff --git a/lib/taurus/core/util/event.py b/lib/taurus/core/util/event.py index 647070618..1c9cf182b 100644 --- a/lib/taurus/core/util/event.py +++ b/lib/taurus/core/util/event.py @@ -48,8 +48,8 @@ class BoundMethodWeakref(object): def __init__(self, bound_method, del_cb=None): cb = (del_cb and self._deleted) - self.func_ref = weakref.ref(bound_method.im_func, cb) - self.obj_ref = weakref.ref(bound_method.im_self, cb) + self.func_ref = weakref.ref(bound_method.__func__, cb) + self.obj_ref = weakref.ref(bound_method.__self__, cb) if cb: self.del_cb = CallableRef(del_cb) self.already_deleted = 0 @@ -100,6 +100,28 @@ def CallableRef(object, del_cb=None): return weakref.ref(object, del_cb) +# Reimplementation of BoundMethodWeakref class to avoid to have a hard +# reference in the event callbacks. +# Related to "Keeping references to event callbacks after unsubscribe_event" +# PyTango #185 issue. +class _BoundMethodWeakrefWithCall(BoundMethodWeakref): + + def __init__(self, bound_method, del_cb=None): + """ Reimplementation of __init__ method""" + super(_BoundMethodWeakrefWithCall, self).__init__(bound_method, + del_cb=del_cb) + self.__name__ = self.func_ref().__name__ + + def __call__(self, *args, **kwargs): + """ Retrieve references and call callback with arguments + """ + obj = self.obj_ref() + if obj is not None: + func = self.func_ref() + if func is not None: + return func(obj, *args, **kwargs) + + class EventStack(object): "internal usage event stack" @@ -540,7 +562,7 @@ def eventReceived(self, s, t, v): elif t == taurus.core.taurusbasetypes.TaurusEventType.Error: self.fireEvent(None) else: - self.fireEvent(v.value) + self.fireEvent(v.rvalue) def fireEvent(self, v): """Notifies that a given event has arrived @@ -683,7 +705,7 @@ def unlock(self): def eventReceived(self, s, t, v): if t not in (taurus.core.taurusbasetypes.TaurusEventType.Change, taurus.core.taurusbasetypes.TaurusEventType.Periodic): return - self.fireEvent(s, v.value) + self.fireEvent(s, v.rvalue) def fireEvent(self, s, v): t = time.time() diff --git a/lib/taurus/core/util/fandango_search.py b/lib/taurus/core/util/fandango_search.py new file mode 100644 index 000000000..3eb2c530f --- /dev/null +++ b/lib/taurus/core/util/fandango_search.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +""" +fandango_search.py: methods for getting matching device/attribute/alias names +from Tango database + +These methods have been borrowed from fandango modules. +""" +# TODO: tango-centric + +import re +import taurus + +############################################################################### +# Utils + + +def searchCl(regexp, target): + return re.search(extend_regexp(regexp).lower(), target.lower()) + + +def matchCl(regexp, target): + return re.match(extend_regexp(regexp).lower(), target.lower()) + + +def is_regexp(s): + return any(c in s for c in '.*[]()+?') + + +def extend_regexp(s): + s = str(s).strip() + if '.*' not in s: + s = s.replace('*', '.*') + if '.*' not in s: + if ' ' in s: + s = s.replace(' ', '.*') + if '/' not in s: + s = '.*' + s + '.*' + else: + if not s.startswith('^'): + s = '^' + s + if not s.endswith('$'): + s = s + '$' + return s + + +def isString(s): + typ = s.__class__.__name__.lower() + return not hasattr(s, '__iter__') and 'str' in typ and 'list' not in typ + + +def isCallable(obj): + return hasattr(obj, '__call__') + + +def isMap(obj): + return hasattr(obj, 'has_key') or hasattr(obj, 'items') + + +def isDictionary(obj): + return isMap(obj) + + +def isSequence(obj): + typ = obj.__class__.__name__.lower() + return (hasattr(obj, '__iter__') or 'list' in typ) and not isString(obj) and not isMap(obj) + + +def split_model_list(modelNames): + '''convert str to list if needed (commas and whitespace are considered as separators)''' + if isString(modelNames): # isinstance(modelNames,(basestring,Qt.QString)): + modelNames = str(modelNames).replace(',', ' ') + modelNames = modelNames.split() + if isSequence(modelNames): # isinstance(modelNames,(list.Qt.QStringList)): + modelNames = [str(s) for s in modelNames] + return modelNames + + +def get_matching_devices(expressions, limit=0, exported=False): + """ + Searches for devices matching expressions, if exported is True only running devices are returned + """ + db = taurus.Authority() + all_devs = [s.lower() for s in db.get_device_name('*', '*')] + # This code is used to get data from multiples hosts + #if any(not fun.matchCl(rehost,expr) for expr in expressions): all_devs.extend(get_all_devices(exported)) + # for expr in expressions: + #m = fun.matchCl(rehost,expr) + # if m: + #host = m.groups()[0] + # print 'get_matching_devices(%s): getting %s devices ...'%(expr,host) + #odb = PyTango.Database(*host.split(':')) + #all_devs.extend('%s/%s'%(host,d) for d in odb.get_device_name('*','*')) + result = [e for e in expressions if e.lower() in all_devs] + expressions = [extend_regexp(e) for e in expressions if e not in result] + result.extend(filter(lambda d: any(matchCl(extend_regexp(e), d) + for e in expressions), all_devs)) + return result + + +def get_device_for_alias(alias): + db = taurus.Authority() + try: + return db.get_device_alias(alias) + except Exception, e: + if 'no device found' in str(e).lower(): + return None + return None # raise e + + +def get_alias_for_device(dev): + db = taurus.Authority() + try: + # .get_database_device().DbGetDeviceAlias(dev) + result = db.get_alias(dev) + return result + except Exception, e: + if 'no alias found' in str(e).lower(): + return None + return None # raise e + + +def get_alias_dict(exp='*'): + tango = taurus.Authority() + return dict((k, tango.get_device_alias(k)) for k in tango.get_device_alias_list(exp)) diff --git a/lib/taurus/core/util/log.py b/lib/taurus/core/util/log.py index 189e00534..9f9bd3251 100644 --- a/lib/taurus/core/util/log.py +++ b/lib/taurus/core/util/log.py @@ -700,6 +700,14 @@ def addLogHandler(self, handler): self.log_obj.addHandler(handler) self.log_handlers.append(handler) + def removeLogHandler(self, handler): + """Removes the given handler from this object's logger + + :param handler: (logging.Handler) the handler to be removed + """ + self.log_obj.removeHandler(handler) + self.log_handlers.remove(handler) + def copyLogHandlers(self, other): """Copies the log handlers of other object to this object diff --git a/lib/taurus/external/ordereddict/__init__.py b/lib/taurus/external/ordereddict/__init__.py new file mode 100644 index 000000000..02d40fa12 --- /dev/null +++ b/lib/taurus/external/ordereddict/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +############################################################################## +## +## This file is part of Taurus +## +## http://taurus-scada.org +## +## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +## Taurus is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Taurus is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Taurus. If not, see . +## +############################################################################## + +from collections import OrderedDict +from taurus.core.util import log as __log + +__log.deprecated(dep='taurus.external.ordereddict', rel='4.0.3', + alt='collections.OrderedDict') diff --git a/lib/taurus/qt/qtcore/model/taurusdatabasemodel.py b/lib/taurus/qt/qtcore/model/taurusdatabasemodel.py index 19131323a..4347da3d3 100644 --- a/lib/taurus/qt/qtcore/model/taurusdatabasemodel.py +++ b/lib/taurus/qt/qtcore/model/taurusdatabasemodel.py @@ -24,6 +24,7 @@ ############################################################################# """This module provides widgets that display the database in a tree format""" +# TODO: tango-centric __all__ = ["TaurusTreeDevicePartItem", "TaurusTreeDeviceDomainItem", "TaurusTreeDeviceFamilyItem", "TaurusTreeDeviceMemberItem", "TaurusTreeSimpleDeviceItem", @@ -42,8 +43,6 @@ from taurus.core.taurusbasetypes import TaurusElementType, TaurusDevState import taurus.qt.qtcore.mimetypes -from taurus.core.tango.tangodatabase import TangoInfo, TangoDatabase - from .taurusmodel import TaurusBaseTreeItem, TaurusBaseModel, TaurusBaseProxyModel ElemType = TaurusElementType @@ -80,8 +79,12 @@ def getDevStateToolTip(*args, **kwargs): class TaurusTreeDbBaseItem(TaurusBaseTreeItem): - DisplayFunc = TangoInfo.name - + try: + # TODO: tango-centric + from taurus.core.tango.tangodatabase import TangoInfo + DisplayFunc = TangoInfo.name + except: + pass class TaurusTreeDevicePartItem(TaurusTreeDbBaseItem): """A node designed to represent a 'part' (or totality) of a device name""" @@ -458,6 +461,8 @@ def pyData(self, index, role): def setupModelData(self, data): if data is None: return + + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() devices = data.devices() @@ -487,6 +492,8 @@ class TaurusDbSimpleDeviceAliasModel(TaurusDbBaseModel): def setupModelData(self, data): if data is None: return + + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() devices = data.devices() @@ -510,6 +517,8 @@ class TaurusDbPlainDeviceModel(TaurusDbBaseModel): def setupModelData(self, data): if data is None: return + + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() devices = data.devices() @@ -522,7 +531,7 @@ def setupModelData(self, data): class TaurusDbDeviceModel(TaurusDbBaseModel): - """A Qt model that structures device elements is a 3 level tree organized + """A Qt model that structures device elements in a 3 level tree organized as: - @@ -534,6 +543,12 @@ class TaurusDbDeviceModel(TaurusDbBaseModel): def setupModelData(self, data): if data is None: return + try: + # TODO: Tango-centric + # TODO: is this try needed? (not done in, e.g. TaurusDbPlainDeviceModel) + from taurus.core.tango.tangodatabase import TangoDatabase + except ImportError: + return if isinstance(data, TangoDatabase): data = data.deviceTree() @@ -564,6 +579,7 @@ def setupModelData(self, data): if data is None: return + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() @@ -594,6 +610,7 @@ def setupModelData(self, data): if data is None: return + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() @@ -645,6 +662,7 @@ def setupModelData(self, data): if data is None: return + from taurus.core.tango.tangodatabase import TangoDatabase if isinstance(data, TangoDatabase): data = data.cache() diff --git a/lib/taurus/qt/qtcore/util/emitter.py b/lib/taurus/qt/qtcore/util/emitter.py index 0212de1fc..df6d4c235 100644 --- a/lib/taurus/qt/qtcore/util/emitter.py +++ b/lib/taurus/qt/qtcore/util/emitter.py @@ -4,29 +4,31 @@ ## # This file is part of Taurus ## -# http://www.tango-controls.org/static/tau/latest/doc/html/index.html +# http://taurus-scada.org ## -# (copyleft) CELLS / ALBA Synchrotron, Bellaterra, Spain +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## -# This is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## -# This software is distributed in the hope that it will be useful, +# Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# GNU Lesser General Public License for more details. ## -# You should have received a copy of the GNU General Public License -# along with this program; if not, see . -########################################################################### +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# """ -emitter.py: This module provides a task scheduler used by TaurusGrid and TaurusDevTree widgets +emitter.py: This module provides a task scheduler used by TaurusGrid and + TaurusDevTree widgets """ -import Queue +from Queue import Queue, Empty import traceback from functools import partial from collections import Iterable @@ -36,6 +38,7 @@ from taurus.core.util.log import Logger from taurus.core.util.singleton import Singleton +from taurus.core.taurusbasetypes import SubscriptionState ############################################################################### @@ -90,33 +93,44 @@ class TaurusEmitterThread(Qt.QThread): The TaurusEmitterThread Class ========================== - This object get items from a python Queue and performs a thread safe operation on them. + This object get items from a python Queue and performs a thread safe + operation on them. It is useful to serialize Qt tasks in a background thread. :param parent: a Qt/Taurus object :param name: identifies object logs - :param queue: if None parent.getQueue() is used, if not then the queue passed as argument is used + :param queue: if None parent.getQueue() is used, if not then the queue + passed as argument is used :param method: the method to be executed using each queue item as argument - :param cursor: if True or QCursor a custom cursor is set while the Queue is not empty + :param cursor: if True or QCursor a custom cursor is set while + the Queue is not empty How TaurusEmitterThread works -------------------------- - TaurusEmitterThread is a worker thread that processes a queue of iterables passed as arguments to the specified method every time that ``doSomething()`` is called: + TaurusEmitterThread is a worker thread that processes a queue of iterables + passed as arguments to the specified method every time that + ``doSomething()`` is called: - * ``self.method(*item)`` will be called if TaurusEmitterThread.method has been initialized. - * ``item[0](item[1:])`` will be called if ``method`` is not initialized and the first element of the queued iterable is *callable*. + * ``self.method(*item)`` will be called if TaurusEmitterThread.method + has been initialized. + * ``item[0](item[1:])`` will be called if ``method`` is not initialized + and the first element of the queued iterable is *callable*. TaurusEmitterThread uses two queues: * ``self.queue`` manages the objects added externally: - + the ``next()`` method passes objects from ``self.queue`` to ``self.todo queue`` + + the ``next()`` method passes objects from ``self.queue`` + to ``self.todo queue`` + every time that a *somethingDone* signal arrives ``next()`` is called. - + ``next()`` can be called also externally to ensure that the main queue is being processed. + + ``next()`` can be called also externally to ensure that the main queue + is being processed. + the queue can be accessed externally using ``getQueue()`` - + ``getQueue().qsize()`` returns the number of remaining objects in queue. - + while there are objects in queue the ``.next()`` method will override applications cursor. a call to ``next()`` with an empty queue will restore the original cursor. + + ``getQueue().qsize()`` returns number of remaining objects in queue. + + while there are objects in queue the ``.next()`` method will + override applications cursor. a call to ``next()`` with an empty queue + will restore the original cursor. * ``self.todo`` is managed by the ``run()/start()`` method: @@ -130,7 +144,7 @@ class TaurusEmitterThread(Qt.QThread): .. code-block:: python #Applying TaurusEmitterThread to an existing class: - import Queue + from Queue import Queue from functools import partial def modelSetter(args): @@ -141,18 +155,21 @@ def modelSetter(args): ... def __init__(self, parent = None, designMode = False): ... - self.modelsQueue = Queue.Queue() - self.modelsThread = TaurusEmitterThread(parent=self,queue=self.modelsQueue,method=modelSetter ) + self.modelsQueue = Queue() + self.modelsThread = TaurusEmitterThread(parent=self, + queue=self.modelsQueue,method=modelSetter ) ... def build_widgets(...): ... - previous,synoptic_value = synoptic_value,TaurusValue(cell_frame) + previous,synoptic_value = \ + synoptic_value,TaurusValue(cell_frame) #synoptic_value.setModel(model) self.modelsQueue.put((synoptic_value,model)) ... def setModel(self,model): ... - if hasattr(self,'modelsThread') and not self.modelsThread.isRunning(): + if hasattr(self,'modelsThread') and \ + not self.modelsThread.isRunning(): self.modelsThread.start() elif self.modelsQueue.qsize(): self.modelsThread.next() @@ -160,21 +177,36 @@ def setModel(self,model): """ - def __init__(self, parent=None, name='', queue=None, method=None, cursor=None, sleep=5000): + def __init__(self, parent=None, name='', queue=None, method=None, + cursor=None, sleep=5000, polling=0, loopwait=5): """ - Parent most not be None and must be a TaurusGraphicsScene! + Parent must be not None and must be a TaurusGraphicsScene! + + :param queue: pass an external action queue (optional) + :param method: action processor (e.g. modelSetter) + :param cursor: QCursor during process (optional) + :param sleep: delay in ms before thread start + :param polling: process actions at fix period (milliseconds) + :param loopwait: wait N milliseconds between actions """ Qt.QThread.__init__(self, parent) self.name = name self.log = Logger('TaurusEmitterThread(%s)' % self.name) self.log.setLogLevel(self.log.Info) - self.queue = queue or Queue.Queue() - self.todo = Queue.Queue() + self.queue = queue or Queue() + self.todo = Queue() self.method = method self.cursor = Qt.QCursor( Qt.Qt.WaitCursor) if cursor is True else cursor self._cursor = False - self.timewait = sleep + self.timewait = int(sleep) + self.polling = int(polling) + self.loopwait = int(loopwait) + if self.polling: + self.refreshTimer = Qt.QTimer() + self.refreshTimer.timeout.connect(self.onRefresh) + else: + self.refreshTimer = None self.emitter = QEmitter() self.emitter.moveToThread(Qt.QApplication.instance().thread()) @@ -184,7 +216,20 @@ def __init__(self, parent=None, name='', queue=None, method=None, cursor=None, s self._done = 0 # Moved to the end to prevent segfaults ... self.emitter.doSomething.connect(self._doSomething) - self.emitter.somethingDone.connect(self.next) + + if not self.refreshTimer: + self.emitter.somethingDone.connect(self.next) + + def onRefresh(self): + try: + size = self.getQueue().qsize() + if size: + self.log.info('onRefresh(%s)' % size) + self.next() + else: + self.log.debug('onRefresh()') + except: + self.log.warning(traceback.format_exc()) def getQueue(self): if self.queue: @@ -196,7 +241,8 @@ def getQueue(self): def getDone(self): """ Returns % of done tasks in 0-1 range """ - return self._done / (self._done + self.getQueue().qsize()) if self._done else 0. + pending = self.getQueue().qsize() + return float(self._done) / (self._done + pending) def clear(self): while not self.todo.empty(): @@ -205,8 +251,11 @@ def clear(self): self.getQueue().get() self._done += 1 - def purge(obj): - nqueue = Queue.Queue() + def purge(self, obj): + """ + Remove a given object from all queues + """ + nqueue = Queue() while not self.todo.empty(): i = self.todo.get() if obj not in i: @@ -229,20 +278,25 @@ def _doSomething(self, params): try: method(*args) except: - self.log.error('At TaurusEmitterThread._doSomething(%s): \n%s' % ( - map(str, args), traceback.format_exc())) + self.log.error('At TaurusEmitterThread._doSomething(%s): \n%s' + % (map(str, args), traceback.format_exc())) self.emitter.somethingDone.emit() self._done += 1 return def next(self): queue = self.getQueue() - msg = 'At TaurusEmitterThread.next(), %d items remaining.' % queue.qsize() - (queue.empty() and self.log.info or self.log.debug)(msg) + msg = ('At TaurusEmitterThread.next(), %d items remaining.' + % queue.qsize()) + if (queue.empty() and not self.polling): + self.log.info(msg) + else: + self.log.debug(msg) try: if not queue.empty(): if not self._cursor and self.cursor is not None: - Qt.QApplication.instance().setOverrideCursor(Qt.QCursor(self.cursor)) + Qt.QApplication.instance().setOverrideCursor( + Qt.QCursor(self.cursor)) self._cursor = True # A blocking get here would hang the GUIs!!! item = queue.get(False) @@ -252,7 +306,7 @@ def next(self): Qt.QApplication.instance().restoreOverrideCursor() self._cursor = False - except Queue.Empty: + except Empty: self.log.warning(traceback.format_exc()) pass except: @@ -260,11 +314,14 @@ def next(self): return def run(self): - Qt.QApplication.instance().thread().sleep(int(self.timewait / 1000) - if self.timewait > 10 else int(self.timewait)) # wait(self.sleep) + Qt.QApplication.instance().thread().msleep(self.timewait) self.log.info('#' * 80) self.log.info('At TaurusEmitterThread.run()') self.next() + + if self.refreshTimer: + self.refreshTimer.start(self.polling) + while True: self.log.debug('At TaurusEmitterThread.run() loop.') item = self.todo.get(True) @@ -275,33 +332,116 @@ def run(self): continue self.log.debug('Emitting doSomething signal ...') self.emitter.doSomething.emit(item) - # End of while + if self.loopwait: + self.msleep(self.loopwait) + # End of while self.log.info( '#' * 80 + '\nOut of TaurusEmitterThread.run()' + '\n' + '#' * 80) # End of Thread -class SingletonWorker(): # Qt.QObject): +class DelayedSubscriber(Logger): """ - The SingletonWorker works - ========================= + DelayedSubscriber(schema) will use a TaurusEmitterThread to perform + a thread safe delayed subscribing on all Attributes of a given + Taurus Schema that has not been previously subscribed. + + .. warning:: This class belongs to a "Delayed Event Subscription" API added + in v.4.2.1-alpha as an *experimental* feature. This API may + not be stable and/or it may be removed in a future release + (even on a minor version change) + """ + + def __init__(self, schema, parent=None, sleep=10000, pause=5, period=0): + + self._schema = schema + self.call__init__(Logger, 'DelayedSubscriber(%s)' % self._schema, None) + self._factory = taurus.Factory(schema) + + self._modelsQueue = Queue() + self._modelsThread = TaurusEmitterThread(parent=parent, + queue=self._modelsQueue, + method=self._modelSubscriber, + sleep=sleep, loopwait=pause, + polling=period) - The SingletonWorker class is constructed using the same arguments than the TaurusTreadEmitter class ; but instead of creating a QThread for each instance of the class it creates a single QThread for all instances. + self._modelsQueue.put((self.addUnsubscribedAttributes,)) + self._modelsThread.start() - The Queue is still different for each of the instances; it is connected to the TaurusEmitterThread signals (*next()* and *somethingDone()*) and each Worker queue is used as a feed for the shared QThread. + def _modelSubscriber(self, method, args=[]): + self.debug('modelSubscriber(%s,%s)' % (method, args)) + return method(*args) + + def getUnsubscribedAttributes(self): + """Check all pending subscriptions in the current factory + """ + attrs = [] + items = self._factory.getExistingAttributes().items() + for name, attr in items: + if attr is None: + continue + elif attr.hasListeners() and not attr.isUsingEvents(): + attrs.append(attr) + + return attrs + + def addUnsubscribedAttributes(self): + """Schedule subscription for all pending attributes + """ + try: + items = self.getUnsubscribedAttributes() + if len(items): + self.info('addUnsubscribedAttributes([%d])' % len(items)) + for attr in items: + self._addModelObj(attr) + self._modelsThread.next() + self.info('Thread queue: [%d]' % (self._modelsQueue.qsize())) + except: + self.warning(traceback.format_exc()) + + def _addModelObj(self, modelObj): + parent = modelObj.getParentObj() + if parent: + proxy = parent.getDeviceProxy() + if not proxy: + self.debug('addModelObj(%s), proxy not available' % modelObj) + return + + self._modelsQueue.put((modelObj.subscribePendingEvents,)) + self.debug('addModelObj(%s)' % str(modelObj)) + + def cleanUp(self): + self.trace("[DelayedSubscriber] cleanUp") + self._modelsThread.stop() + Logger.cleanUp(self) + + +class SingletonWorker(): + """ + SingletonWorker is used to manage TaurusEmitterThread as Singleton objects + + SingletonWorker is constructed using the same arguments + than TaurusTreadEmitter ; but instead of creating a QThread for each + instance it creates a single QThread for all instances. + + The Queue is still different for each of the instances; it is connected + to the TaurusEmitterThread signals (*next()* and *somethingDone()*) + and each Worker queue is used as a feed for the shared QThread. This implementation reduced the cpu of vacca application in a 50% factor. :param parent: a Qt/Taurus object :param name: identifies object logs - :param queue: if None parent.getQueue() is used, if not then the queue passed as argument is used + :param queue: if None parent.getQueue() is used, if not then the queue + passed as argument is used :param method: the method to be executed using each queue item as argument - :param cursor: if True or QCursor a custom cursor is set while the Queue is not empty - This class is used to manage TaurusEmitterThread as Singleton objects: + :param cursor: if True or QCursor a custom cursor is set while + the Queue is not empty """ _thread = None - def __init__(self, parent=None, name='', queue=None, method=None, cursor=None, sleep=5000, log=Logger.Warning, start=True): + def __init__(self, parent=None, name='', queue=None, method=None, + cursor=None, sleep=5000, log=Logger.Warning, start=True): self.name = name self.log = Logger('SingletonWorker(%s)' % self.name) self.log.setLogLevel(log) @@ -313,7 +453,7 @@ def __init__(self, parent=None, name='', queue=None, method=None, cursor=None, s SingletonWorker._thread = TaurusEmitterThread( parent, name='SingletonWorker', cursor=cursor, sleep=sleep) self.thread = SingletonWorker._thread - self.queue = queue or Queue.Queue() + self.queue = queue or Queue() if start: self.start() @@ -328,9 +468,11 @@ def next(self, item=None): self.put(item) elif self.queue.empty(): return - msg = 'At SingletonWorker.next(), %d items not passed yet to Emitter.' % self.queue.qsize() + msg = ('At SingletonWorker.next(), ' + '%d items not passed yet to Emitter.' + % self.queue.qsize()) self.log.info(msg) - #(queue.empty() and self.log.info or self.log.debug)(msg) + # (queue.empty() and self.log.info or self.log.debug)(msg) try: i = 0 while not self.queue.empty(): @@ -343,7 +485,7 @@ def next(self, item=None): i += 1 self.log.info('%d Items added to emitter queue' % i) self.thread.emitter.newQueue.emit() - except Queue.Empty: + except Empty: self.log.warning(traceback.format_exc()) except: self.log.warning(traceback.format_exc()) @@ -375,20 +517,25 @@ def stop(self): def clear(self): """ This method will clear queue only if next() has not been called. - If you call self.thread.clear() it will clear objects for all workers!, be careful + If you call self.thread.clear() it will clear objects for all workers!, + be careful """ while not self.queue.empty(): self.queue.get() - # self.thread.clear() + # self.thread.clear() - def purge(obj): - nqueue = Queue.Queue() + def purge(self, obj): + """ + Remove a given object from all queues + """ + nqueue = Queue() while not self.queue.empty(): i = self.queue.get() if obj not in i: nqueue.put(i) while not nqueue.empty(): self.queue.put(nqueue.get()) + self.next() def isRunning(self): return self._running diff --git a/lib/taurus/qt/qtcore/util/properties.py b/lib/taurus/qt/qtcore/util/properties.py index 44cd52789..db292eed9 100644 --- a/lib/taurus/qt/qtcore/util/properties.py +++ b/lib/taurus/qt/qtcore/util/properties.py @@ -57,7 +57,7 @@ def resetFilters(self): from functools import partial from taurus.external.qt import Qt -from taurus.core.tango.search import * +from taurus.core.util.fandango_search import isSequence, isDictionary def join(*seqs): diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index a2e0cc2ca..c0499237f 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -34,8 +34,11 @@ # register icon path files and icon theme on import of taurus.qt.qtgui import icon as __icon import os +import sys import glob +import pkg_resources from taurus import tauruscustomsettings as __S +from taurus import debug as __debug icon_dir = os.path.join(os.path.dirname(os.path.abspath(__icon.__file__))) # TODO: get .path file glob pattern from tauruscustomsettings @@ -45,4 +48,25 @@ path=getattr(__S, 'QT_THEME_DIR', ''), force=getattr(__S, 'QT_THEME_FORCE_ON_LINUX', False)) -del os, glob, __icon, icon_dir \ No newline at end of file +# ------------------------------------------------------------------------ +# Note: this is an experimental feature introduced in v 4.3.0a +# It may be removed or changed in future releases + +# Discover the taurus.qt.qtgui plugins +__mod = __modname = None +for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui'): + try: + __modname = '%s.%s' % (__name__, __p.name) + __mod = __p.load() + # Add it to the current module + setattr(sys.modules[__name__], __p.name, __mod) + # Add it to sys.modules + sys.modules[__modname] = __mod + __debug('Plugin "%s" loaded as "%s"', __p.module_name, __modname) + except Exception as e: + __debug('Could not load plugin "%s". Reason: %s', __p.module_name, e) + +# ------------------------------------------------------------------------ + +del os, glob, __icon, icon_dir, pkg_resources, sys, __mod, __modname, __debug + diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 9c05e7621..f4b561240 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -95,7 +95,8 @@ class TaurusBaseComponent(TaurusListener, BaseConfigurableClass): _eventBufferPeriod = 0 # Python format string or Formatter callable - FORMAT = defaultFormatter + # (None means that the default formatter will be used) + FORMAT = None # Dictionary mapping dtypes to format strings defaultFormatDict = {float: "{:.{bc.modelObj.precision}f}", @@ -144,11 +145,20 @@ def __init__(self, name, parent=None, designMode=False): else: self._exception_listener = set([TaurusExceptionListener()]) + # Use default formatter if none has been set by the class + if self.FORMAT is None: + self.setFormat(getattr(taurus.tauruscustomsettings, + 'DEFAULT_FORMATTER', + defaultFormatter)) + # register configurable properties - self.registerConfigProperty( - self.isModifiableByUser, self.setModifiableByUser, "modifiableByUser") + self.registerConfigProperty(self.isModifiableByUser, + self.setModifiableByUser, + "modifiableByUser") self.registerConfigProperty( self.getModelInConfig, self.setModelInConfig, "ModelInConfig") + self.registerConfigProperty(self.getFormat, self.setFormat, + 'formatter') self.resetModelInConfig() @deprecation_decorator(rel='4.0') @@ -630,6 +640,16 @@ def getModelFragmentObj(self, fragmentName=None): fragmentName = self.modelFragmentName return self.modelObj.getFragmentObj(fragmentName) + def getModelIndexValue(self): + """ + Called inside getDisplayValue to use with spectrum attributes. + By default not used, but some widget might want to support this + feature. + + Override when needed. + """ + return None + def getFormatedToolTip(self, cache=True): """Returns a string with contents to be displayed in a tooltip. @@ -670,20 +690,24 @@ def displayValue(self, v): :function:`defaultFormatter`, which makes use of :attribute:`defaultFormatDict`. - In order to customize the formatting behaviour, one can change - :attribute:`defaultFormatDict` or :attribute:`FORMAT` directly - at class level, or use :method:`setFormat` to alter the - format string of an specific instance + In order to customize the formatting behaviour, one can + use :method:`setFormat` to alter the formatter of an specific instance + (recommended) or change :attribute:`defaultFormatDict` or + :attribute:`FORMAT` directly at class level. - `FORMAT` can be set to a python format string [1] or a callable + The formatter can be set to a python format string [1] or a callable that returns a python format string. If a callable is used, it will be called with the following keyword arguments: - dtype: the data type of the value to be formatted - - basecomponent: the affected widget + - basecomponent: the affected widget The following are some examples for customizing the formatting: + - Change the format for widget instance `foo`: + + foo.setFormat("{:.2e}") + - Change FORMAT for all widgets (using a string): TaurusBaseComponent.FORMAT = "{:.2e}" @@ -695,9 +719,13 @@ def baseFormatter(dtype=None, basecomponent=None, **kwargs): TaurusLabel.FORMAT = baseFormatter - - Use the defaultFormatter but modify the format string for dtype=str: + - Use the defaultFormatDict but modify the format string for dtype=str: + + TaurusLabel.defaultFormatDict.update({"str": "{!r}"}) - TaurusBaseComponent.defaultFormatDict.update({"str": "{!r}"}) + .. seealso:: :attribute:`tauruscustomsettings.DEFAULT_FORMATTER`, + `--default-formatter` option in :class:`TaurusApplication`, + :meth:`TaurusBaseWidget.onSetFormatter` [1] https://docs.python.org/2/library/string.html @@ -743,14 +771,37 @@ def _updateFormat(self, dtype, **kwargs): def setFormat(self, format): """ Method to set the `FORMAT` attribute for this instance. It also resets the internal format string, which will be recalculated - in the next call to :method"`displayValue` + in the next call to :method:`displayValue` - :param format: (str or callable) A format string or a callable - that returns it + :param format: (str or callable) A format string + or a formatter callable (or the callable name in + "full.module.callable" format) """ + # Check if the format is a callable string representation + if isinstance(format, basestring): + try: + moduleName, formatterName = format.rsplit('.', 1) + __import__(moduleName) + module = sys.modules[moduleName] + format = getattr(module, formatterName) + except: + format = str(format) self.FORMAT = format self.resetFormat() + def getFormat(self): + """ Method to get the `FORMAT` attribute for this instance. + + :return: (str) a string of the current format. + It could be a python format string or a callable string representation. + """ + if isinstance(self.FORMAT, basestring): + formatter = self.FORMAT + else: + formatter = '{0}.{1}'.format(self.FORMAT.__module__, + self.FORMAT.__name__) + return formatter + def resetFormat(self): """Reset the internal format string. It forces a recalculation in the next call to :method:`displayValue`. @@ -777,6 +828,11 @@ def getDisplayValue(self, cache=True, fragmentName=None): except: return self.getNoneValue() + idx = self.getModelIndexValue() + if v is not None and idx: + for i in idx: + v = v[i] + return self.displayValue(v) def setNoneValue(self, v): @@ -1244,6 +1300,44 @@ def __init__(self, name, parent=None, designMode=False): parent=parent, designMode=designMode) self._setText = self._findSetTextMethod() + def showFormatterDlg(self): + """ + showFormatterDlg show a dialog to get the formatter from the user. + :return: formatter: python fromat string or formatter callable + (in string version) or None + """ + current_format = self.getFormat() + + formatter, ok = Qt.QInputDialog.getText(self, "Set formatter", + "Enter a formatter:", + Qt.QLineEdit.Normal, + current_format) + if ok and formatter: + return formatter + + return None + + def onSetFormatter(self): + """Slot to allow interactive setting of the Formatter. + + .. seealso:: :meth:`TaurusBaseWidget.showFormatterDlg`, + :meth:`TaurusBaseComponent.displayValue`, + :attribute:`tauruscustomsettings.DEFAULT_FORMATTER` + """ + format = self.showFormatterDlg() + if format is not None: + self.debug( + 'Default format has been changed to: {0}'.format(format)) + # ----------------------------------------------------------------- + # TODO: Tango-centric (replace by agnostic entry point solution) + # shortcut to setup the tango formatter + if format.strip() == "tangoFormatter": + from taurus.core.tango.util.formatter import tangoFormatter + format = tangoFormatter + # ----------------------------------------------------------------- + self.setFormat(format) + return format + # It makes the GUI to hang... If this needs implementing, we should # reimplement it using the Qt parent class, not QWidget... # def destroy(self): @@ -1995,6 +2089,21 @@ def getOperationCallbacks(self): ''' return [] + def getDisplayValue(self, cache=True, fragmentName=None): + """Reimplemented from class:`TaurusBaseWidget`""" + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # The widgets inheriting from this class interact with + # writable models and therefore the fragmentName should fall back to 'wvalue' + # instead of 'rvalue'. + # But changing it now is delicate due to risk of introducing API + # incompatibilities for widgets already assuming the current default. + # So instead of reimplementing it here, the fix was constrained to + # TaurusValueLineEdit.getDisplayValue() + # TODO: Consider reimplementing this to use wvalue by default + return TaurusBaseWidget.getDisplayValue(self, cache=cache, + fragmentName=fragmentName) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def getValue(self): ''' This method must be implemented in derived classes to return diff --git a/lib/taurus/qt/qtgui/base/tauruscontroller.py b/lib/taurus/qt/qtgui/base/tauruscontroller.py index 195b9ba9c..28103fe6d 100644 --- a/lib/taurus/qt/qtgui/base/tauruscontroller.py +++ b/lib/taurus/qt/qtgui/base/tauruscontroller.py @@ -97,11 +97,17 @@ def getDisplayValue(self, write=False): return self.widget().getDisplayValue() def handleEvent(self, evt_src, evt_type, evt_value): - if evt_src == self.modelObj(): # update the "_last" values only if the event source is the model (it could be the background...) - if evt_type == TaurusEventType.Change or evt_type == TaurusEventType.Periodic: + # update the "_last" values only if the event source is the model + # (it could be the background...) + if evt_src == self.modelObj(): + if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic): + if self._last_value is None: + # reset the format so that it gets updated by displayValue + self.widget().resetFormat() self._last_value = evt_value elif evt_type == TaurusEventType.Config: # TODO: adapt to tep14 self._last_config_value = evt_value + self.widget().resetFormat() else: self._last_error_value = evt_value # In case of error, modify the last_value as well @@ -243,7 +249,7 @@ def deviceObj(self): @property def configParam(self): if self._configParam is None: - self._configParam = self.widget().getModelFragmentName() or '' + self._configParam = self.widget().modelFragmentName or '' return self._configParam def getDisplayValue(self, write=False): @@ -306,7 +312,7 @@ def updateLabelBackground(ctrl, widget): if ctrl.usePalette(): widget.setAutoFillBackground(True) - if bgRole in ('', 'none'): + if bgRole in ('', 'none', 'None'): transparentBrush = Qt.QBrush(Qt.Qt.transparent) frameBrush = transparentBrush bgBrush, fgBrush = transparentBrush, Qt.QBrush(Qt.Qt.black) @@ -320,10 +326,18 @@ def updateLabelBackground(ctrl, widget): bgItem = ctrl.state() elif bgRole == 'value': bgItem = ctrl.value() + else: + # TODO: this is an *experimental* extension of the bgRole API + # added in v 4.1.2-alpha. It may change in future versions + modelObj = widget.getModelObj() + try: + bgItem = modelObj.getFragmentObj(bgRole) + except: + widget.warning('Invalid bgRole "%s"', bgRole) bgBrush, fgBrush = palette.qbrush(bgItem) _updatePaletteColors(widget, bgBrush, fgBrush, frameBrush) else: - if bgRole in ('', 'none'): + if bgRole in ('', 'none', 'None'): ss = StyleSheetTemplate.format("rgba(0,0,0,0)", "") else: bgItem, palette = None, QT_DEVICE_STATE_PALETTE @@ -334,6 +348,14 @@ def updateLabelBackground(ctrl, widget): bgItem = ctrl.state() elif bgRole == 'value': bgItem = ctrl.value() + else: + # TODO: this is an *experimental* extension of the bgRole API + # added in v 4.1.2-alpha. It may change in future versions + modelObj = widget.getModelObj() + try: + bgItem = modelObj.getFragmentObj(bgRole) + except: + widget.warning('Invalid bgRole "%s"', bgRole) color_ss = palette.qtStyleSheet(bgItem) ss = StyleSheetTemplate.format("rgba(255,255,255,128)", color_ss) widget.setStyleSheet(ss) diff --git a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py index 93b604030..ee49a9920 100644 --- a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py +++ b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py @@ -29,7 +29,7 @@ from taurus.external import unittest from taurus.test import insertTest from taurus.qt.qtgui.test import BaseWidgetTestCase -from taurus.core.tango.test import TangoSchemeTestLauncher +from taurus.core.tango.test import TangoSchemeTestLauncher # tango-centric from taurus.qt.qtgui.container import TaurusWidget DEV_NAME = TangoSchemeTestLauncher.DEV_NAME diff --git a/lib/taurus/qt/qtgui/button/taurusbutton.py b/lib/taurus/qt/qtgui/button/taurusbutton.py index 8d83d23e5..2b5ee9b53 100644 --- a/lib/taurus/qt/qtgui/button/taurusbutton.py +++ b/lib/taurus/qt/qtgui/button/taurusbutton.py @@ -30,8 +30,6 @@ __docformat__ = 'restructuredtext' -import PyTango - from taurus.external.qt import Qt from taurus.core.taurusbasetypes import LockStatus, TaurusLockInfo from taurus.core.taurusdevice import TaurusDevice @@ -265,7 +263,7 @@ class TaurusCommandButton(Qt.QPushButton, TaurusBaseWidget): .. seealso:: :class:`TaurusCommandsForm` provides a good example of use of TaurusCommandButton (including managing the return value) ''' - + # TODO: tango-centric commandExecuted = Qt.pyqtSignal(object) def __init__(self, parent=None, designMode=False, command=None, @@ -372,6 +370,8 @@ def _castParameters(self, parameters=None, command=None, dev=None): :return: (sequence or scalar) a sequence of parameters (or a scalar if only one parameter) ''' + import PyTango + if parameters is None: parameters = self._parameters if command is None: @@ -545,10 +545,10 @@ def getQtDesignerPluginInfo(cls): class TaurusLockButton(Qt.QPushButton, TaurusBaseWidget): - _LOCK_MAP = {LockStatus.Unlocked: "extra_icon:lock_unlocked.svg", - LockStatus.Locked: "extra_icon:lock_locked_unpreviledged.svg", - LockStatus.LockedMaster: "extra_icon:lock_locked.svg", - LockStatus.Unknown: "extra_icon:lock_unknown.svg"} + _LOCK_MAP = {LockStatus.Unlocked: "extra_icons:lock_unlocked.svg", + LockStatus.Locked: "extra_icons:lock_locked_unpreviledged.svg", + LockStatus.LockedMaster: "extra_icons:lock_locked.svg", + LockStatus.Unknown: "extra_icons:lock_unknown.svg"} def __init__(self, parent=None, designMode=False): self._lock_info = TaurusLockInfo() diff --git a/lib/taurus/qt/qtgui/button/test/res/Timeout b/lib/taurus/qt/qtgui/button/test/res/Timeout index a4a421cfa..860b41ba7 100755 --- a/lib/taurus/qt/qtgui/button/test/res/Timeout +++ b/lib/taurus/qt/qtgui/button/test/res/Timeout @@ -40,7 +40,7 @@ The argument value is the time that will take to execute the command. """ -import PyTango +import PyTango # Tango-centric import sys import time diff --git a/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py b/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py index 8fb95c0c0..dd9f542aa 100644 --- a/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py +++ b/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py @@ -33,10 +33,16 @@ from taurus.qt.qtgui.test import BaseWidgetTestCase, GenericWidgetTestCase from taurus.qt.qtgui.button import TaurusCommandButton -# The following are Tango-centric imports. -# TODO: change them if/when TaurusCommandbuttongets generalized -from PyTango import CommunicationFailed -from taurus.core.tango.starter import ProcessStarter +skip, skipmsg = False, None + +try: + # The following are Tango-centric imports. + # TODO: change them if/when TaurusCommandbuttongets generalized + from PyTango import CommunicationFailed + from taurus.core.tango.starter import ProcessStarter +except: + skip = True + skipmsg = "tango-dependent test" class TaurusCommandButtonTest(GenericWidgetTestCase, unittest.TestCase): @@ -44,6 +50,7 @@ class TaurusCommandButtonTest(GenericWidgetTestCase, unittest.TestCase): _modelnames = ['sys/tg_test/1', None, 'sys/database/2', ''] +@unittest.skipIf(skip, skipmsg) class TaurusCommandButtonTest2(BaseWidgetTestCase, unittest.TestCase): _klass = TaurusCommandButton diff --git a/lib/taurus/qt/qtgui/container/taurusmainwindow.py b/lib/taurus/qt/qtgui/container/taurusmainwindow.py index d456f877c..0eeb676f7 100644 --- a/lib/taurus/qt/qtgui/container/taurusmainwindow.py +++ b/lib/taurus/qt/qtgui/container/taurusmainwindow.py @@ -35,6 +35,7 @@ import sys from taurus import tauruscustomsettings +from taurus.core.util import deprecation_decorator from taurus.external.qt import Qt from taurusbasecontainer import TaurusBaseContainer @@ -189,7 +190,9 @@ class TaurusMainWindow(Qt.QMainWindow, TaurusBaseContainer): # Allows the user to change/create/delete perspectives _supportUserPerspectives = True _showLogger = True - _splashLogo = ":/TaurusSplash.png" # set to None for disabling splash screen + # + # set to None for disabling splash screen + _splashLogo = "large:TaurusSplash.png" _splashMessage = "Initializing Main window..." def __init__(self, parent=None, designMode=False, splash=None): @@ -875,6 +878,8 @@ def deleteExternalAppLauncher(self, action): self.unregisterConfigurableItem("_extApp[%s]" % str(action.text()), raiseOnError=False) + @deprecation_decorator(dbg_msg="Change Tango Host action is TangoCentric", + rel="4.1.2") def _onChangeTangoHostAction(self): ''' slot called when the Change Tango Host is triggered. It prompts for a @@ -1080,7 +1085,8 @@ class MyMainWindow(TaurusMainWindow): # Allows the user to change/create/delete perspectives _supportUserPerspectives = True _showLogger = True - _splashLogo = ":/TaurusSplash.png" # set to None for disabling splash screen + # set to None for disabling splash screen + _splashLogo = "large:TaurusSplash.png" _splashMessage = "Initializing Main window..." def __init__(self): diff --git a/lib/taurus/qt/qtgui/dialog/taurusmessagebox.py b/lib/taurus/qt/qtgui/dialog/taurusmessagebox.py index f0a227ed1..1d8142e73 100644 --- a/lib/taurus/qt/qtgui/dialog/taurusmessagebox.py +++ b/lib/taurus/qt/qtgui/dialog/taurusmessagebox.py @@ -306,7 +306,7 @@ def py_exc(): def tg_exc(): - import PyTango + import PyTango # TODO: tango-centric try: PyTango.Except.throw_exception('TangoException', 'A simple tango exception', 'right here') @@ -316,7 +316,7 @@ def tg_exc(): def tg_serv_exc(): - import PyTango + import PyTango # TODO: tango-centric import taurus dev = taurus.Device("sys/tg_test/1") try: @@ -330,7 +330,7 @@ def tg_serv_exc(): def py_tg_serv_exc(): - import PyTango + import PyTango # TODO: tango-centric try: PyTango.Except.throw_exception('TangoException', 'A simple tango exception', 'right here') diff --git a/lib/taurus/qt/qtgui/display/demo/tauruslabeldemo.py b/lib/taurus/qt/qtgui/display/demo/tauruslabeldemo.py index a9ce1d76c..6851f5840 100644 --- a/lib/taurus/qt/qtgui/display/demo/tauruslabeldemo.py +++ b/lib/taurus/qt/qtgui/display/demo/tauruslabeldemo.py @@ -91,7 +91,7 @@ def __init__(self, parent=None): fg_widget.addItems(["", "rvalue", "rvalue.magnitude", "rvalue.units", "wvalue", "wvalue.magnitude", "wvalue.units", "state", "quality", "none"]) - bg_widget.addItems(["quality", "state", "none"]) + bg_widget.addItems(["quality", "state", "value", "none"]) model_widget.textChanged.connect(w.setModel) fg_widget.currentIndexChanged[str].connect(w.setFgRole) @@ -104,6 +104,7 @@ def __init__(self, parent=None): fg_widget.setCurrentIndex(0) fg_widget.setEditable(True) bg_widget.setCurrentIndex(0) + bg_widget.setEditable(True) self.w_label = w self.w_model = model_widget @@ -123,7 +124,7 @@ def __init__(self, parent=None): p1.w_model.setText("sys/tg_test/1/double_scalar") p2 = TaurusLabelTestPanel() p2.w_model.setText("sys/tg_test/1/double_scalar#label") - p2.w_bg.setCurrentIndex(2) + p2.w_bg.setCurrentIndex(3) # bgRole='none' layout.addWidget(p1, 0, 0) layout.addWidget(p2, 0, 1) layout.addItem(Qt.QSpacerItem(10, 10), 1, 0, 1, 2) diff --git a/lib/taurus/qt/qtgui/display/tauruslabel.py b/lib/taurus/qt/qtgui/display/tauruslabel.py index 8e6e19a4f..7bf7af39c 100644 --- a/lib/taurus/qt/qtgui/display/tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/tauruslabel.py @@ -32,10 +32,8 @@ import operator import re -# shame of me for importing PyTango! -import PyTango - -from taurus.core.taurusbasetypes import TaurusElementType, TaurusEventType +from taurus.core.taurusbasetypes import (TaurusElementType, TaurusEventType, + AttrQuality, TaurusDevState) from taurus.external.qt import Qt from taurus.qt.qtgui.base import TaurusBaseWidget from taurus.qt.qtgui.base import TaurusBaseController @@ -100,7 +98,7 @@ def _updateForeground(self, label): self._trimmedText = self._shouldTrim(label, text) if self._trimmedText: text = "..." - label.setText(text) + label.setText_(text) def _shouldTrim(self, label, text): if not label.autoTrim: @@ -170,10 +168,10 @@ def w_value(self): return 0.0 def quality(self): - return PyTango.AttrQuality.ATTR_VALID + return AttrQuality.ATTR_VALID def state(self): - return PyTango.DevState.ON + return TaurusDevState.Ready def _updateToolTip(self, lcd): lcd.setToolTip("Some random value for design purposes only") @@ -231,6 +229,7 @@ class TaurusLabel(Qt.QLabel, TaurusBaseWidget): def __init__(self, parent=None, designMode=False): self._prefix = self.DefaultPrefix self._suffix = self.DefaultSuffix + self._permanentText = None self._bgRole = self.DefaultBgRole self._fgRole = self.DefaultFgRole self._modelIndex = self.DefaultModelIndex @@ -251,6 +250,11 @@ def __init__(self, parent=None, designMode=False): if self._designMode: self.controllerUpdate() + # register configurable properties + self.registerConfigProperty( + self.getPermanentText, self._setPermanentText, "permanentText" + ) + def _calculate_controller_class(self): ctrl_map = _CONTROLLER_MAP if self._designMode: @@ -357,7 +361,20 @@ def getBgRole(self): return self._bgRole def setBgRole(self, bgRole): - self._bgRole = str(bgRole).lower() + """ + Set the background role. The label background will be set according + to the current palette and the role. Valid roles are: + - 'none' : no background + - 'state' a color depending on the device state + - 'quality' a color depending on the attribute quality + - 'value' a color depending on the rvalue of the attribute + - a color based on the value of an arbitrary + member of the model object (warning: experimental feature!) + + .. warning:: the support is still experimental + and its API may change in future versions + """ + self._bgRole = str(bgRole) self.controllerUpdate() def resetBgRole(self): @@ -398,6 +415,22 @@ def setSuffixText(self, suffix): def resetSuffixText(self): self.setSuffixText(self.DefaultSuffix) + def getPermanentText(self): + return self._permanentText + + def _setPermanentText(self, text): + self._permanentText = text + if text is not None: + self.setText_(text) + + def setText_(self, text): + """Method to expose QLabel.setText""" + Qt.QLabel.setText(self, text) + + def setText(self, text): + """Reimplementation of setText to set permanentText""" + self._setPermanentText(text) + def setAutoTrim(self, trim): self._autoTrim = trim self.controllerUpdate() @@ -423,6 +456,25 @@ def getAutoTrim(self): def resetAutoTrim(self): self.setAutoTrim(self.DefaultAutoTrim) + def displayValue(self, v): + """Reimplementation of displayValue for TaurusLabel""" + if self._permanentText is None: + value = TaurusBaseWidget.displayValue(self, v) + else: + value = self._permanentText + + attr = self.getModelObj() + dev = attr.getParent() + + try: + v = value.format(dev=dev, attr=attr) + except Exception as e: + self.warning( + "Error formatting display (%r). Reverting to raw string", e) + v = value + + return v + @classmethod def getQtDesignerPluginInfo(cls): d = TaurusBaseWidget.getQtDesignerPluginInfo() diff --git a/lib/taurus/qt/qtgui/display/tauruslcd.py b/lib/taurus/qt/qtgui/display/tauruslcd.py index 501eed174..ffc744211 100644 --- a/lib/taurus/qt/qtgui/display/tauruslcd.py +++ b/lib/taurus/qt/qtgui/display/tauruslcd.py @@ -31,10 +31,8 @@ import operator -# shame of me for importing PyTango! -import PyTango - -from taurus.core.taurusbasetypes import TaurusElementType, TaurusEventType +from taurus.core.taurusbasetypes import (TaurusElementType, TaurusEventType, + AttrQuality, TaurusDevState) from taurus.external.qt import Qt from taurus.qt.qtgui.base import TaurusBaseWidget from taurus.qt.qtgui.base import TaurusBaseController @@ -141,10 +139,10 @@ def w_value(self): return 0.0 def quality(self): - return PyTango.AttrQuality.ATTR_VALID + return AttrQuality.ATTR_VALID def state(self): - return PyTango.DevState.ON + return TaurusDevState.ON def _updateToolTip(self, lcd): lcd.setToolTip("Some random value for design purposes only") diff --git a/lib/taurus/qt/qtgui/display/taurusled.py b/lib/taurus/qt/qtgui/display/taurusled.py index f49183952..7ed559add 100644 --- a/lib/taurus/qt/qtgui/display/taurusled.py +++ b/lib/taurus/qt/qtgui/display/taurusled.py @@ -36,7 +36,6 @@ from taurus.external.qt import Qt from taurus.core import DataFormat, AttrQuality, DataType -from taurus.core.tango import DevState from taurus.qt.qtgui.base import TaurusBaseWidget from qled import QLed @@ -77,9 +76,9 @@ def value(self): fgRole = widget.fgRole value = None if fgRole == 'rvalue': - value = obj.rvalue + value = bool(obj.rvalue) elif fgRole == 'wvalue': - value = obj.wvalue + value = bool(obj.wvalue) elif fgRole == 'quality': return obj.quality @@ -148,29 +147,44 @@ def usePreferedColor(self, widget): # value. If representing the quality, use the quality map return widget.fgRole != 'quality' +try: + from taurus.core.tango import DevState # TODO: Tango-centric + class _TaurusLedControllerState(_TaurusLedController): + + # key status, color, inTrouble + LedMap = {DevState.ON: (True, "green", False), + DevState.OFF: (False, "black", False), + DevState.CLOSE: (True, "white", False), + DevState.OPEN: (True, "green", False), + DevState.INSERT: (True, "green", False), + DevState.EXTRACT: (True, "green", False), + DevState.MOVING: (True, "blue", False), + DevState.STANDBY: (True, "yellow", False), + DevState.FAULT: (True, "red", False), + DevState.INIT: (True, "yellow", False), + DevState.RUNNING: (True, "blue", False), + DevState.ALARM: (True, "orange", False), + DevState.DISABLE: (True, "magenta", False), + DevState.UNKNOWN: (False, "black", False), + None: (False, "black", True)} + + def value(self): + widget, obj = self.widget(), self.modelObj() + fgRole = widget.fgRole + value = None + if fgRole == 'rvalue': + value = obj.rvalue + elif fgRole == 'wvalue': + value = obj.wvalue + elif fgRole == 'quality': + value = obj.quality + return value -class _TaurusLedControllerState(_TaurusLedController): - - # key status, color, inTrouble - LedMap = {DevState.ON: (True, "green", False), - DevState.OFF: (False, "black", False), - DevState.CLOSE: (True, "white", False), - DevState.OPEN: (True, "green", False), - DevState.INSERT: (True, "green", False), - DevState.EXTRACT: (True, "green", False), - DevState.MOVING: (True, "blue", False), - DevState.STANDBY: (True, "yellow", False), - DevState.FAULT: (True, "red", False), - DevState.INIT: (True, "yellow", False), - DevState.RUNNING: (True, "blue", False), - DevState.ALARM: (True, "orange", False), - DevState.DISABLE: (True, "magenta", False), - DevState.UNKNOWN: (False, "black", False), - None: (False, "black", True)} - - def usePreferedColor(self, widget): - # never use prefered widget color. Use always the map - return False + def usePreferedColor(self, widget): + # never use prefered widget color. Use always the map + return False +except: + pass class _TaurusLedControllerDesignMode(_TaurusLedController): @@ -226,7 +240,7 @@ def _calculate_controller_class(self): elif model.isBoolean(): klass = _TaurusLedControllerBool elif model.type == DataType.DevState: - klass = _TaurusLedControllerState + klass = _TaurusLedControllerState # TODO: tango-centric return klass def controller(self): diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index 317ff3638..21e8d60b4 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -100,6 +100,10 @@ def test_relativemodelclass(self): @testOldFgroles(fgRole='quality', expected='ATTR_VALID') @testOldFgroles(fgRole='none', expected='') # ------------------------------------------------------------------------------ +@insertTest(helper_name='text', + model='tango:' + DEV_NAME + '/double_spectrum', + modelIndex=1, + expected='1.23 mm') @insertTest(helper_name='text', model='tango:' + DEV_NAME + '/double_scalar#state', expected='Ready') @@ -138,11 +142,14 @@ class TaurusLabelTest2(TangoSchemeTestLauncher, BaseWidgetTestCase, ''' _klass = TaurusLabel - def text(self, model=None, expected=None, fgRole=None, maxdepr=0): - '''Check that the label text''' + def text(self, model=None, expected=None, fgRole=None, maxdepr=0, + modelIndex=None): + """Check that the label text""" self._widget.setModel(model) if fgRole is not None: self._widget.setFgRole(fgRole) + if modelIndex is not None: + self._widget.setModelIndex(modelIndex) self.processEvents(repetitions=10, sleep=.1) got = str(self._widget.text()) msg = ('wrong text for "%s":\n expected: %s\n got: %s' % @@ -178,36 +185,79 @@ def baseFormatter1(dtype, **kwargs): def baseFormatter2(dtype, **kwargs): return dtype.__name__ - -@insertTest(helper_name='checkFormat', +# ------------------------------------------------------------------------- +# Class formatter tests +@insertTest(helper_name='checkClassFormat', + model='eval:1.2345', + formatter='>>{}<<', + expected=">>1.2345<<") +@insertTest(helper_name='checkClassFormat', + model='eval:Q(5)#rvalue.magnitude', + formatter=baseFormatter2, + expected="int") +@insertTest(helper_name='checkClassFormat', + model='eval:Q("5m")#rvalue.units', + formatter=baseFormatter2, + expected="Unit") +@insertTest(helper_name='checkClassFormat', + model='eval:1.2345', + formatter=baseFormatter1, + expected="1.2") +@insertTest(helper_name='checkClassFormat', + model='eval:"hello"', + formatter=baseFormatter1, + expected="hello") +@insertTest(helper_name='checkClassFormat', + model='eval:"hello"', + formatter=baseFormatter2, + expected="str") +@insertTest(helper_name='checkClassFormat', + model='eval:"hello"', + formatter=None, + expected="hello") +@insertTest(helper_name='checkClassFormat', + model='eval:1.2345', + formatter='{:~.3f}', + expected="1.234") +@insertTest(helper_name='checkClassFormat', + model='eval:1.2345', + formatter='{:.3f}', + expected="1.234 dimensionless") +# ------------------------------------------------------------------------- +# Instance formatter tests +@insertTest(helper_name='checkInstanceFormat', + model='eval:1.2345', + formatter='>>{}<<', + expected=">>1.2345<<") +@insertTest(helper_name='checkInstanceFormat', model='eval:Q(5)#rvalue.magnitude', formatter=baseFormatter2, expected="int") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:Q("5m")#rvalue.units', formatter=baseFormatter2, expected="Unit") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:1.2345', formatter=baseFormatter1, expected="1.2") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:"hello"', formatter=baseFormatter1, expected="hello") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:"hello"', formatter=baseFormatter2, expected="str") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:"hello"', formatter=None, expected="hello") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:1.2345', formatter='{:~.3f}', expected="1.234") -@insertTest(helper_name='checkFormat', +@insertTest(helper_name='checkInstanceFormat', model='eval:1.2345', formatter='{:.3f}', expected="1.234 dimensionless") @@ -219,11 +269,18 @@ class TaurusLabelFormatTest(BaseWidgetTestCase, unittest.TestCase): _klass = TaurusLabel - def checkFormat(self, model, formatter, expected): + def setUp(self): + BaseWidgetTestCase.setUp(self) + # store original class format + self._origFormatter = self._klass.FORMAT + + def tearDown(self): + # restore original class format + self._klass.FORMAT = self._origFormatter + + def checkInstanceFormat(self, model, formatter, expected): if formatter is not None: self._widget.setFormat(formatter) - # self._widget.FORMAT = formatter - # self._widget.updateFormat() self._widget.setModel(model) self.processEvents(repetitions=50, sleep=.1) @@ -232,30 +289,18 @@ def checkFormat(self, model, formatter, expected): (model, expected, got)) self.assertEqual(got, expected, msg) + def checkClassFormat(self, model, formatter, expected): + self._klass.FORMAT = formatter + # self._widget was already created by BaseWidgetTestCase.setUp(self) + # but we need to re-create it to use the class formatter + self._widget = self._klass(*self.initargs, **self.initkwargs) + self._widget.setModel(model) + self.processEvents(repetitions=50, sleep=.1) - -@insertTest(helper_name='checkFormat', - model='eval:1.2345', - formatter='{:.3f}', - expected="1.234 dimensionless") -@insertTest(helper_name='checkFormat', - model='eval:1.2345', - formatter=None, - expected="1.2") -class TaurusLabelFormatClassTest(TaurusLabelFormatTest): - """ - Specific tests for testing the Formatting API with TaurusLabel - """ - - _klass = TaurusLabel - - def setUp(self): - TaurusLabelFormatTest.setUp(self) - self._defaultFormatter = TaurusLabel.FORMAT - TaurusLabel.FORMAT = baseFormatter1 - - def tearDown(self): - TaurusLabel.FORMAT = self._defaultFormatter + got = self._widget.text() + msg = ('wrong text for "%s":\n expected: %s\n got: %s' % + (model, expected, got)) + self.assertEqual(got, expected, msg) # diff --git a/lib/taurus/qt/qtgui/editor/__init__.py b/lib/taurus/qt/qtgui/editor/__init__.py index 16cdc8e33..3d8d9ffc7 100644 --- a/lib/taurus/qt/qtgui/editor/__init__.py +++ b/lib/taurus/qt/qtgui/editor/__init__.py @@ -27,10 +27,10 @@ __docformat__ = 'restructuredtext' -from .tauruseditor import * +try: + from .tauruseditor import * +except Exception as e: + from taurus import warning, debug + warning('Problem with taurus.qt.editor (hint: is spyder >=3 installed?)') + debug('%r', e) -# try: -# from .tauruseditor import * -# except: -# from taurus.qt.qtgui.display import create_fallback as __create -# TaurusBaseEditor = __create("TaurusBaseEditor") diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index f24ac7d16..af5649057 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -279,7 +279,7 @@ def resetMaxDataBufferSize(self): def getQtDesignerPluginInfo(cls): """reimplemented from :class:`TaurusBaseWidget`""" ret = TaurusBaseWidget.getQtDesignerPluginInfo() - ret['module'] = 'taurus.qt.qtgui.plot' + ret['module'] = 'taurus.qt.qtgui.extra_guiqwt' ret['group'] = 'Taurus Display' ret['icon'] = 'designer:qwtplot.png' return ret diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py index 27239959d..7373eac44 100755 --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -26,6 +26,8 @@ taurusgraphic.py: """ +# TODO: Tango-centric + __all__ = ['SynopticSelectionStyle', 'parseTangoUri', 'QEmitter', # TODO: QEmitter should probably be removed (kept priv) @@ -69,10 +71,6 @@ from taurus.core.taurusdevice import TaurusDevice from taurus.core.taurusattribute import TaurusAttribute from taurus.core.util.enumeration import Enumeration -# TODO: tango-centric! -from taurus.core.tango import DevState -from taurus.core.tango.tangovalidator import (TangoDeviceNameValidator, - TangoAttributeNameValidator) from taurus.external.qt import Qt from taurus.qt.qtgui.base import TaurusBaseComponent from taurus.qt.qtgui.util import (QT_ATTRIBUTE_QUALITY_PALETTE, QT_DEVICE_STATE_PALETTE, @@ -88,7 +86,11 @@ def parseTangoUri(name): + # TODO: Tango-centric from taurus.core import tango + from taurus.core.tango.tangovalidator import (TangoDeviceNameValidator, + TangoAttributeNameValidator) + validator = {tango.TangoDevice: TangoDeviceNameValidator, tango.TangoAttribute: TangoAttributeNameValidator} try: @@ -348,6 +350,11 @@ def getItemByName(self, item_name, strict=None): :return: (list) items """ + + # TODO: Tango-centric + from taurus.core.tango.tangovalidator import ( + TangoDeviceNameValidator, TangoAttributeNameValidator) + strict = ( not self.ANY_ATTRIBUTE_SELECTS_DEVICE) if strict is None else strict alnum = '(?:[a-zA-Z0-9-_\*]|(?:\.\*))(?:[a-zA-Z0-9-_\*]|(?:\.\*))*' @@ -1171,6 +1178,9 @@ def __init__(self, name=None, parent=None): self.call__init__(TaurusGraphicsItem, name, parent) def updateStyle(self): + + from taurus.core.tango import DevState # Tango-centric + v = self.getModelValueObj() self._currBrush = Qt.QBrush(Qt.Qt.NoBrush) diff --git a/lib/taurus/qt/qtgui/input/tauruscombobox.py b/lib/taurus/qt/qtgui/input/tauruscombobox.py index f74216515..9baebcbca 100644 --- a/lib/taurus/qt/qtgui/input/tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/tauruscombobox.py @@ -70,14 +70,12 @@ def preAttach(self): '''reimplemented from :class:`TaurusBaseWritableWidget`''' TaurusBaseWritableWidget.preAttach(self) self.currentIndexChanged.connect(self.writeIndexValue) - self.applied.connect(self.writeValue) def postDetach(self): '''reimplemented from :class:`TaurusBaseWritableWidget`''' TaurusBaseWritableWidget.postDetach(self) try: self.currentIndexChanged.disconnect(self.writeIndexValue) - self.applied.disconnect(self.writeValue) except TypeError: # In new style-signal if a signal is disconnected without # previously was connected it, it raises a TypeError @@ -124,7 +122,8 @@ def setValue(self, value): def updateStyle(self): '''reimplemented from :class:`TaurusBaseWritableWidget`''' if self.hasPendingOperations(): - self.setStyleSheet('TaurusValueComboBox {color: blue; }') + self.setStyleSheet( + 'TaurusValueComboBox {color: blue; font-weight: bold;}') else: self.setStyleSheet('TaurusValueComboBox {}') super(TaurusValueComboBox, self).updateStyle() @@ -135,16 +134,19 @@ def updateStyle(self): @Qt.pyqtSlot(int, name='currentIndexChanged') def writeIndexValue(self, index): - '''slot called to emit a valueChanged signal when the currentIndex is changed''' + """slot called to emit a valueChanged signal when the currentIndex is + changed (and trigger a write if AutoApply is enabled) + """ self.emitValueChanged() if self.getAutoApply(): - self.applied.emit() + self.writeValue() def keyPressEvent(self, event): - '''reimplemented to emit an 'applied()' signal when Enter (or Return) - key is pressed''' + """reimplemented to trigger a write when Enter (or Return) key is + pressed + """ if event.key() in [Qt.Qt.Key_Return, Qt.Qt.Key_Enter]: - self.applied.emit() + self.writeValue() event.accept() else: return Qt.QComboBox.keyPressEvent(self, event) @@ -379,7 +381,7 @@ def _taurusValueComboboxTest(): from taurus.qt.qtgui.application import TaurusApplication """tests TaurusValueCombobox """ # model = sys.argv[1] - model = 'sys/tg_test/1/short_scalar' + names = [ ('name0', 0), ('name1', 1), @@ -387,15 +389,22 @@ def _taurusValueComboboxTest(): ('name3', 3) ] a = TaurusApplication() - w = TaurusValueComboBox() - w.setModel(model) - w.addValueNames(names) - #w.autoApply = True + w = Qt.QWidget() + w.setLayout(Qt.QVBoxLayout()) + + cs = [] + for model in ['sys/tg_test/1/short_scalar'] * 2: + c = TaurusValueComboBox() + c.setModel(model) + c.addValueNames(names) + w.layout().addWidget(c) + cs.append(c) + # c.autoApply = True w.show() return a.exec_() if __name__ == '__main__': import sys - # main = _taurusValueComboboxTest #uncomment to test TaurusValueCombobox - main = _taurusAttrListTest # uncomment to testtaurusAttrList + main = _taurusValueComboboxTest #uncomment to test TaurusValueCombobox + # main = _taurusAttrListTest # uncomment to testtaurusAttrList sys.exit(main()) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index 7552e40ea..6ce50d2ca 100755 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -23,11 +23,9 @@ ## ############################################################################# -"""This module provides a set of basic taurus widgets based on QLineEdit""" - -__all__ = ["TaurusValueLineEdit"] - -__docformat__ = 'restructuredtext' +""" +This module provides a set of basic taurus widgets based on QLineEdit +""" import sys import numpy @@ -37,6 +35,10 @@ from taurus.qt.qtgui.util import PintValidator from taurus.core import DataType, DataFormat, TaurusEventType +__all__ = ["TaurusValueLineEdit"] + +__docformat__ = 'restructuredtext' + class TaurusValueLineEdit(Qt.QLineEdit, TaurusBaseWritableWidget): @@ -46,6 +48,7 @@ def __init__(self, qt_parent=None, designMode=False): self.call__init__(TaurusBaseWritableWidget, name, designMode=designMode) self._enableWheelEvent = False + self._last_value = None self.setAlignment(Qt.Qt.AlignRight) self.setValidator(None) @@ -56,7 +59,7 @@ def __init__(self, qt_parent=None, designMode=False): self.editingFinished.connect(self._onEditingFinished) def _updateValidator(self, value): - '''This method sets a validator depending on the data type''' + """This method sets a validator depending on the data type""" if isinstance(value.wvalue, Quantity): val = self.validator() if not isinstance(val, PintValidator): @@ -72,14 +75,15 @@ def _updateValidator(self, value): if units != val.units: val.setUnits(units) - # @TODO Other validators can be configured for other types (e.g. with string lengths, tango names,...) + # @TODO Other validators can be configured for other types + # (e.g. with string lengths, tango names,...) else: self.setValidator(None) self.debug("Validator disabled") def __decimalDigits(self, fmt): - '''returns the number of decimal digits from a format string - (or None if they are not defined)''' + """returns the number of decimal digits from a format string + (or None if they are not defined)""" try: if fmt[-1].lower() in ['f', 'g'] and '.' in fmt: return int(fmt[:-1].split('.')[-1]) @@ -89,23 +93,39 @@ def __decimalDigits(self, fmt): return None def _onEditingFinished(self): - '''slot for performing autoapply only when edition is finished''' + """slot for performing autoapply only when edition is finished""" if self._autoApply: self.writeValue() - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # TaurusBaseWritableWidget overwriting - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ def notifyValueChanged(self, *args): - '''reimplement to avoid autoapply on every partial edition''' + """reimplement to avoid autoapply on every partial edition""" self.emitValueChanged() def handleEvent(self, evt_src, evt_type, evt_value): + + # handle the case in which the line edit is not yet initialized + if self._last_value is None: + try: + value = self.getModelObj().read(cache=True) + self._updateValidator(value) + self.setValue(value.wvalue) + except Exception as e: + self.info('Failed attempt to initialize value: %r', e) + + self.setEnabled(evt_type != TaurusEventType.Error) + if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic): self._updateValidator(evt_value) + TaurusBaseWritableWidget.handleEvent( self, evt_src, evt_type, evt_value) + if evt_type == TaurusEventType.Error: + self.updateStyle() + def isTextValid(self): """ Validates current text @@ -119,11 +139,12 @@ def isTextValid(self): return val.validate(str(self.text()), 0)[0] == val.Acceptable def updateStyle(self): + """Reimplemented from :class:`TaurusBaseWritableWidget`""" TaurusBaseWritableWidget.updateStyle(self) value = self.getValue() - - if value is None or not self.isTextValid(): + + if value is None or not self.isTextValid() or not self.isEnabled(): # invalid value color, weight = 'gray', 'normal' else: @@ -134,17 +155,19 @@ def updateStyle(self): color, weight = 'black', 'normal' # also check alarms (if applicable) modelObj = self.getModelObj() - if modelObj and modelObj.type in [DataType.Integer, DataType.Float]: + if modelObj and modelObj.type in [DataType.Integer, + DataType.Float]: min_, max_ = modelObj.alarms if ((min_ is not None and value < min_) or (max_ is not None and value > max_)): color = 'orange' # apply style - style = 'TaurusValueLineEdit {color: %s; font-weight: %s}' %\ - (color, weight) + style = ('TaurusValueLineEdit {color: %s; font-weight: %s}' % + (color, weight)) self.setStyleSheet(style) def wheelEvent(self, evt): + """Wheel event handler""" if not self.getEnableWheelEvent() or Qt.QLineEdit.isReadOnly(self): return Qt.QLineEdit.wheelEvent(self, evt) model = self.getModelObj() @@ -162,6 +185,7 @@ def wheelEvent(self, evt): self._stepBy(numSteps) def keyPressEvent(self, evt): + """Key press event handler""" if evt.key() in (Qt.Qt.Key_Return, Qt.Qt.Key_Enter): Qt.QLineEdit.keyPressEvent(self, evt) evt.accept() @@ -192,13 +216,17 @@ def _stepBy(self, v): self.setValue(value + Quantity(v, value.units)) def setValue(self, v): - model = self.getModelObj() - if model is None: - v_str = str(v) - else: - v_str = str(self.displayValue(v)) - v_str = v_str.strip() - self.setText(v_str) + """Set the displayed text from a given value object""" + # Support displaying the value without units (enabled by fragment) + # Other fragments are ignored by setValue + if self.modelFragmentName == "wvalue.magnitude": + try: + units = self.validator().units + v = v.to(units).magnitude + except Exception as e: + self.debug('Cannot enforce fragment. Reason: %r', e) + self._last_value = v + self.setText(str(self.displayValue(v)).strip()) def getValue(self): text = self.text() @@ -211,6 +239,10 @@ def getValue(self): model_format = model_obj.data_format if model_type in [DataType.Integer, DataType.Float]: try: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # workaround for https://github.com/hgrecco/pint/issues/614 + text = text.lstrip('0') or '0' + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ q = Quantity(text) # allow implicit units (assume wvalue.units implicitly) if q.unitless: @@ -231,9 +263,13 @@ def getValue(self): elif model_type == DataType.Bytes: return bytes(text) else: - raise TypeError('Unsupported model type "%s"', model_type) - except Exception, e: - self.warning('Cannot return value for "%s". Reason: %r', text, e) + raise TypeError('Unsupported model type "%s"' % model_type) + except Exception as e: + msg = 'Cannot return value for "%s". Reason: %r' + if text in (str(None), self.getNoneValue()): + self.debug(msg, text, e) + else: + self.warning(msg, text, e) return None def setEnableWheelEvent(self, b): @@ -252,23 +288,25 @@ def getQtDesignerPluginInfo(cls): ret['icon'] = "designer:lineedit.png" return ret - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # QT properties - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ model = Qt.pyqtProperty("QString", TaurusBaseWritableWidget.getModel, TaurusBaseWritableWidget.setModel, TaurusBaseWritableWidget.resetModel) - useParentModel = Qt.pyqtProperty("bool", TaurusBaseWritableWidget.getUseParentModel, - TaurusBaseWritableWidget.setUseParentModel, - TaurusBaseWritableWidget.resetUseParentModel) + useParentModel = Qt.pyqtProperty( + "bool", TaurusBaseWritableWidget.getUseParentModel, + TaurusBaseWritableWidget.setUseParentModel, + TaurusBaseWritableWidget.resetUseParentModel) autoApply = Qt.pyqtProperty("bool", TaurusBaseWritableWidget.getAutoApply, TaurusBaseWritableWidget.setAutoApply, TaurusBaseWritableWidget.resetAutoApply) - forcedApply = Qt.pyqtProperty("bool", TaurusBaseWritableWidget.getForcedApply, + forcedApply = Qt.pyqtProperty("bool", + TaurusBaseWritableWidget.getForcedApply, TaurusBaseWritableWidget.setForcedApply, TaurusBaseWritableWidget.resetForcedApply) @@ -279,21 +317,41 @@ def getQtDesignerPluginInfo(cls): def main(): import sys - from taurus.qt.qtgui.application import TaurusApplication + import taurus.qt.qtgui.application + Application = taurus.qt.qtgui.application.TaurusApplication + + app = Application.instance() + owns_app = app is None + + if owns_app: + import taurus.core.util.argparse + parser = taurus.core.util.argparse.get_taurus_parser() + parser.usage = "%prog [options] " + app = Application(sys.argv, cmd_line_parser=parser, + app_name="Taurus lineedit demo", app_version="1.0", + org_domain="Taurus", org_name="Tango community") - app = TaurusApplication() + args = app.get_command_line_args() form = Qt.QWidget() layout = Qt.QVBoxLayout() form.setLayout(layout) - for m in ('sys/tg_test/1/double_scalar', - 'sys/tg_test/1/double_scalar' - ): + if len(args) == 0: + models = ['sys/tg_test/1/double_scalar', 'sys/tg_test/1/double_scalar'] + else: + models = args + for model in models: w = TaurusValueLineEdit() - w.setModel(m) + w.setModel(model) layout.addWidget(w) + form.resize(300, 50) form.show() - sys.exit(app.exec_()) + + if owns_app: + sys.exit(app.exec_()) + else: + return form + if __name__ == "__main__": sys.exit(main()) diff --git a/lib/taurus/qt/qtgui/input/taurusspinbox.py b/lib/taurus/qt/qtgui/input/taurusspinbox.py index 82ee026e6..6623b5557 100644 --- a/lib/taurus/qt/qtgui/input/taurusspinbox.py +++ b/lib/taurus/qt/qtgui/input/taurusspinbox.py @@ -23,17 +23,20 @@ ## ############################################################################# -"""This module provides a set of basic taurus widgets based on QAbstractSpinBox""" - -__all__ = ["TaurusValueSpinBox", "TaurusValueSpinBoxEx"] - -__docformat__ = 'restructuredtext' +""" +This module provides a set of basic taurus widgets based on QAbstractSpinBox +""" from taurus.external.qt import Qt from tauruslineedit import TaurusValueLineEdit from taurus.qt.qtgui.icon import getStandardIcon from taurus.external.pint import Quantity +from taurus.qt.qtgui.util import PintValidator + +__all__ = ["TaurusValueSpinBox", "TaurusValueSpinBoxEx"] + +__docformat__ = 'restructuredtext' class TaurusValueSpinBox(Qt.QAbstractSpinBox): @@ -49,6 +52,7 @@ def __init__(self, qt_parent=None, designMode=False): self._singleStep = 1.0 lineEdit = TaurusValueLineEdit(designMode=designMode) + lineEdit.setValidator(PintValidator(self)) self.setLineEdit(lineEdit) self.setAccelerated(True) @@ -64,9 +68,9 @@ def getValue(self): def keyPressEvent(self, evt): return self.lineEdit().keyPressEvent(evt) - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # Mandatory overload from QAbstractSpinBox - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ def stepBy(self, steps): self.setValue(self.getValue() + self._getSingleStepQuantity() * steps) @@ -99,9 +103,9 @@ def stepEnabled(self): ret |= Qt.QAbstractSpinBox.StepDownEnabled return ret - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # Overload from QAbstractSpinBox - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ def validate(self, input, pos): """Overloaded to use the current validator from the TaurusValueLineEdit, @@ -113,9 +117,9 @@ def validate(self, input, pos): return Qt.QAbstractSpinBox.validate(self, input, pos) return val.validate(input, pos) - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # Model related methods - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ def setModel(self, model): self.lineEdit().setModel(model) @@ -195,6 +199,7 @@ def getQtDesignerPluginInfo(cls): forcedApply = Qt.pyqtProperty("bool", getForcedApply, setForcedApply, resetForcedApply) + _S = """ QSpinBox::up-button { border-width: 0px; @@ -247,14 +252,54 @@ def __setattr__(self, name, value): setattr(self.spinBox, name, value) -if __name__ == "__main__": +def main(): import sys - from taurus.qt.qtgui.application import TaurusApplication - app = TaurusApplication() - - w = TaurusValueSpinBox() - w.setModel('sys/tg_test/1/double_scalar') - w.resize(300, 50) + import taurus.qt.qtgui.application + Application = taurus.qt.qtgui.application.TaurusApplication + + app = Application.instance() + owns_app = app is None + + if owns_app: + import taurus.core.util.argparse + parser = taurus.core.util.argparse.get_taurus_parser() + parser.usage = "%prog [options] " + app = Application(sys.argv, cmd_line_parser=parser, + app_name="Taurus spinbox demo", app_version="1.0", + org_domain="Taurus", org_name="Tango community") + + args = app.get_command_line_args() + + if len(args) == 0: + w = TaurusValueSpinBox() + w.setModel('sys/tg_test/1/double_scalar') + w.resize(300, 50) + else: + w = Qt.QWidget() + layout = Qt.QGridLayout() + w.setLayout(layout) + for model in args: + label = TaurusValueSpinBox() + label.setModel(model) + layout.addWidget(label) + w.resize(300, 50) w.show() - sys.exit(app.exec_()) \ No newline at end of file + if owns_app: + sys.exit(app.exec_()) + else: + return w + + +if __name__ == "__main__": + main() +# import sys +# from taurus.qt.qtgui.application import TaurusApplication +# app = TaurusApplication() + +# w = TaurusValueSpinBox() +# w.setModel('sys/tg_test/1/double_scalar') +# w.resize(300, 50) +# w.show() + +# sys.exit(app.exec_()) diff --git a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py index 34e2d5fd4..bd19f7b76 100644 --- a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py +++ b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py @@ -49,8 +49,6 @@ from taurus.qt.qtgui.util.ui import UILoadable from taurus.qt.qtgui.icon import getCachedPixmap -from taurus.core.tango.tangodatabase import TangoDevInfo # @todo: Tango-centric! - ############################################################################### # TaurusDevicePanel (from Vacca) @@ -219,11 +217,8 @@ def __init__(self, parent=None, model=None, palette=None, bound=True): self._stateframe.layout().addWidget(Qt.QLabel('State'), 0, 0, Qt.Qt.AlignCenter) self._statelabel = TaurusLabel(self._stateframe) self._statelabel.setMinimumWidth(100) - self._statelabel.setBgRole('state') + self._statelabel.setBgRole('value') self._stateframe.layout().addWidget(self._statelabel, 0, 1, Qt.Qt.AlignCenter) - self._state = TaurusLed(self._stateframe) - self._state.setShowQuality(False) - self._stateframe.layout().addWidget(self._state, 0, 2, Qt.Qt.AlignCenter) self._statusframe = Qt.QScrollArea(self) self._status = TaurusLabel(self._statusframe) @@ -344,7 +339,6 @@ def setModel(self, model, pixmap=None): qpixmap = getCachedPixmap(logo) self._image.setPixmap(qpixmap) - self._state.setModel(model + '/state') # TODO: Tango-centric if hasattr(self, '_statelabel'): self._statelabel.setModel( model + '/state') # TODO: Tango-centric @@ -413,7 +407,6 @@ def detach_recursive(obj): detach_recursive(self) try: self._label.setText('') - self._state.setModel('') if hasattr(self, '_statelabel'): self._statelabel.setModel('') self._status.setModel('') @@ -490,6 +483,9 @@ def get_comms_form(self, device, form=None, parent=None): def filterNonExported(obj): + # TODO: Tango-centric + from taurus.core.tango.tangodatabase import TangoDevInfo + if not isinstance(obj, TangoDevInfo) or obj.exported(): return obj return None @@ -566,6 +562,8 @@ def setTangoHost(self, host): def onItemSelectionChanged(self, current, previous): itemData = current.itemData() + + from taurus.core.tango.tangodatabase import TangoDevInfo if isinstance(itemData, TangoDevInfo): # TODO: Tango-centric self.onDeviceSelected(itemData) diff --git a/lib/taurus/qt/qtgui/panel/taurusfilterpanel.py b/lib/taurus/qt/qtgui/panel/taurusfilterpanel.py deleted file mode 100644 index d792b9b96..000000000 --- a/lib/taurus/qt/qtgui/panel/taurusfilterpanel.py +++ /dev/null @@ -1,760 +0,0 @@ -#!/usr/bin/env python - -############################################################################# -## -# This file is part of Taurus -## -# http://taurus-scada.org -## -# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain -## -# Taurus is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -## -# Taurus is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -## -# You should have received a copy of the GNU Lesser General Public License -# along with Taurus. If not, see . -## -############################################################################# - -"""This module provides widgets that display the authority in a tree format""" - -__docformat__ = 'restructuredtext' - -import os -import re - -from taurus.external.qt import Qt - -from taurus.core.taurusauthority import TaurusAuthority -from taurus.core.taurusbasetypes import TaurusElementType as ElemType -import taurus.qt.qtgui.base - -from taurus.core.tango.tangodatabase import (TangoAttrInfo, TangoDevInfo, - TangoServInfo) - -from taurus.qt.qtgui.icon import getElementTypeIcon - - - - -class BaseFilter(object): - - def __init__(self, re_expr): - if len(re_expr) == 0: - re_expr = ".*" - else: - if re_expr[0] != "^": - re_expr = "^" + re_expr - if re_expr[-1] != "$": - re_expr += "$" - self._re_expr = re.compile(re_expr, re.IGNORECASE) - - def __call__(self, obj): - return self.filter(obj) - - -class BaseElementFilter(BaseFilter): - - def __init__(self, re_expr, func=None): - super(BaseElementFilter, self).__init__(re_expr) - self._klass = func.im_class - self._func = func - - def filter(self, obj): - if not isinstance(obj, self._klass): - return obj - v = self._func(obj) - if self._re_expr.match(v): - return obj - - -class DeviceFilter(BaseElementFilter): - - def __init__(self, re_expr, func=TangoDevInfo.name): - super(DeviceFilter, self).__init__(re_expr, func=func) - - -class DeviceClassFilter(BaseElementFilter): - - def __init__(self, re_expr, func=TangoDevInfo.name): - super(DeviceClassFilter, self).__init__(re_expr, func=func) - - -class ServerFilter(BaseElementFilter): - - def __init__(self, re_expr, func=TangoServInfo.name): - super(ServerFilter, self).__init__(re_expr, func=func) - - -class AttributeFilter(BaseElementFilter): - - def __init__(self, re_expr, func=TangoAttrInfo.name): - super(AttributeFilter, self).__init__(re_expr, func=func) - - -class KlassFilter(BaseFilter): - - def __init__(self, klass): - # don't call super on purpose. We don't need/have a regular expression here! - #super(KlassFilter, self).__init__(re_expr) - self._klass = klass - - def filter(self, obj): - if isinstance(obj, self._klass): - return obj - - -def getFilter(type, re_expr=None): - if re_expr is None: - if type == ElemType.Device: - return KlassFilter(TangoDevInfo) - elif type == ElemType.Server: - return KlassFilter(TangoServInfo) - elif type == ElemType.DeviceClass: - return KlassFilter(TangoDevInfo) - return None - - if type == ElemType.Device: - return DeviceFilter(re_expr) - elif type == ElemType.Domain: - return DeviceFilter(re_expr, TangoDevInfo.domain) - elif type == ElemType.Family: - return DeviceFilter(re_expr, TangoDevInfo.family) - elif type == ElemType.Member: - return DeviceFilter(re_expr, TangoDevInfo.member) - elif type == ElemType.Server: - return ServerFilter(re_expr) - elif type == ElemType.ServerName: - return ServerFilter(re_expr, TangoServInfo.serverName) - elif type == ElemType.ServerInstance: - return ServerFilter(re_expr, TangoServInfo.serverInstance) - elif type == ElemType.DeviceClass: - return DeviceClassFilter(re_expr) - elif type == ElemType.Attribute: - return AttributeFilter(re_expr) - - -class TaurusFilterPanelOld1(Qt.QWidget, taurus.qt.qtgui.base.TaurusBaseWidget): - - def __init__(self, parent=None, designMode=False): - name = self.__class__.__name__ - self.call__init__wo_kw(Qt.QWidget, parent) - self.call__init__(taurus.qt.qtgui.base.TaurusBaseWidget, - name, designMode=designMode) - self.init() - - def init(self): - l = Qt.QBoxLayout(Qt.QBoxLayout.TopToBottom) - self.setLayout(l) - self.addFilterHeader() - self.insertFilterItem() - l.addStretch(1) - - def addFilterHeader(self): - label = Qt.QLabel("Type:") - comboBox = Qt.QComboBox() - comboBox.addItem(getElementTypeIcon(ElemType.Attribute), - "Attribute", ElemType.Attribute) - comboBox.addItem(getElementTypeIcon(ElemType.Device), - "Device", ElemType.Device) - comboBox.addItem(getElementTypeIcon(ElemType.DeviceClass), - "Device type", ElemType.DeviceClass) - comboBox.addItem(getElementTypeIcon(ElemType.Server), - "Server", ElemType.Server) - comboBox.addItem("Any") - previewButton = Qt.QPushButton("Preview") - previewButton.clicked.connect(self.onPreview) - field = Qt.QWidget() - l = Qt.QHBoxLayout() - field.setLayout(l) - l.addWidget(label) - l.addWidget(comboBox) - l.addWidget(previewButton) - l.addStretch(1) - self.layout().addWidget(field) - - def insertFilterItem(self, row=None): - - sl = self.layout() - - comboBox = Qt.QComboBox() - self._fillComboBox(comboBox) - comboBox.currentIndexChanged.connect(self.onFilterComboBoxItemSelected) - - edit = Qt.QLineEdit() - - addButton = Qt.QPushButton(Qt.QIcon(":/actions/list-add.svg"), "") - addButton.clicked.connect(self.onAddFilterButtonClicked) - - removeButton = Qt.QPushButton( - Qt.QIcon(":/actions/list-remove.svg"), "") - removeButton.clicked.connect(self.onRemoveFilterButtonClicked) - - field = Qt.QWidget() - l = Qt.QHBoxLayout() - field.setLayout(l) - l.addWidget(Qt.QLabel("Filter by")) - l.addWidget(comboBox) - l.addWidget(edit) - l.addWidget(addButton) - l.addWidget(removeButton) - - if row is None: - sl.addWidget(field) - else: - sl.insertWidget(row, field) - - def _fillComboBox(self, comboBox): - comboBox.addItem(getElementTypeIcon(ElemType.Attribute), - "Attribute", ElemType.Attribute) - comboBox.addItem(getElementTypeIcon(ElemType.Device), - "Device", ElemType.Device) - comboBox.addItem(getElementTypeIcon(ElemType.DeviceClass), - "Device type", ElemType.DeviceClass) - comboBox.addItem(getElementTypeIcon(ElemType.Domain), - "Domain", ElemType.Domain) - comboBox.addItem(getElementTypeIcon(ElemType.Family), - "Family", ElemType.Family) - comboBox.addItem(getElementTypeIcon(ElemType.Member), - "Member", ElemType.Member) - comboBox.addItem(getElementTypeIcon(ElemType.Server), - "Server", ElemType.Server) - comboBox.addItem(getElementTypeIcon(ElemType.ServerName), - "Server Name", ElemType.ServerName) - comboBox.addItem(getElementTypeIcon(ElemType.ServerInstance), - "Server Instance", ElemType.ServerInstance) - - def onFilterComboBoxItemSelected(self, index): - pass - - def onAddFilterButtonClicked(self): - button = self.sender() - if button is None: - return - field = button.parent() - index = self.layout().indexOf(field) - self.insertFilterItem(index + 1) - - def onRemoveFilterButtonClicked(self): - l = self.layout() - # there is a header row, at least one filter row and a stretch at the - # end, therefore, if there are only three rows, we don't allow to delete - # the only existing filter - if l.count() <= 3: - return - button = self.sender() - if button is None: - return - field = button.parent() - l.removeWidget(field) - field.setParent(None) - - def onPreview(self): - import trees - model = self.getModel() - dialog = Qt.QDialog() - dialog.setModal(True) - w = trees.TaurusTreeWidget(dialog, perspective=self.getHeaderType()) - w.setModel(model) - w.setFilters(self.calculate()) - dialog.exec_() - - def getHeaderType(self): - g_layout = self.layout() - header = g_layout.itemAt(0).widget() - headerCombo = header.layout().itemAt(1).widget() - return Qt.from_qvariant(headerCombo.itemData(headerCombo.currentIndex())) - - def calculate(self): - db = self.getModelObj() - if db is None: - return - - g_layout = self.layout() - - filters = [] - for i in xrange(1, g_layout.count() - 1): - field_layout = g_layout.itemAt(i).widget().layout() - comboBox = field_layout.itemAt(1).widget() - edit = field_layout.itemAt(2).widget() - - type = Qt.from_qvariant(comboBox.itemData(comboBox.currentIndex())) - expr = str(edit.text()) - f = getFilter(type, expr) - filters.append(f) - - finalType = self.getHeaderType() - filters.append(getFilter(finalType)) - return filters - - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - # TaurusBaseWidget overwriting - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - - def getModelClass(self): - return TaurusAuthority - - #: This property holds the unique URI string representing the model name - #: with which this widget will get its data from. The convention used for - #: the string can be found :ref:`here `. - #: - #: In case the property :attr:`useParentModel` is set to True, the model - #: text must start with a '/' followed by the attribute name. - #: - #: **Access functions:** - #: - #: * :meth:`TaurusBaseWidget.getModel` - #: * :meth:`TaurusBaseWidget.setModel` - #: * :meth:`TaurusBaseWidget.resetModel` - #: - #: .. seealso:: :ref:`model-concept` - model = Qt.pyqtProperty("QString", taurus.qt.qtgui.base.TaurusBaseWidget.getModel, - taurus.qt.qtgui.base.TaurusBaseWidget.setModel, - taurus.qt.qtgui.base.TaurusBaseWidget.resetModel) - - -class TaurusFilterPanelOld2(Qt.QWidget, taurus.qt.qtgui.base.TaurusBaseWidget): - - def __init__(self, parent=None, designMode=False): - name = self.__class__.__name__ - self.call__init__wo_kw(Qt.QWidget, parent) - self.call__init__(taurus.qt.qtgui.base.TaurusBaseWidget, - name, designMode=designMode) - self.init() - - def init(self): - l = Qt.QGridLayout() - l.setContentsMargins(0, 0, 0, 0) - self.setLayout(l) - - comboBox = Qt.QComboBox() - comboBox.addItem(getElementTypeIcon(ElemType.Attribute), - "Attribute", ElemType.Attribute) - comboBox.addItem(getElementTypeIcon(ElemType.Device), - "Device", ElemType.Device) - comboBox.addItem(getElementTypeIcon(ElemType.DeviceClass), - "Device type", ElemType.DeviceClass) - comboBox.addItem(getElementTypeIcon(ElemType.Server), - "Server", ElemType.Server) - - l.addWidget(Qt.QLabel("Filter for:"), 0, 0) - l.addWidget(comboBox, 0, 1) - - import trees - self._deviceEdit = Qt.QComboBox() - self._deviceEdit.setEditable(True) - self._deviceEdit.setMaxVisibleItems(10) - self._deviceEdit.setInsertPolicy(Qt.QComboBox.InsertAtTop) - self._deviceDomainEdit = Qt.QLineEdit() - self._deviceFamilyEdit = Qt.QLineEdit() - self._deviceMemberEdit = Qt.QLineEdit() - self._deviceClass = Qt.QLineEdit() - self._serverEdit = Qt.QLineEdit() - self._serverNameEdit = Qt.QLineEdit() - self._serverInstanceEdit = Qt.QLineEdit() - self._attributeEdit = Qt.QLineEdit() - - lbl = Qt.QLabel("Device type:") - l.addWidget(lbl, 1, 0) - l.setAlignment(lbl, Qt.Qt.AlignRight) - l.addWidget(self._deviceClass, 1, 1) - l.addWidget(Qt.QLabel("Device:"), 2, 0) - l.addWidget(self._deviceEdit, 2, 1) - l.addWidget(Qt.QLabel("Device domain:"), 3, 0) - l.addWidget(self._deviceDomainEdit, 3, 1) - l.addWidget(Qt.QLabel("Device family:"), 4, 0) - l.addWidget(self._deviceFamilyEdit, 4, 1) - l.addWidget(Qt.QLabel("Device member:"), 5, 0) - l.addWidget(self._deviceMemberEdit, 5, 1) - l.addWidget(Qt.QLabel("Server:"), 6, 0) - l.addWidget(self._serverEdit, 6, 1) - l.addWidget(Qt.QLabel("Server name:"), 7, 0) - l.addWidget(self._serverNameEdit, 7, 1) - l.addWidget(Qt.QLabel("Server instance:"), 8, 0) - l.addWidget(self._serverInstanceEdit, 8, 1) - l.addWidget(Qt.QLabel("Attribute:"), 9, 0) - l.addWidget(self._attributeEdit, 9, 1) - - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - # TaurusBaseWidget overwriting - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - - def getModelClass(self): - return TaurusAuthority - - def setModel(self, m): - taurus.qt.qtgui.base.TaurusBaseWidget.setModel(self, m) - db = self.getModelObj() - #model = self._deviceEdit.model() - # if model is None: return - # model.setDataSource(db) - self._deviceEdit.clear() - if db is not None: - deviceNames = db.cache().getDeviceNames() - deviceNames.sort() - #icon = taurus.core.icons.getElementTypeIcon(ElemType.Device) - self._deviceEdit.addItems(deviceNames) - - #: This property holds the unique URI string representing the model name - #: with which this widget will get its data from. The convention used for - #: the string can be found :ref:`here `. - #: - #: In case the property :attr:`useParentModel` is set to True, the model - #: text must start with a '/' followed by the attribute name. - #: - #: **Access functions:** - #: - #: * :meth:`TaurusBaseWidget.getModel` - #: * :meth:`TaurusBaseWidget.setModel` - #: * :meth:`TaurusBaseWidget.resetModel` - #: - #: .. seealso:: :ref:`model-concept` - model = Qt.pyqtProperty("QString", taurus.qt.qtgui.base.TaurusBaseWidget.getModel, - taurus.qt.qtgui.base.TaurusBaseWidget.setModel, - taurus.qt.qtgui.base.TaurusBaseWidget.resetModel) - - -class _MessageWidget(Qt.QWidget): - - def __init__(self, parent=None, pixmap=None): - Qt.QWidget.__init__(self, parent) - l = Qt.QHBoxLayout() - self.setLayout(l) - self._icon = Qt.QLabel() - if pixmap is None: - pixmap = Qt.QIcon.fromTheme("dialog-warning").pixmap(16, 16) - self._icon.setPixmap(pixmap) - self._label = Qt.QLabel() - l.addWidget(self._icon) - l.addWidget(self._label) - - def setText(self, text): - self._label.setText(text) - - -from taurus.external.qt.uic import loadUi - - -class TaurusFilterPanel(Qt.QWidget, taurus.qt.qtgui.base.TaurusBaseWidget): - - _Items = "server", "serverName", "serverInstance", \ - "deviceName", "deviceType", "deviceDomain", "deviceFamily", "deviceMember", \ - "attribute" - - def __init__(self, parent=None, designMode=False): - name = self.__class__.__name__ - self.call__init__wo_kw(Qt.QWidget, parent) - self.call__init__(taurus.qt.qtgui.base.TaurusBaseWidget, - name, designMode=designMode) - self.init() - - def init(self): - l = Qt.QVBoxLayout() - self.setLayout(l) - - panel = self._mainPanel = Qt.QWidget() - l.addWidget(panel, 1) - this_dir = os.path.dirname(os.path.abspath(__file__)) - ui_filename = os.path.join(this_dir, 'ui', 'TaurusFilterPanel.ui') - self.ui = ui = loadUi(ui_filename, baseinstance=panel) - - comboBox = ui.filterTypeCombo - comboBox.addItem(getElementTypeIcon(ElemType.Attribute), - "Attribute", ElemType.Attribute) - comboBox.addItem(getElementTypeIcon(ElemType.Device), - "Device", ElemType.Device) - comboBox.addItem(getElementTypeIcon(ElemType.DeviceClass), - "Device type", ElemType.DeviceClass) - comboBox.addItem(getElementTypeIcon(ElemType.Server), - "Server", ElemType.Server) - - clickedSig = Qt.SIGNAL("clicked()") - idxChangedSig = Qt.SIGNAL("currentIndexChanged(int)") - ui.serverNameCombo.currentIndexChanged.connect( - self._updateServerInstanceCombo) - ui.deviceDomainCombo.currentIndexChanged.connect( - self._updateDeviceFamilyCombo) - ui.deviceFamilyCombo.currentIndexChanged.connect( - self._updateDeviceMemberCombo) - - class clearSelection(object): - - def __init__(self, cb): - self._cb = cb - - def __call__(self): - self._cb.setCurrentIndex(-1) - - clear_icon = Qt.QIcon.fromTheme("edit-clear") - for combo, clearButton in zip(self.combos(), self.clearButtons()): - combo.currentIndexChanged.connect(self._updateStatusBar) - clearButton.clicked.connect(clearSelection(combo)) - clearButton.setIcon(clear_icon) - - sb = self._statusbar = Qt.QStatusBar() - sb.setSizeGripEnabled(False) - l.addWidget(sb) - sbWarningMsg = self._sbWarningMsg = _MessageWidget() - sbWarningMsg.setVisible(False) - sb.addWidget(sbWarningMsg) - - def combos(self): - if not hasattr(self, "_combos"): - f = self.ui - self._combos = [getattr(f, name + "Combo") for name in self._Items] - return self._combos - - def clearButtons(self): - if not hasattr(self, "_clearButtons"): - f = self.ui - self._clearButtons = [getattr(f, name + "ClearButton") - for name in self._Items] - return self._clearButtons - - def _db_cache(self): - db = self.getModelObj() - if db is None: - return - return db.cache() - - def _updateStatusBar(self, index=None): - form = self.ui - server = str(form.serverCombo.currentText()) - serverName = str(form.serverNameCombo.currentText()) - serverInstance = str(form.serverInstanceCombo.currentText()) - - msg = self._sbWarningMsg - msg.setVisible(False) - - if server and (serverName or serverInstance): - sb = self._statusbar - s = "Specifying name filter and type/instance filters at the same " \ - "time may result in an empty filter" - msg.setVisible(True) - msg.setText(s) - - deviceName = str(form.deviceNameCombo.currentText()) - deviceDomain = str(form.deviceDomainCombo.currentText()) - deviceFamily = str(form.deviceFamilyCombo.currentText()) - deviceMember = str(form.deviceMemberCombo.currentText()) - - if deviceName and (deviceDomain or deviceFamily or deviceMember): - sb = self._statusbar - s = "Specifying name filter and domain/family/member filters at the same " \ - "time may result in an empty filter" - msg.setVisible(True) - msg.setText(s) - - def _updateServerCombo(self, index=None): - combo = self.ui.serverCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - servers = db_cache.servers() - icon = getElementTypeIcon(ElemType.Server) - for serverName in sorted(servers): - serverInfo = servers[serverName] - combo.addItem(icon, serverName, serverInfo) - combo.setCurrentIndex(-1) - - def _updateServerNameCombo(self, index=None): - combo = self.ui.serverNameCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - servers = db_cache.servers() - serverNames = [] - for server in servers.values(): - name = server.serverName() - if name not in serverNames: - serverNames.append(name) - serverNames.sort() - icon = getElementTypeIcon(ElemType.ServerName) - for serverName in serverNames: - combo.addItem(icon, serverName) - combo.setCurrentIndex(-1) - - def _updateServerInstanceCombo(self, index=None): - combo = self.ui.serverInstanceCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - if index is None or index == -1: - return - serverName = str(self.sender().currentText()) - servers = db_cache.servers() - serverInstances = [] - for server in servers.values(): - if server.serverName() == serverName: - serverInstances.append(server.serverInstance()) - serverInstances.sort() - icon = getElementTypeIcon(ElemType.ServerInstance) - for serverInstance in serverInstances: - combo.addItem(icon, serverInstance) - combo.setCurrentIndex(-1) - - def _updateDeviceTypeCombo(self, index=None): - combo = self.ui.deviceTypeCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - deviceKlasses = db_cache.klasses() - icon = getElementTypeIcon(ElemType.DeviceClass) - for klassName in sorted(deviceKlasses): - klassInfo = deviceKlasses[klassName] - combo.addItem(icon, klassName, klassInfo) - combo.setCurrentIndex(-1) - - def _updateDeviceNameCombo(self, index=None): - combo = self.ui.deviceNameCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - devices = db_cache.devices() - icon = getElementTypeIcon(ElemType.Device) - for deviceName in sorted(devices): - deviceInfo = devices[deviceName] - combo.addItem(icon, deviceName, deviceInfo) - combo.setCurrentIndex(-1) - - def _updateDeviceDomainCombo(self, index=None): - combo = self.ui.deviceDomainCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - domains = db_cache.getDeviceDomainNames() - domains.sort() - icon = getElementTypeIcon(ElemType.Domain) - for domain in domains: - combo.addItem(icon, domain) - combo.setCurrentIndex(-1) - - def _updateDeviceFamilyCombo(self, index=None): - combo = self.ui.deviceFamilyCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - - deviceDomain = str(self.ui.deviceDomainCombo.currentText()) - if deviceDomain == "": - return - families = db_cache.getDeviceFamilyNames(deviceDomain) - families.sort() - icon = getElementTypeIcon(ElemType.Family) - for family in families: - combo.addItem(icon, family) - combo.setCurrentIndex(-1) - - def _updateDeviceMemberCombo(self, index=None): - combo = self.ui.deviceMemberCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - - deviceDomain = str(self.ui.deviceDomainCombo.currentText()) - if deviceDomain == "": - return - deviceFamily = str(self.ui.deviceFamilyCombo.currentText()) - if deviceFamily == "": - return - members = db_cache.getDeviceMemberNames(deviceDomain, deviceFamily) - members.sort() - icon = getElementTypeIcon(ElemType.Member) - for member in members: - combo.addItem(icon, member) - combo.setCurrentIndex(-1) - - def _updateAttributeCombo(self, index=None): - combo = self.ui.attributeCombo - combo.clear() - db_cache = self._db_cache() - if db_cache is None: - return - - def _fillItems(self): - self._updateServerCombo() - self._updateServerNameCombo() - self._updateServerInstanceCombo() - self._updateDeviceTypeCombo() - self._updateDeviceNameCombo() - self._updateDeviceDomainCombo() - self._updateDeviceFamilyCombo() - self._updateDeviceMemberCombo() - self._updateAttributeCombo() - - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - # TaurusBaseWidget overwriting - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - - def getModelClass(self): - return TaurusAuthority - - def setModel(self, m): - taurus.qt.qtgui.base.TaurusBaseWidget.setModel(self, m) - db = self.getModelObj() - #model = self._deviceEdit.model() - # if model is None: return - # model.setDataSource(db) - self.ui.deviceNameCombo.clear() - self._fillItems() - - #: This property holds the unique URI string representing the model name - #: with which this widget will get its data from. The convention used for - #: the string can be found :ref:`here `. - #: - #: In case the property :attr:`useParentModel` is set to True, the model - #: text must start with a '/' followed by the attribute name. - #: - #: **Access functions:** - #: - #: * :meth:`TaurusBaseWidget.getModel` - #: * :meth:`TaurusBaseWidget.setModel` - #: * :meth:`TaurusBaseWidget.resetModel` - #: - #: .. seealso:: :ref:`model-concept` - model = Qt.pyqtProperty("QString", taurus.qt.qtgui.base.TaurusBaseWidget.getModel, - taurus.qt.qtgui.base.TaurusBaseWidget.setModel, - taurus.qt.qtgui.base.TaurusBaseWidget.resetModel) - - -def main(): - from taurus.qt.qtgui.application import TaurusApplication - from taurus.core.util import argparse - import sys - - parser = argparse.get_taurus_parser() - parser.usage = "%prog [options] [hostname]" - - app = TaurusApplication(cmd_line_parser=parser) - args = app.get_command_line_args() - - if len(args) > 0: - host = args[0] - else: - host = taurus.Authority().getNormalName() - - w = TaurusFilterPanel() - w.setWindowIcon(Qt.QIcon(":/actions/system-shutdown.svg")) - w.setWindowTitle("A Taurus Filter Example") - w.setModel(host) - w.show() - - sys.exit(app.exec_()) - -if __name__ == "__main__": - main() diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index d5cb9ee3d..45cc4a641 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -32,10 +32,9 @@ from datetime import datetime from taurus.external.qt import Qt -import PyTango import taurus.core -from taurus.core import TaurusDevState +from taurus.core import TaurusDevState, DisplayLevel from taurus.qt.qtcore.mimetypes import (TAURUS_ATTR_MIME_TYPE, TAURUS_DEV_MIME_TYPE, TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_MODEL_MIME_TYPE) @@ -89,6 +88,8 @@ def __init__(self, parent=None, buttons=None, withButtons=True, designMode=False): + + self._children = [] TaurusWidget.__init__(self, parent, designMode) if buttons is None: @@ -96,7 +97,7 @@ def __init__(self, parent=None, Qt.QDialogButtonBox.Reset self._customWidgetMap = {} self._model = [] - self._children = [] + # self._children = [] self.setFormWidget(formWidget) self.setLayout(Qt.QVBoxLayout()) @@ -137,6 +138,10 @@ def __init__(self, parent=None, self.addAction(self.compactModeAction) self.compactModeAction.triggered[bool].connect(self.setCompact) + self.setFormatterAction = Qt.QAction('Set formatter (all items)', self) + self.addAction(self.setFormatterAction) + self.setFormatterAction.triggered[()].connect(self.onSetFormatter) + self.resetModifiableByUser() self.setSupportedMimeTypes([TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_DEV_MIME_TYPE, TAURUS_ATTR_MIME_TYPE, TAURUS_MODEL_MIME_TYPE, 'text/plain']) @@ -170,6 +175,7 @@ def setCustomWidgetMap(self, cwmap): type strings (i.e. see :class:`PyTango.DeviceInfo`) and whose values are tuples of classname,args,kwargs ''' + # TODO: tango-centric self._customWidgetMap = cwmap def getCustomWidgetMap(self): @@ -179,6 +185,7 @@ def getCustomWidgetMap(self): type strings (i.e. see :class:`PyTango.DeviceInfo`) and whose values are tuples of classname,args,kwargs ''' + # TODO: tango-centric return self._customWidgetMap @Qt.pyqtSlot('QString', name='modelChanged') @@ -362,6 +369,17 @@ def setWithButtons(self, trueFalse): def resetWithButtons(self): self.setWithButtons(True) + def onSetFormatter(self): + """Reimplemented from TaurusBaseWidget""" + # Form delegates se to the taurusvalues + format = TaurusWidget.onSetFormatter(self) + if format is not None: + for item in self.getItems(): + rw = item.readWidget() + if hasattr(rw, 'setFormat'): + rw.setFormat(format) + return format + def setCompact(self, compact): self._compact = compact for item in self.getItems(): @@ -546,6 +564,7 @@ def getQtDesignerPluginInfo(cls): class TaurusCommandsForm(TaurusWidget): '''A form that shows commands available for a Device Server''' + # TODO: tango-centric def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) @@ -574,7 +593,7 @@ def __init__(self, parent=None, designMode=False): self._defaultParameters = [] self._sortKey = lambda x: x.cmd_name - self._operatorViewFilter = lambda x: x.disp_level == PyTango.DispLevel.OPERATOR + self._operatorViewFilter = lambda x: x.disp_level == DisplayLevel.OPERATOR # self.setLayout(Qt.QGridLayout()) self.modelChanged.connect(self._updateCommandWidgets) @@ -639,8 +658,11 @@ def _updateCommandWidgets(self, *args): button.setUseParentModel(True) self._cmdWidgets.append(button) button.commandExecuted.connect(self._onCommandExecuted) + + import taurus.core.tango.util.tango_taurus as tango_taurus + in_type = tango_taurus.FROM_TANGO_TO_TAURUS_TYPE[c.in_type] - if c.in_type != PyTango.CmdArgType.DevVoid: + if in_type is not None: self.debug('Adding arguments for command %s' % c.cmd_name) pwidget = ParameterCB() if c.cmd_name.lower() in self._defaultParameters: @@ -654,7 +676,7 @@ def _updateCommandWidgets(self, *args): button.setParameters(self._defaultParameters[ c.cmd_name.lower()][0]) pwidget.editTextChanged.connect(button.setParameters) - pwidget.currentIndexChanged.connect(button.setParameters) + pwidget.currentIndexChanged['QString'].connect(button.setParameters) pwidget.activated.connect(button.setFocus) button.commandExecuted.connect(pwidget.rememberCurrentText) layout.addWidget(pwidget, row, 1) @@ -771,7 +793,7 @@ def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) self._viewFilters = [] - self._operatorViewFilter = lambda x: x.disp_level == PyTango.DispLevel.OPERATOR + self._operatorViewFilter = lambda x: x.disp_level == DisplayLevel.OPERATOR self.setLayout(Qt.QVBoxLayout()) diff --git a/lib/taurus/qt/qtgui/panel/taurusmessagepanel.py b/lib/taurus/qt/qtgui/panel/taurusmessagepanel.py index e52851a3d..8c022e8a5 100644 --- a/lib/taurus/qt/qtgui/panel/taurusmessagepanel.py +++ b/lib/taurus/qt/qtgui/panel/taurusmessagepanel.py @@ -42,10 +42,6 @@ except: pygments = None -# shame on me for importing PyTango! well not so much since this is designed to -# show PyTango exceptions -import PyTango - from taurus.core.util.report import TaurusMessageReportHandler from taurus.external.qt import Qt from taurus.qt.qtgui.util.ui import UILoadable @@ -94,6 +90,7 @@ def setError(self, err_type=None, err_value=None, err_traceback=None): class TangoMessageErrorHandler(TaurusMessageErrorHandler): """This class is designed to handle :class:`PyTango.DevFailed` error into a :class:`TaurusMessagePanel`""" + # TODO: tango-centric def setError(self, err_type=None, err_value=None, err_traceback=None): """Translates the given error object into an HTML string and places it @@ -488,7 +485,11 @@ def getError(self): :rtype: tuple""" return self._exc_info - ErrorHandlers = {PyTango.DevFailed: TangoMessageErrorHandler} + try: + import PyTango + ErrorHandlers = {PyTango.DevFailed: TangoMessageErrorHandler} + except: + ErrorHandlers = {} @classmethod def registerErrorHandler(klass, err_type, err_handler): @@ -552,6 +553,8 @@ def py_exc(): def tg_exc(): """Shows a tango exception in a TaurusMessagePanel""" + # TODO: This function is Tango centric + import PyTango try: PyTango.Except.throw_exception( 'TangoException', 'A simple tango exception', 'right here') @@ -562,6 +565,8 @@ def tg_exc(): def tg_serv_exc(): """Shows a tango exception from a server in a TaurusMessagePanel""" + # TODO: This function is Tango centric + import PyTango import taurus dev = taurus.Device("sys/tg_test/1") try: @@ -576,6 +581,8 @@ def tg_serv_exc(): def py_tg_serv_exc(): """Shows a tango exception from a python server in a TaurusMessagePanel""" + # TODO: This function is Tango centric + import PyTango try: PyTango.Except.throw_exception( 'TangoException', 'A simple tango exception', 'right here') diff --git a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py index d72b4aa16..26365975b 100644 --- a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py +++ b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py @@ -37,9 +37,6 @@ from taurus.core.util.containers import CaselessList from taurusmodellist import TaurusModelList -#@todo: tango-centric!! -from taurus.core.tango.tangodatabase import TangoDevInfo, TangoAttrInfo - class TaurusModelSelectorTree(TaurusWidget): @@ -107,8 +104,13 @@ def setButtonsPos(self, buttonsPos): raise ValueError("Invalid buttons position") def getSelectedModels(self): - # todo: this method is tango-centric, but it could be fixed... selected = [] + try: + from taurus.core.tango.tangodatabase import (TangoDevInfo, + TangoAttrInfo) + except: + return selected + # TODO: Tango-centric for item in self._deviceTree.selectedItems(): nfo = item.itemData() if isinstance(nfo, TangoDevInfo): @@ -163,8 +165,12 @@ def __init__(self, parent=None, selectables=None, host=None, designMode=None, si model. Otherwise (default) a list of models can be selected ''' TaurusWidget.__init__(self, parent) + if host is None: - host = taurus.Authority().getNormalName() + try: # TODO: Tango-centric! + host = taurus.Factory('tango').getAuthority().getFullName() + except Exception as e: + taurus.info('Cannot populate Tango Tree: %r', e) self._allowDuplicates = False @@ -294,7 +300,9 @@ def resetSingleModelMode(self): self.setSingleModelMode(self, False) @staticmethod - def modelChooserDlg(parent=None, selectables=None, host=None, asMimeData=False, singleModel=False, windowTitle='Model Chooser'): + def modelChooserDlg(parent=None, selectables=None, host=None, asMimeData=False, + singleModel=False, windowTitle='Model Chooser', + listedModels=None): '''Static method that launches a modal dialog containing a TaurusModelChooser :param parent: (QObject) parent for the dialog @@ -308,6 +316,8 @@ def modelChooserDlg(parent=None, selectables=None, host=None, asMimeData=False, :param singleModel: (bool) If True, the selection will be of just one model. Otherwise (default) a list of models can be selected :param windowTitle: (str) Title of the dialog (default="Model Chooser") + :param listedModels: (list) List of model names for initializing the + model list :return: (list,bool or QMimeData,bool) Returns a models,ok tuple. models can be either a list of models or a QMimeData object, depending on @@ -320,6 +330,8 @@ def modelChooserDlg(parent=None, selectables=None, host=None, asMimeData=False, layout = Qt.QVBoxLayout() w = TaurusModelChooser( parent=parent, selectables=selectables, host=host, singleModel=singleModel) + if listedModels is not None: + w.setListedModels(listedModels) layout.addWidget(w) dlg.setLayout(layout) w.updateModels.connect(dlg.accept) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 671ae1565..df71a62a5 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -27,9 +27,10 @@ taurusvalue.py: """ -__all__ = ["TaurusValue", "TaurusValuesFrame", "DefaultTaurusValueCheckBox", "DefaultLabelWidget", +__all__ = ["TaurusValue", "TaurusValuesFrame", "DefaultTaurusValueCheckBox", "DefaultUnitsWidget", "TaurusPlotButton", "TaurusArrayEditorButton", - "TaurusValuesTableButton", "TaurusValuesTableButton_W", "TaurusDevButton"] + "TaurusValuesTableButton", "TaurusValuesTableButton_W", + "DefaultLabelWidget", "TaurusDevButton", "TaurusImageButton"] __docformat__ = 'restructuredtext' @@ -37,7 +38,7 @@ import re from taurus.external.qt import Qt import taurus.core -from taurus.core import DataType, DataFormat +from taurus.core import DataType, DataFormat, TaurusEventType from taurus.core.taurusbasetypes import TaurusElementType from taurus.qt.qtcore.mimetypes import TAURUS_ATTR_MIME_TYPE, TAURUS_DEV_MIME_TYPE, TAURUS_MODEL_MIME_TYPE @@ -90,7 +91,7 @@ def setModel(self, model): try: config = self.taurusValueBuddy().getLabelConfig() except Exception: - config = 'label' + config = '{attr.label}' elementtype = self.taurusValueBuddy().getModelType() fullname = self.taurusValueBuddy().getModelObj().getFullName() if elementtype == TaurusElementType.Attribute: @@ -109,11 +110,12 @@ def setModel(self, model): _BCK_COMPAT_TAGS = {'': '{attr.name}', '': '{attr.fullname}', '': '{dev.name}', - '': '{dev_name}', + '': '{dev.name}', '': '{dev.fullname}', } def getDisplayValue(self, cache=True, fragmentName=None): + """Reimplementation of getDisplayValue""" if self.modelObj is None or fragmentName is None: return self.getNoneValue() # support bck-compat tags @@ -121,9 +123,8 @@ def getDisplayValue(self, cache=True, fragmentName=None): new = self._BCK_COMPAT_TAGS.get(old, '{attr.%s}' % old) self.deprecated(dep=old, alt=new) fragmentName = fragmentName.replace(old, new) - attr = self.getModelObj() - dev = attr.getParent() - return fragmentName.format(dev=dev, attr=attr) + + return TaurusLabel.displayValue(self, fragmentName) def sizeHint(self): return Qt.QSize(Qt.QLabel.sizeHint(self).width(), 18) @@ -141,9 +142,11 @@ def contextMenuEvent(self, event): r_action.setEnabled(self.taurusValueBuddy().hasPendingOperations()) if self.taurusValueBuddy().isModifiableByUser(): menu.addAction("Change label", - self.taurusValueBuddy().onChangeLabelConfig) + self.taurusValueBuddy()._onChangeLabelText) menu.addAction("Change Read Widget", self.taurusValueBuddy().onChangeReadWidget) + menu.addAction("Set Formatter", + self.taurusValueBuddy().onSetFormatter) cw_action = menu.addAction( "Change Write Widget", self.taurusValueBuddy().onChangeWriteWidget) # disable the action if the taurusValue is readonly @@ -254,9 +257,9 @@ class TaurusArrayEditorButton(_AbstractTaurusValueButton): class TaurusImageButton(_AbstractTaurusValueButton): - '''A button that launches a TaurusPlot''' + '''A button that launches an ImageDialog''' _widgetClassName = 'TaurusImageDialog' - _icon = ':/mimetypes/image-x-generic.svg' + _icon = 'mimetypes:image-x-generic.svg' class TaurusValuesTableButton(_AbstractTaurusValueButton): @@ -275,7 +278,7 @@ class TaurusValuesTableButton_W(TaurusValuesTableButton): class TaurusDevButton(_AbstractTaurusValueButton): '''A button that launches a TaurusAttrForm''' _widgetClassName = 'TaurusDevicePanel' - _icon = ':/places/folder-remote.svg' + _icon = 'places:folder-remote.svg' _text = 'Show Device' @@ -309,6 +312,7 @@ def __init__(self, parent=None, designMode=False, customWidgetMap=None): self.call__init__wo_kw(Qt.QWidget, parent) self.call__init__(TaurusBaseWidget, name, designMode=designMode) + self.__error = False self.__modelClass = None self._designMode = designMode @@ -431,6 +435,13 @@ def setParent(self, parent): # do the base class stuff too Qt.QWidget.setParent(self, parent) + def onSetFormatter(self): + """ + Reimplemented to call onSetFormatter of the read widget (if provided) + """ + if hasattr(self._readWidget, 'onSetFormatter'): + return self._readWidget.onSetFormatter() + def getAllowWrite(self): return self._allowWrite @@ -625,8 +636,13 @@ def getCustomWidgetMap(self): return self._customWidgetMap def onChangeLabelConfig(self): + self.deprecated(msg="onChangeLabelConfig is deprecated", rel="Jan2018") + self._onChangeLabelText() + + def _onChangeLabelText(self): keys = ['{attr.label}', '{attr.name}', '{attr.fullname}', '{dev.name}', '{dev.fullname}'] + try: current = keys.index(self.labelConfig) except: @@ -641,7 +657,7 @@ def onChangeLabelConfig(self): labelConfig, ok = Qt.QInputDialog.getItem(self, 'Change Label', msg, keys, current, True) if ok: - self.labelConfig = str(labelConfig) + self.labelConfig = labelConfig def onChangeReadWidget(self): classnames = ['None', 'Auto'] + \ @@ -1153,7 +1169,10 @@ def handleEvent(self, evt_src, evt_type, evt_value): """Reimplemented from :meth:`TaurusBaseWidget.handleEvent` to update subwidgets on config events """ - if evt_type == taurus.core.taurusbasetypes.TaurusEventType.Config and not self._designMode: + if self._designMode: + return + + if self.__error or evt_type == TaurusEventType.Config: self.updateCustomWidget() self.updateLabelWidget() self.updateReadWidget() @@ -1161,6 +1180,9 @@ def handleEvent(self, evt_src, evt_type, evt_value): self.updateUnitsWidget() self.updateExtraWidget() + # set/unset the error flag + self.__error = (evt_type == TaurusEventType.Error) + def isValueChangedByUser(self): try: return self._writeWidget.isValueChangedByUser() @@ -1214,7 +1236,29 @@ def getLabelConfig(self): @Qt.pyqtSlot('QString') def setLabelConfig(self, config): + """Sets fragment configuration to the label widget. + + :param config: fragment + :type config: str + """ self._labelConfig = config + # backwards compatibility: this method used to work for setting + # an arbitrary text to the label widget + try: + self.getModelFragmentObj(config) + self._labelWidget._permanentText = None + except Exception: + try: + for old in re.findall('<.+?>', config): + new = self._BCK_COMPAT_TAGS.get(old, old) + self.deprecated(dep=old, alt=new) + config = config.replace(old, new) + + self._labelWidget.setText(config) + except: + self.debug("Setting permanent text to the label widget failed") + return + self.updateLabelWidget() def resetLabelConfig(self): diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py index 7c4441a46..5728acf5d 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py @@ -34,35 +34,35 @@ DEV_NAME = TangoSchemeTestLauncher.DEV_NAME -@insertTest(helper_name='texts', - model='tango:' + DEV_NAME + '/double_scalar', - expected=('double_scalar', '1.23', '0.00 mm', 'mm'), - # expected=('double_scalar', '1.23', '0.0', 'mm'), +@insertTest(helper_name="texts", + model="tango:" + DEV_NAME + "/double_scalar", + expected=("double_scalar", "1.23", "0.00 mm", "mm"), + # expected=("double_scalar", "1.23", "0.0", "mm"), # TODO: change taurusvalue's line edit to hide units ) class TaurusValueTest(TangoSchemeTestLauncher, BaseWidgetTestCase, unittest.TestCase): - ''' + """ Specific tests for TaurusValue - ''' + """ _klass = TaurusValue def test_bug126(self): - '''Verify that case is not lost when customizing a label (bug#126)''' + """Verify that case is not lost when customizing a label (bug#126)""" w = self._widget - # self._widget.setModel('eval:1') - self._widget.setModel('tango:' + DEV_NAME + '/double_scalar') - label = 'MIXEDcase' + # self._widget.setModel("eval:1") + self._widget.setModel("tango:" + DEV_NAME + "/double_scalar") + label = "MIXEDcase" w.setLabelConfig(label) self.processEvents(repetitions=10, sleep=.1) shownLabel = str(w.labelWidget().text()) msg = 'Shown label ("%s") differs from set label ("%s")' % (shownLabel, label) self.assertEqual(label, shownLabel, msg) - self.assertMaxDeprecations(0) + self.assertMaxDeprecations(1) def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): - '''Checks the texts for scalar attributes''' + """Checks the texts for scalar attributes""" self._widget.setModel(model) if fgRole is not None: self._widget.setFgRole(fgRole) @@ -77,11 +77,23 @@ def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): self.assertEqual(got, expected, msg) self.assertMaxDeprecations(maxdepr) + def test_labelCaseSensitivity(self): + """Verify that case is respected of in the label widget""" + w = self._widget + self._widget.setModel("tango:" + DEV_NAME + "/MIXEDcase") + label = "MIXEDcase" + self.processEvents(repetitions=10, sleep=.1) + shownLabel = str(w.labelWidget().text()) + msg = 'Shown label ("%s") differs from set label ("%s")' % (shownLabel, + label) + self.assertEqual(label, shownLabel, msg) + self.assertMaxDeprecations(0) + def tearDown(self): - '''Set Model to None''' + """Set Model to None""" self._widget.setModel(None) TangoSchemeTestLauncher.tearDown(self) unittest.TestCase.tearDown(self) -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/lib/taurus/qt/qtgui/plot/curveprops.py b/lib/taurus/qt/qtgui/plot/curveprops.py index 229ae8c97..ecfaee425 100755 --- a/lib/taurus/qt/qtgui/plot/curveprops.py +++ b/lib/taurus/qt/qtgui/plot/curveprops.py @@ -36,6 +36,7 @@ from taurus.external.qt import Qt, Qwt5 import taurus import taurus.core +from taurus.core import TaurusElementType from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE from taurus.qt.qtgui.util.ui import UILoadable @@ -45,26 +46,6 @@ NamedColors, CurveAppearanceProperties -# URI regexp including slices in fragment (adapted from http://www.ietf.org/rfc/rfc2396.txt (appendix B)) -# '^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*)(\[[0-9,:]*?\]))?' -# 12 3 4 5 6 7 8 9 A -# -# we base the nexus and ascii file regexps on this one, keeping the group numbers -# The different components of an URI can be obtained from this regexp using match.group(n1,n2,...), where: -# COMPONENT GROUP_number -# scheme 2 -# authority 4 -# path 5 -# query 7 -# fragment 9 -# slice 10 (0xA) - -NEXUS_SRC = re.compile( - r'^((nxfile|nxdata):)(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*)(\[[0-9,: ]*?\]))?') -ASCII_SRC = re.compile( - r'^((file):)(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*)(\[[0-9,: ]*?\]))?') - - # set some named constants # columns: NUMCOLS = 4 @@ -72,10 +53,7 @@ SRC_ROLE = Qt.Qt.UserRole + 1 PROPS_ROLE = Qt.Qt.UserRole + 2 -# TODO: Tango-centric (use agnostic validation) -from taurus.core.tango.tangovalidator import TangoAttributeNameValidator -ATTRNAMEVALIDATOR = TangoAttributeNameValidator() - +from taurus import isValidName class Component(object): @@ -84,7 +62,6 @@ def __init__(self, src): self.display = '' self.icon = Qt.QIcon() self.ok = True - self._attrnamevalidator = ATTRNAMEVALIDATOR self._dbCache = taurus.Authority() self.setSrc(src) @@ -104,27 +81,9 @@ def processSrc(self, src): if src.startswith('='): #@todo: evaluate/validate the expression return src, src[1:].strip(), Qt.QIcon.fromTheme('accessories-calculator'), True - # for tango attributes - if self._attrnamevalidator.isValid(src): - pars = self._attrnamevalidator.getUriGroups(src) - dev = self._dbCache.getDevice(pars['devname']) - if dev is None: - return src, src, Qt.QIcon.fromTheme('network-error'), False - attr = dev.getAttribute(pars['_shortattrname']) - if attr is None: - return src, pars['_shortattrname'], Qt.QIcon.fromTheme('network-error'), False - return src, attr.name(), Qt.QIcon.fromTheme('network-server'), True - # for nexus files - m = re.match(NEXUS_SRC, src) - if m is not None: - host, path, nxpath, slice = m.group(4, 5, 9, 10) - #@todo:open file and check the data is accessible - return src, nxpath, Qt.QIcon.fromTheme('x-office-spreadsheet'), True - # for ascii files - m = re.match(ASCII_SRC, src) - if m is not None: - host, path, = m.group(4, 5) - #@todo: open and check the file + # for attributes + if isValidName(src, etypes=[TaurusElementType.Attribute]): + return src, src, Qt.QIcon.fromTheme('network-server'), True # If nothing matches... return src, src, Qt.QIcon.fromTheme('dialog-warning'), False diff --git a/lib/taurus/qt/qtgui/plot/taurusplot.py b/lib/taurus/qt/qtgui/plot/taurusplot.py index 7873c5283..7163a5ddf 100644 --- a/lib/taurus/qt/qtgui/plot/taurusplot.py +++ b/lib/taurus/qt/qtgui/plot/taurusplot.py @@ -1031,8 +1031,12 @@ class TaurusPlot(Qwt5.QwtPlot, TaurusBaseWidget): def __init__(self, parent=None, designMode=False): name = "TaurusPlot" + # book-keeping of attached tauruscurves + self.curves = CaselessDict() # TODO: Tango-centric + Qwt5.QwtPlot.__init__(self, parent) TaurusBaseWidget.__init__(self, name) + self._designMode = designMode self._modelNames = [] self._useParentModel = False @@ -1066,8 +1070,6 @@ def __init__(self, parent=None, designMode=False): # enable dropping (see also dragEnterEvent and dropEvent methods) self.setAcceptDrops(True) - # book-keeping of attached tauruscurves - self.curves = CaselessDict() # TODO: Tango-centric #self.curves_lock = threading.RLock() self.curves_lock = DummyLock() @@ -1177,6 +1179,9 @@ def __initActions(self): self._dataInspectorAction.setChecked(self._pointPicker.isEnabled()) self._dataInspectorAction.toggled[bool].connect(self.toggleDataInspectorMode) + self._setFormatterAction = Qt.QAction("Set Formatter...", None) + self._setFormatterAction.triggered[()].connect(self.onSetFormatter) + self._curveStatsAction = Qt.QAction("Calculate statistics", None) self._curveStatsAction.setShortcut(Qt.Qt.Key_S) self._curveStatsAction.triggered[()].connect(self.onCurveStatsAction) @@ -1248,11 +1253,15 @@ def __initActions(self): # add all actions and limit the scope of the key shortcuts to the # widget (default is Window) - for action in (self._dataInspectorAction, self._pauseAction, self._autoscaleAllAxisAction, - self._toggleZoomAxisAction, self._configDialogAction, self._inputDataAction, - self._saveConfigAction, self._loadConfigAction, self._showLegendAction, - self._showMaxAction, self._showMinAction, self._printAction, self._exportPdfAction, - self._exportAsciiAction, self._setCurvesTitleAction, self._curveStatsAction): + for action in (self._dataInspectorAction, self._pauseAction, + self._autoscaleAllAxisAction, + self._toggleZoomAxisAction, self._configDialogAction, + self._inputDataAction, self._saveConfigAction, + self._loadConfigAction, self._showLegendAction, + self._showMaxAction, self._showMinAction, + self._printAction, self._exportPdfAction, + self._exportAsciiAction, self._setCurvesTitleAction, + self._curveStatsAction, self._setFormatterAction): # this is needed to avoid ambiguity when more than one TaurusPlot # is used in the same window action.setShortcutContext(Qt.Qt.WidgetShortcut) @@ -1260,6 +1269,15 @@ def __initActions(self): # that gets the focus (the canvas instead of self) self.canvas().addAction(action) + def setFormat(self, format): + """Reimplemented from TaurusBaseComponent""" + targetCurveNames = self.curves.iterkeys() + for name in targetCurveNames: + curve = self.curves.get(name, None) + w = getattr(curve, 'owner', curve) + w.setFormat(format) + TaurusBaseComponent.setFormat(self, format) + def dropEvent(self, event): '''reimplemented to support dropping of modelnames in taurusplots''' mtype = self.handleMimeData(event.mimeData(), self.addModels) @@ -2159,6 +2177,7 @@ def _canvasContextMenu(self): menu.addAction(self._showLegendAction) menu.addAction(self._dataInspectorAction) + menu.addAction(self._setFormatterAction) menu.addSeparator() exportSubMenu = menu.addMenu("&Export && Print") @@ -2301,7 +2320,8 @@ def _createMiscDict(self): miscdict = {'defaultCurvesTitle': self.getDefaultCurvesTitle(), 'canvasBackground': self.canvasBackground(), 'orderedCurveNames': self.getCurveNamesSorted(), - 'plotTitle': unicode(self.title().text())} + 'plotTitle': unicode(self.title().text()), + 'formatter': self.getFormat()} if self.isWindow(): miscdict["Geometry"] = self.saveGeometry() return miscdict @@ -2430,6 +2450,8 @@ def applyMiscConfig(self, miscdict): # set geometry (if this is a top level window) if self.isWindow() and 'Geometry' in miscdict: self.restoreGeometry(miscdict['Geometry']) + if "formatter" in miscdict: + self.setFormat(miscdict['formatter']) def applyAxesConfig(self, axes): '''sets the axes according to settings stored in the axes dict, @@ -3668,6 +3690,8 @@ def main(): help="interprete X values as either timestamps (t) or numbers (n). Accepted values: t|n (e is also accepted as a synonim of n)") parser.add_option("--config", "--config-file", dest="config_file", default=None, help="use the given config file for initialization") + parser.add_option("--import-ascii", dest="import_ascii", default=None, + help="import the given ascii file into the plot") parser.add_option("--export", "--export-file", dest="export_file", default=None, help="use the given file to as output instead of showing the plot") parser.add_option("--window-name", dest="window_name", @@ -3690,8 +3714,12 @@ def main(): if options.config_file is not None: w.loadConfig(options.config_file) + if options.import_ascii is not None: + w.importAscii([options.import_ascii], xcol=0) + if models: w.setModel(models) + if options.export_file is not None: curves = dict.fromkeys(w.trendSets.keys(), 0) @@ -3715,7 +3743,10 @@ def exportIfAllCurves(curve, trend=w, counters=curves): # show the widget w.show() # if no models are passed, show the data import dialog - if len(models) == 0 and options.config_file is None: + if (len(models) == 0 + and options.config_file is None + and options.import_ascii is None + ): w.showDataImportDlg() sys.exit(app.exec_()) diff --git a/lib/taurus/qt/qtgui/plot/taurustrend.py b/lib/taurus/qt/qtgui/plot/taurustrend.py index 75fbfb7a4..e40b0327c 100644 --- a/lib/taurus/qt/qtgui/plot/taurustrend.py +++ b/lib/taurus/qt/qtgui/plot/taurustrend.py @@ -45,7 +45,7 @@ def getArchivedTrendValues(*args, **kwargs): try: - import PyTangoArchiving + import PyTangoArchiving # TODO: tango-centric return PyTangoArchiving.getArchivedTrendValues(*args, **kwargs) except: return [] @@ -491,14 +491,14 @@ def _onDroppedEvent(self, reason='Unknown'): mustwarn = False if self.droppedEventsCount == self.droppedEventsWarning: mustwarn = True - msg = ('At least %i events from model "%s" have being dropped. This attribute may have problems\n' + + msg = ('At least %i events from model "%s" have been dropped. This attribute may have problems\n' + 'Future occurrences will be silently ignored') % (self.droppedEventsWarning, self.modelName) # disable the consecutive Dropped events warning (we do not want it # if we got this one) self.consecutiveDroppedEventsWarning = -1 if self.consecutiveDroppedEventsCount == self.consecutiveDroppedEventsWarning: mustwarn = True - msg = ('At least %i consecutive events from model "%s" have being dropped. This attribute may have problems\n' + + msg = ('At least %i consecutive events from model "%s" have been dropped. This attribute may have problems\n' + 'Future occurrences will be silently ignored') % (self.consecutiveDroppedEventsWarning, self.modelName) # disable the consecutive Dropped events warning self.consecutiveDroppedEventsWarning = -1 @@ -1626,7 +1626,12 @@ def showArchivingWarning(self): # stop the scale change notification temporally (to avoid duplicate # warnings) self.setUseArchiving(False) - self.axisWidget(self.xBottom).scaleDivChanged.disconnect(self._scaleChangeWarning) + try: + self.axisWidget(self.xBottom).scaleDivChanged.disconnect( + self._scaleChangeWarning) + except: + self.warning('Failed to disconnect ScaleChangeWarning dialog') + # show a dialog dlg = Qt.QDialog(self) dlg.setModal(True) diff --git a/lib/taurus/qt/qtgui/table/taurusgrid.py b/lib/taurus/qt/qtgui/table/taurusgrid.py index 6fbbdb4fc..6a4c5c0b2 100644 --- a/lib/taurus/qt/qtgui/table/taurusgrid.py +++ b/lib/taurus/qt/qtgui/table/taurusgrid.py @@ -46,7 +46,8 @@ from taurus.external.qt import Qt, QtGui, QtCore import taurus -from taurus.qt.qtcore.util.emitter import modelSetter, TaurusEmitterThread, SingletonWorker, MethodModel +from taurus.qt.qtcore.util.emitter import (modelSetter, TaurusEmitterThread, + SingletonWorker, MethodModel) from taurus.core.taurusmanager import TaurusManager from taurus.core.util.log import Logger from taurus.qt.qtgui.base import TaurusBaseWidget @@ -71,31 +72,32 @@ def get_all_models(expressions, limit=1000): It practically equals to fandango.get_matching_attributes; check which is better! Move this method to taurus.core.tango.search ''' - #print( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) + # print( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) if isinstance(expressions, str): # if any(re.match(s,expressions) for s in ('\{.*\}','\(.*\)','\[.*\]')): - ##self.debug( 'evaluating expressions ....') - #expressions = list(eval(expressions)) + ##self.debug( 'evaluating expressions ....') + # expressions = list(eval(expressions)) # else: - ##self.debug( 'expressions as string separated by commas ...') + ##self.debug( 'expressions as string separated by commas ...') expressions = expressions.split(',') - elif any(isinstance(expressions, klass) for klass in (QtCore.QStringList, list, tuple, dict)): - #self.debug( 'expressions converted from list ...') + elif any(isinstance(expressions, klass) for klass in + (QtCore.QStringList, list, tuple, dict)): + # self.debug( 'expressions converted from list ...') expressions = list(str(e) for e in expressions) - #self.debug( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) + # self.debug( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) taurus_db = taurus.Authority() - #taurus_db = taurus.Authority(os.environ['TANGO_HOST']) + # taurus_db = taurus.Authority(os.environ['TANGO_HOST']) # WHAAAAAAT????? Someone should get beaten for this line if 'SimulationAuthority' in str(type(taurus_db)): - #self.trace( 'Using a simulated database ...') + # self.trace( 'Using a simulated database ...') models = expressions else: all_devs = taurus_db.get_device_exported('*') models = [] for exp in expressions: - #self.trace( 'evaluating exp = "%s"' % exp) + # self.trace( 'evaluating exp = "%s"' % exp) exp = str(exp) devs = [] targets = [] @@ -111,21 +113,23 @@ def get_all_models(expressions, limit=1000): else: devs = [device] - #self.trace( 'TaurusGrid.get_all_models(): devices matched by %s / %s are %d:' % (device,attribute,len(devs))) - #self.debug( '%s' % (devs)) + # self.trace( 'TaurusGrid.get_all_models(): devices matched by %s / %s are %d:' % (device,attribute,len(devs))) + # self.debug( '%s' % (devs)) for dev in devs: if any(c in attribute for c in '.*[]()+?'): if '*' in attribute and '.*' not in attribute: attribute = attribute.replace('*', '.*') try: - #taurus_dp = taurus.core.taurusdevice.TaurusDevice(dev) - taurus_dp = taurus.core.taurusmanager.TaurusManager().getFactory()().getDevice(dev) - #self.debug( "taurus_dp = %s"%taurus_dp.getFullName()) - attrs = [att.name for att in taurus_dp.attribute_list_query( - ) if re_match_low(attribute, att.name)] + # taurus_dp = taurus.core.taurusdevice.TaurusDevice(dev) + taurus_dp = taurus.core.taurusmanager.TaurusManager().getFactory()().getDevice( + dev) + # self.debug( "taurus_dp = %s"%taurus_dp.getFullName()) + attrs = [att.name for att in + taurus_dp.attribute_list_query( + ) if re_match_low(attribute, att.name)] targets.extend(dev + '/' + att for att in attrs) except Exception, e: - #self.warning( 'ERROR! TaurusGrid.get_all_models(): Unable to get attributes for device %s: %s' % (dev,str(e))) + # self.warning( 'ERROR! TaurusGrid.get_all_models(): Unable to get attributes for device %s: %s' % (dev,str(e))) pass else: targets.append(dev + '/' + attribute) @@ -133,7 +137,7 @@ def get_all_models(expressions, limit=1000): # % (exp,targets) models.extend(targets) models = models[:limit] - #print( 'Out of TaurusGrid.get_all_models(...)') + # print( 'Out of TaurusGrid.get_all_models(...)') return models @@ -142,16 +146,18 @@ def get_readwrite_models(expressions, limit=1000): All devices matching expressions must be obtained. For each device only the good attributes are read. ''' - #self.debug( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) + # self.debug( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) if isinstance(expressions, str): - if any(re.match(s, expressions) for s in ('\{.*\}', '\(.*\)', '\[.*\]')): - #self.trace( 'evaluating expressions ....') + if any(re.match(s, expressions) for s in + ('\{.*\}', '\(.*\)', '\[.*\]')): + # self.trace( 'evaluating expressions ....') expressions = list(eval(expressions)) else: - #self.trace( 'expressions as string separated by commas ...') + # self.trace( 'expressions as string separated by commas ...') expressions = expressions.split(',') - elif any(isinstance(expressions, klass) for klass in (QtCore.QStringList, list, tuple, dict)): + elif any(isinstance(expressions, klass) for klass in + (QtCore.QStringList, list, tuple, dict)): expressions = list(str(e) for e in expressions) taurus_db = taurus.Authority() @@ -182,9 +188,12 @@ def get_readwrite_models(expressions, limit=1000): if '*' in attribute and '.*' not in attribute: attribute = attribute.replace('*', '.*') try: - taurus_dp = taurus.core.taurusmanager.TaurusManager().getFactory()().getDevice(dev) - attrs = [att.name for att in taurus_dp.attribute_list_query( - ) if re_match_low(attribute, att.name) and att.isReadOnly()] + taurus_dp = taurus.core.taurusmanager.TaurusManager().getFactory()().getDevice( + dev) + attrs = [att.name for att in + taurus_dp.attribute_list_query( + ) if re_match_low(attribute, + att.name) and att.isReadOnly()] targets.extend(dev + '/' + att for att in attrs) except Exception, e: pass @@ -209,21 +218,38 @@ class TaurusGrid(QtGui.QFrame, TaurusBaseWidget): @todo _TAGS property should allow to change row/columns meaning and also add new Custom tags based on regexp """ - #------------------------------------------------------------------------- + # ------------------------------------------------------------------------- # Write your own code here to define the signals generated by this widget # itemSelected = Qt.pyqtSignal('QString') - itemClicked = Qt.pyqtSignal() + itemClicked = Qt.pyqtSignal('QString') _TAGS = ['DOMAIN', 'FAMILY', 'HOST', 'LEVEL', 'CLASS', 'ATTRIBUTE', 'DEVICE'] + class _TaurusGridCell(Qt.QFrame): + + itemClicked = Qt.pyqtSignal('QString') + + # Done in this way as TaurusValue.mousePressEvent is never called + def mousePressEvent(self, event): + # print 'In cell clicked' + targets = set(str(child.getModelName()) + for child in self.children() + if hasattr(child, 'underMouse') + and child.underMouse() + and hasattr(child, 'getModelName') + ) + for t in targets: + self.itemClicked.emit(t) + def __init__(self, parent=None, designMode=False): name = self.__class__.__name__ self.call__init__wo_kw(QtGui.QFrame, parent) - #self.call__init__(TaurusBaseWidget, name, parent, designMode=designMode) + # self.call__init__(TaurusBaseWidget, name, parent, + # designMode=designMode) # It was needed to avoid exceptions in TaurusDesigner! if isinstance(parent, TaurusBaseWidget): self.call__init__(TaurusBaseWidget, name, @@ -268,7 +294,8 @@ def save(self, filename): d = { 'model': self.filter, 'row_labels': self.row_labels, 'column_labels': self.column_labels, - 'frames': self._show_row_frame or self._show_column_frame, 'labels': self._show_attr_labels, + 'frames': self._show_row_frame or self._show_column_frame, + 'labels': self._show_attr_labels, 'units': self._show_attr_units, 'others': self._show_others } f = open(filename, 'w') @@ -299,7 +326,7 @@ def load(self, filename, delayed=False): def defineStyle(self): """ Defines the initial style for the widget """ - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # Write your own code here to set the initial style of your widget # self.setLayout(QtGui.QGridLayout()) @@ -312,12 +339,12 @@ def sizeHint(self): def minimumSizeHint(self): return QtGui.QFrame.minimumSizeHint(self) - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # TaurusBaseWidget over writing - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- def getModelClass(self): - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # [MANDATORY] # Replace your own code here # ex.: return taurus.core.taurusattribute.Attribute @@ -330,7 +357,7 @@ def attach(self): if self.isAttached(): return True - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # Write your own code here before attaching widget to attribute connect # the proper signal so that the first event is correctly received by the # widget @@ -350,7 +377,7 @@ def detach(self): TaurusBaseWidget.detach(self) - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # Write your own code here after detaching the widget from the model # # Typical code is: @@ -362,7 +389,7 @@ def detach(self): # by default disable widget when dettached self.setEnabled(False) - #------------------------------------------------------------------------- + # ------------------------------------------------------------------------- # [MANDATORY] # Uncomment the following method if your superclass does not provide with a # isReadOnly() method or if you need to change its behavior @@ -371,7 +398,7 @@ def detach(self): # return True def updateStyle(self): - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # Write your own code here to update your widget style self.trace('@' * 80) self.trace( @@ -379,7 +406,7 @@ def updateStyle(self): self.trace('@' * 80) # It was showing an annoying "True" in the widget - #value = self.getShowText() or '' + # value = self.getShowText() or '' # if self._setText: self._setText(value) ##It must be included # update tooltip @@ -394,10 +421,11 @@ def updateStyle(self): self.update() - #------------------------------------------------------------------------- + # ------------------------------------------------------------------------- # Write your own code here for your own widget properties - def setModel(self, model, devsInRows=False, delayed=False, append=False, load=True): + def setModel(self, model, devsInRows=False, delayed=False, append=False, + load=True): '''The model can be initialized as a list of devices or hosts or dictionary or ...''' # self.setModelCheck(model) ##It must be included # differenciate if the model is a RegExp @@ -431,22 +459,28 @@ def setModel(self, model, devsInRows=False, delayed=False, append=False, load=Tr (self._modelNames))[:100] + '...') if load: - self.trace('In TaurusGrid.setModel(%s,load=True): modelNames are %d' % ( - str(model)[:100] + '...', len(self._modelNames))) # ,self._modelNames)) + self.trace( + 'In TaurusGrid.setModel(%s,load=True): modelNames are %d' % ( + str(model)[:100] + '...', + len(self._modelNames))) # ,self._modelNames)) if devsInRows: self.setRowLabels( - ','.join(set(d.rsplit('/', 1)[0] for d in self._modelNames))) + ','.join( + set(d.rsplit('/', 1)[0] for d in self._modelNames))) self.create_widgets_table(self._modelNames) self.modelsQueue.put( (MethodModel(self.showRowFrame), self._show_row_frame)) self.modelsQueue.put( - (MethodModel(self.showColumnFrame), self._show_column_frame)) + ( + MethodModel(self.showColumnFrame), self._show_column_frame)) self.modelsQueue.put( (MethodModel(self.showOthers), self._show_others)) self.modelsQueue.put( - (MethodModel(self.showAttributeLabels), self._show_attr_labels)) + (MethodModel(self.showAttributeLabels), + self._show_attr_labels)) self.modelsQueue.put( - (MethodModel(self.showAttributeUnits), self._show_attr_units)) + (MethodModel(self.showAttributeUnits), + self._show_attr_units)) self.updateStyle() if not self.delayed: @@ -487,7 +521,8 @@ def setTitle(self, title): self.title_widget.hide() def parse_labels(self, text): - if any(text.startswith(c[0]) and text.endswith(c[1]) for c in [('{', '}'), ('(', ')'), ('[', ']')]): + if any(text.startswith(c[0]) and text.endswith(c[1]) for c in + [('{', '}'), ('(', ')'), ('[', ']')]): try: labels = eval(text) return labels @@ -513,7 +548,7 @@ def setRowLabels(self, rows): i, QtGui.QTableWidgetItem(section)) except Exception, e: self.debug("setRowLabels(): Exception! %s" % e) - # self.create_widgets_table(self._columnsNames) + # self.create_widgets_table(self._columnsNames) def getRowLabels(self): return ','.join(':'.join(c) for c in self.row_labels) @@ -534,7 +569,7 @@ def setColumnLabels(self, columns): i, QtGui.QTableWidgetItem(equipment)) except Exception, e: self.debug("setColumnLabels(): Exception! %s" % e) - # self.create_widgets_table(self._columnsNames) + # self.create_widgets_table(self._columnsNames) def getColumnLabels(self): return ','.join(':'.join(c) for c in self.column_labels) @@ -589,9 +624,9 @@ def showAttributeUnits(self, boolean): pass return self._show_attr_units - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties - #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- model = QtCore.pyqtProperty("QStringList", getModel, setModel, @@ -610,30 +645,32 @@ def showAttributeUnits(self, boolean): TaurusBaseWidget.setUseParentModel, TaurusBaseWidget.resetUseParentModel) - #------------------------------------------------------------------------- + # ------------------------------------------------------------------------- # Write your own code here for your own widget properties def create_widgets_dict(self, models): from collections import defaultdict # recursive dictionary with 2 levels values = defaultdict(lambda: defaultdict(list)) - #domains = list(set(m.split('/')[0].upper())) - #families = list(set(m.split('/')[1].upper())) + # domains = list(set(m.split('/')[0].upper())) + # families = list(set(m.split('/')[1].upper())) if not self.row_labels: # Domains used by default self.row_labels = sorted( - list(set(m.split('/')[0].upper() for m in models if m.count('/') >= 2))) + list(set(m.split('/')[0].upper() for m in models if + m.count('/') >= 2))) self.row_labels = zip(self.row_labels, self.row_labels) if not self.column_labels: # Families used by default self.column_labels = sorted( - list(set(m.split('/')[1].upper() for m in models if m.count('/') >= 2))) + list(set(m.split('/')[1].upper() for m in models if + m.count('/') >= 2))) self.column_labels = zip(self.column_labels, self.column_labels) - # for m in models: + # for m in models: # if m.count('/')<2: - #self.warning('Wrong model cannot be added: %s'%m) + # self.warning('Wrong model cannot be added: %s'%m) # else: - #domain,family = m.upper().split('/')[:2] + # domain,family = m.upper().split('/')[:2] # values[domain][family].append(m) row_not_found, col_not_found = False, False @@ -668,7 +705,7 @@ def create_widgets_dict(self, models): def create_frame_with_gridlayout(self): """ Just a 'macro' to create the layouts that seem to fit better. """ - frame = QtGui.QFrame() + frame = TaurusGrid._TaurusGridCell() frame.setLayout(QtGui.QGridLayout()) frame.layout().setContentsMargins(2, 2, 2, 2) frame.layout().setSpacing(0) @@ -693,7 +730,7 @@ def create_widgets_table(self, models): for row in self.rows: line = [] for col in self.columns: - #line.append(dct[row][col] if col in dct[row] else []) + # line.append(dct[row][col] if col in dct[row] else []) if col in dct[row]: line.append(dct[row][col]) else: @@ -716,13 +753,13 @@ def create_widgets_table(self, models): self.table.setVerticalHeaderItem( i, QtGui.QTableWidgetItem(section)) -# table.setAutoScroll(True) -# resize_mode = QtGui.QHeaderView.Stretch -# table.horizontalHeader().setSectionResizeMode(resize_mode) + # table.setAutoScroll(True) + # resize_mode = QtGui.QHeaderView.Stretch + # table.horizontalHeader().setSectionResizeMode(resize_mode) # table.verticalHeader().setSectionResizeMode(resize_mode) -# for row in range(len(self.rows)): -# table.setRowHeight(row,5+25*sum(len(dct[self.rows[row]][col]) for col in self.columns)) + # for row in range(len(self.rows)): + # table.setRowHeight(row,5+25*sum(len(dct[self.rows[row]][col]) for col in self.columns)) # for col in range(len(self.columns)): # table.setColumnWidth(col,300) @@ -734,7 +771,7 @@ def create_widgets_table(self, models): else: self.layout().addWidget(self.table, 1, 0) - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # SECTION CHECKBOXES self.checkboxes_frame = self.create_frame_with_gridlayout() @@ -784,7 +821,8 @@ def create_widgets_table(self, models): checkbox.hide() else: checkbox.setChecked(True) - self.columns_frame.layout().addWidget(checkbox, layout_row, layout_col) + self.columns_frame.layout().addWidget(checkbox, layout_row, + layout_col) layout_col += 1 if layout_col == 3: layout_col = 0 @@ -823,14 +861,16 @@ def showOthers(self, boolean): self._show_others = boolean if hasattr(self, 'rows_frame'): for checkbox in self.rows_frame.children(): - if isinstance(checkbox, QtGui.QCheckBox) and checkbox.text() == 'Others': + if isinstance(checkbox, + QtGui.QCheckBox) and checkbox.text() == 'Others': if self._show_others: checkbox.show() else: checkbox.hide() if hasattr(self, 'columns_frame'): for checkbox in self.columns_frame.children(): - if isinstance(checkbox, QtGui.QCheckBox) and checkbox.text() == 'Others': + if isinstance(checkbox, + QtGui.QCheckBox) and checkbox.text() == 'Others': if self._show_others: checkbox.show() else: @@ -865,16 +905,19 @@ def build_table(self, values): table.horizontalHeader().setSectionResizeMode(QtGui.QHeaderView.Stretch) # table.verticalHeader().setSectionResizeMode(QtGui.QHeaderView.Stretch) # table.horizontalHeader().setSectionResizeMode(QtGui.QHeaderView.ResizeToContents) - table.verticalHeader().setSectionResizeMode(QtGui.QHeaderView.ResizeToContents) + table.verticalHeader().setSectionResizeMode( + QtGui.QHeaderView.ResizeToContents) return table - def build_widgets(self, values, show_labels=False, width=240, height=20, value_width=120): + def build_widgets(self, values, show_labels=False, width=240, height=20, + value_width=120): widgets_matrix = [] for row in values: widgets_row = [] for cell in row: cell_frame = self.create_frame_with_gridlayout() + cell_frame.itemClicked.connect(self.onItemClicked) count = 0 for synoptic in sorted(cell): self.debug("processing synoptic %s" % synoptic) @@ -883,7 +926,6 @@ def build_widgets(self, values, show_labels=False, width=240, height=20, value_w self.debug('Creating TaurusValue with model = %s' % model) synoptic_value = TaurusValue(cell_frame) self.modelsQueue.put((synoptic_value, model)) - synoptic_value.itemClicked.connect(self.itemClicked) if self.hideLabels: synoptic_value.setLabelWidgetClass(None) @@ -894,23 +936,11 @@ def build_widgets(self, values, show_labels=False, width=240, height=20, value_w self._widgets_list.append(synoptic_value) count += 1 - # Done in this way as TauValue.mousePressEvent are never called - def mousePressEvent(event, obj): - # print 'In cell clicked' - targets = set(str(child.getModelName()) for child in obj.children() - if hasattr(child, 'underMouse') and child.underMouse() and hasattr(child, 'getModelName')) - [obj.itemClicked.emit(t) - for t in targets] - - cell_frame.mousePressEvent = partial( - mousePressEvent, obj=cell_frame) - cell_frame.itemClicked.connect(self.itemClicked) - widgets_row.append(cell_frame) widgets_matrix.append(widgets_row) return widgets_matrix - def itemClicked(self, item_name): + def onItemClicked(self, item_name): self.trace('In TaurusGrid.itemClicked(%s)' % item_name) self.setItemSelected(item_name) self.itemClicked.emit(str(item_name)) @@ -962,7 +992,6 @@ def getQtDesignerPluginInfo(cls): class Delegate(QtGui.QItemDelegate): - def __init__(self, parent=None): QtGui.QItemDelegate.__init__(self, parent) @@ -990,13 +1019,15 @@ def sysargs_to_dict(defaults=[]): if __name__ == '__main__': import sys + from taurus.qt.qtgui.application import TaurusApplication + if len(sys.argv) < 2: print "The format of the call is something like:" print '\t/usr/bin/python taurusgrid.py grid.pickle.file' print '\t/usr/bin/python taurusgrid.py "model=lt.*/VC.*/.*/((C*)|(P*)|(I*))" cols=IP,CCG,PNV rows=LT01,LT02 others=False rowframe=True colframe=False' exit() - app = QtGui.QApplication(sys.argv[0:1]) + app = TaurusApplication(sys.argv[0:1]) gui = TaurusGrid() try: # first try if argument is a file to be opened diff --git a/lib/taurus/qt/qtgui/taurusgui/macrolistener.py b/lib/taurus/qt/qtgui/taurusgui/macrolistener.py index 4a90225ed..3e430d847 100644 --- a/lib/taurus/qt/qtgui/taurusgui/macrolistener.py +++ b/lib/taurus/qt/qtgui/taurusgui/macrolistener.py @@ -44,8 +44,6 @@ from taurus.core.util.containers import CaselessList from taurus.external.qt import Qt from taurus.qt.qtgui.base import TaurusBaseComponent -from sardana.taurus.core.tango.sardana import PlotType -from sardana.taurus.core.tango.sardana.pool import getChannelConfigs class ChannelFilter(object): @@ -126,6 +124,9 @@ def onExpConfChanged(self, expconf): QDoor.getExperimentDescription` for more details ''' + + from sardana.taurus.core.tango.sardana import PlotType + from sardana.taurus.core.tango.sardana.pool import getChannelConfigs activeMntGrp = expconf['ActiveMntGrp'] if activeMntGrp is None: return diff --git a/lib/taurus/qt/qtgui/tree/taurusdevicetree.py b/lib/taurus/qt/qtgui/tree/taurusdevicetree.py index f1e5840e7..9c57f0e92 100644 --- a/lib/taurus/qt/qtgui/tree/taurusdevicetree.py +++ b/lib/taurus/qt/qtgui/tree/taurusdevicetree.py @@ -37,9 +37,9 @@ import time import os +import re import traceback from functools import partial -import PyTango # to change!! # @todo: icons_dev_tree is not an included or standard module. # Is anybody using it? If not, the following lines should be removed and @@ -54,9 +54,15 @@ import taurus.core from taurus.core.util.colors import DEVICE_STATE_PALETTE, ATTRIBUTE_QUALITY_PALETTE from taurus.core.util.containers import CaselessDict -from taurus.core.tango.search import * # @TODO: Avoid implicit imports +from taurus.core.util.fandango_search import ( + isCallable, isString, split_model_list, isSequence, isMap, + get_matching_devices, matchCl, get_alias_for_device, extend_regexp) from taurus.qt.qtcore.util.emitter import SingletonWorker -from taurus.qt.qtcore.mimetypes import * # @TODO: Avoid implicit imports +from taurus.qt.qtcore.mimetypes import (TAURUS_MODEL_LIST_MIME_TYPE, + TAURUS_DEV_MIME_TYPE, + TAURUS_ATTR_MIME_TYPE, + TAURUS_MODEL_MIME_TYPE + ) from taurus.qt.qtcore.util import properties from taurus.qt.qtcore.util.properties import djoin from taurus.qt.qtgui.base import TaurusBaseComponent, TaurusBaseWidget @@ -212,6 +218,7 @@ def getNodeIcon(self, node=None): def getNodeDraggable(self, node=None): """ This method will return True only if the selected node belongs to a numeric Tango attribute """ + import PyTango # TODO: tango-centric numtypes = [PyTango.DevDouble, PyTango.DevFloat, PyTango.DevLong, PyTango.DevLong64, PyTango.DevULong, PyTango.DevShort, PyTango.DevUShort, PyTango.DevBoolean] if node is None: @@ -310,6 +317,8 @@ class TaurusDevTree(TaurusTreeNodeContainer, Qt.QTreeWidget, TaurusBaseWidget): addModels merges the tree with new models setFilters clears previous models and adds new one ''' + + # TODO: tango-centric __properties__ = ( 'ModelInConfig', 'modifiableByUser', @@ -730,6 +739,7 @@ def addAttrToDev(self, my_device, expert=False, allow_types=None): @argin expert If False only PyTango.DispLevel.OPERATOR attributes are displayed @argin allow_types Only those types included in the list will be displayed (e.g. may be restricted to numeric types only) """ + import PyTango # TODO: tango-centric numeric_types = [PyTango.DevDouble, PyTango.DevFloat, PyTango.DevLong, PyTango.DevLong64, PyTango.DevULong, PyTango.DevShort, PyTango.DevUShort, PyTango.DevBoolean, PyTango.DevState] allow_types = allow_types or [PyTango.DevString] + numeric_types diff --git a/lib/taurus/qt/qtgui/util/taurusaction.py b/lib/taurus/qt/qtgui/util/taurusaction.py index 6df3e6d66..cad0d5158 100644 --- a/lib/taurus/qt/qtgui/util/taurusaction.py +++ b/lib/taurus/qt/qtgui/util/taurusaction.py @@ -125,6 +125,7 @@ def actionTriggered(self, args=None): if any(args): #Qt.QMessageBox.warning(self.parentWidget(),'Warning','In ExternalAppAction(%s)'%args) self._process.append(subprocess.Popen(args)) + self._process = [p for p in self._process if p.poll() is None] return True else: return False diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index f4da96303..20af7d416 100755 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -64,10 +64,33 @@ # Set your default scheme (if not defined, "tango" is assumed) DEFAULT_SCHEME = "tango" +# Filter old tango events: +# Sometimes TangoAttribute can receive an event with an older timestamp +# than its current one. See https://github.com/taurus-org/taurus/issues/216 +# True discards (Tango) events whose timestamp is older than the cached one. +# False (or commented out) for backwards (pre 4.1) compatibility +FILTER_OLD_TANGO_EVENTS = True + # Extra Taurus schemes. You can add a list of modules to be loaded for # providing support to new schemes # EXTRA_SCHEME_MODULES = ['myownschememodule'] +# Custom formatter. Taurus widgets use a default formatter based on the +# attribute type, but sometimes a custom formatter is needed. +# IMPORTANT: setting this option in this file will affect ALL widgets +# of ALL applications (which is probably **not** what you want, since it +# may have unexpected effects in some applications). +# Consider using the API for modifying this on a per-widget or per-class +# basis at runtime, or using the related `--default-formatter` parameter +# from TaurusApplication, e.g.: +# $ taurusform MODEL --default-formatter='{:2.3f}' +# The formatter can be a python format string or the name of a formatter +# callable +# e.g. +# DEFAULT_FORMATTER = '{0}' +# DEFAULT_FORMATTER = 'taurus.core.tango.util.formatter.tangoFormatter' + + # ---------------------------------------------------------------------------- # PLY (lex/yacc) optimization: 1=Active (default) , 0=disabled. # Set PLY_OPTIMIZE = 0 if you are getting yacc exceptions while loading diff --git a/lib/taurus/test/test_import.py b/lib/taurus/test/test_import.py index 215d4b59e..9f575a4a6 100644 --- a/lib/taurus/test/test_import.py +++ b/lib/taurus/test/test_import.py @@ -30,9 +30,9 @@ class TaurusImportTestCase(unittest.TestCase): - ''' + """ Test if all the submodules can be imported - ''' + """ def setUp(self): """Preconditions: moduleexplorer utility has to be available """ @@ -46,7 +46,18 @@ def testImportSubmodules(self): Expected Results: It is expected to get no warning message on module importing """ - exclude_patterns = (r'taurus.qt.qtgui.extra_.*',) + exclude_patterns = [r'taurus\.qt\.qtgui\.extra_.*'] + + try: + import PyTango + except ImportError: + exclude_patterns.append(r'taurus\.core\.tango') + try: + import epics + except ImportError: + exclude_patterns.append(r'taurus\.core\.epics') + + moduleinfo, wrn = self.explore('taurus', verbose=False, exclude_patterns=exclude_patterns) msg = None diff --git a/setup.py b/setup.py index fc7d0a317..804201dd4 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,8 @@ def get_release_info(): provides = [ 'taurus', - 'taurus.core', - 'taurus.qt', + # 'taurus.core', + # 'taurus.qt', # 'Taurus-Tango', # [Taurus-Tango] # 'Taurus-Qt', # [Taurus-Qt] # 'Taurus-Qt-PyQwt', # [Taurus-Qt-Plot] @@ -65,7 +65,6 @@ def get_release_info(): # 'PyQt4.Qwt5 >=5.2.0', # [Taurus-Qt-Plot] 'ply >=2.3', # [Taurus-Qt-Synoptic] 'lxml >=2.1', # [Taurus-Qt-TaurusGUI] - 'spyder >=3.0', # [Taurus-Qt-Editor] 'guiqwt >=3', # [Taurus-Qt-Guiqwt] ], 'taurus-tango': ['PyTango >=7.1', @@ -74,14 +73,14 @@ def get_release_info(): ], 'taurus-h5file': ['h5file', ], + # separate the editor from 'taurus-qt' to avoid forcing spyder>3 + # which implies many more dependencies that may be hard on older system + 'taurus-qt-editor': ['spyder >=3', + ], } console_scripts = [ 'taurustestsuite = taurus.test.testsuite:main', - # TODO: taurusdoc, -] - -gui_scripts = [ 'taurusconfigbrowser = taurus.qt.qtgui.panel.taurusconfigeditor:main', 'taurusplot = taurus.qt.qtgui.plot.taurusplot:main', 'taurustrend = taurus.qt.qtgui.plot.taurustrend:main', @@ -96,11 +95,11 @@ def get_release_info(): 'taurustrend2d = taurus.qt.qtgui.extra_guiqwt.taurustrend2d:taurusTrend2DMain', 'taurusiconcatalog = taurus.qt.qtgui.icon.catalog:main', 'taurusdemo = taurus.qt.qtgui.panel.taurusdemo:main', + # TODO: taurusdoc, ] entry_points = {'console_scripts': console_scripts, - 'gui_scripts': gui_scripts, - } +} classifiers = [ 'Development Status :: 3 - Alpha',