diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 3d90a6c2..df3ff6c0 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -2,7 +2,15 @@ name: Code CI on: push: + branches: + - master + - main + tags: + - "*" pull_request: + schedule: + # Run every Monday at 8am + - cron: '0 8 * * MON' jobs: lint: @@ -30,15 +38,18 @@ jobs: python: [cp36, cp37, cp38, cp39] include: - # Put coverage in the project directory for mac + # Put coverage and results files in the project directory for mac - os: macos-latest cov_file: "{project}/dist/coverage.xml" + results_file: "{project}/dist/pytest-results.xml" # And for windows - os: windows-latest cov_file: "{project}/dist/coverage.xml" - # But put coverage in the output dir mounted in docker for linux + results_file: "{project}/dist/pytest-results.xml" + # But put coverage and results files in the output dir mounted in docker for linux - os: ubuntu-latest cov_file: /output/coverage.xml + results_file: /output/pytest-results.xml # Build an sdist on linux so it has the right line endings - os: ubuntu-latest sdist: true @@ -60,7 +71,8 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. + # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962 + run: pip install build cibuildwheel>=2.3.1 - name: Build Sdist if: matrix.sdist @@ -69,14 +81,17 @@ jobs: - name: Build Wheel run: cibuildwheel --output-dir dist env: + # Force old behaviour of pip - see https://github.com/pypa/cibuildwheel/issues/962 + CIBW_ENVIRONMENT: PIP_USE_DEPRECATED=out-of-tree-build CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev - CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} + CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 - CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux2014_x86_64 + CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 @@ -90,6 +105,30 @@ jobs: name: ${{ matrix.os }}/${{ matrix.python }} directory: dist + - name: Upload Unit Test Results + if: always() + uses: actions/upload-artifact@v2 + with: + name: Unit Test Results (${{ matrix.os }}-${{ matrix.python }}) + path: dist/pytest-results.xml + + publish-test-results: + name: Publish Unit Tests Results + needs: build + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v2 + with: + path: artifacts + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + with: + files: artifacts/**/*.xml + release: needs: [build] runs-on: ubuntu-latest @@ -115,4 +154,4 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.pypi_token }} - run: pipx run twine upload dist/* \ No newline at end of file + run: pipx run twine upload dist/* diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 10e35589..e3866cf4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,12 @@ name: Docs CI on: push: + branches: + # Add more branches here to publish docs from other branches + - master + - main + tags: + - "*" pull_request: jobs: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cdadaac3..d6c29451 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,10 @@ Changed: - `Remove python2 support <../../pull/64>` - `Default DISP to TRUE for all In records <../../pull/74>` +- `Overhaul record value setting/getting <../../pull/60>` + +Added: +- New longStringIn/longStringOut record types 3.2.1_ - 2021-11-25 ------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2606bfb3..a9ba7ece 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -26,6 +26,21 @@ While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage remains the same or is improved by a pull request! + +Updating Dependencies +--------------------- + +pythonSoftIoc has a variety of types of dependencies, from Python module to +C files and built libraries. If it is necessary to update any of them, the +safest way to do that is:: + + $ Ensure all changes are committed/saved/backed up + $ pipenv --rm + $ pipenv run gitclean + $ rm Pipfile.lock + $ Update dependencies in setup.cfg / pyproject.toml + $ pipenv install --dev + Code Styling ------------ diff --git a/Pipfile.lock b/Pipfile.lock index a057c09d..1794c8eb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,10 +16,10 @@ "default": { "aioca": { "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", + "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" ], - "version": "==1.2" + "version": "==1.3" }, "cothread": { "hashes": [ @@ -29,69 +29,78 @@ }, "epicscorelibs": { "hashes": [ - "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", - "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", - "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", - "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", - "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", - "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", - "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", - "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", - "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", - "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", - "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", - "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", - "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", - "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", - "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", - "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", - "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", - "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", - "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", - "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", - "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", - "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", - "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", - "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", - "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" - ], - "version": "==7.0.6.99.1.0" + "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13", + "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995", + "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5", + "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35", + "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313", + "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef", + "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808", + "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b", + "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761", + "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7", + "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11", + "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f", + "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623", + "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6", + "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b", + "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f", + "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439", + "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda", + "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69", + "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05", + "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53", + "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7", + "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328", + "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985", + "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e", + "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67", + "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455", + "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b" + ], + "version": "==7.0.6.99.2.0" }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" - ], - "version": "==1.20.2" + "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb", + "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939", + "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc", + "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced", + "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441", + "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b", + "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166", + "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640", + "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13", + "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8", + "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5", + "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611", + "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76", + "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c", + "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7", + "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a", + "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5", + "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee", + "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449", + "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02", + "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a", + "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815", + "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593", + "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e", + "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa", + "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954", + "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354", + "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9", + "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a", + "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089" + ], + "version": "==1.21.5" }, "scipy": { "hashes": [ @@ -133,20 +142,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], - "version": "==3.10.0.0" + "version": "==4.0.1" } }, "develop": { "aioca": { "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", + "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" ], - "version": "==1.2" + "version": "==1.3" }, "alabaster": { "hashes": [ @@ -171,17 +179,18 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "version": "==4.0.0" + "markers": "python_version >= '3'", + "version": "==2.0.7" }, "cothread": { "hashes": [ @@ -190,119 +199,120 @@ "version": "==2.17" }, "coverage": { - "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" - ], - "version": "==5.5" + "extras": [ + "toml" + ], + "hashes": [ + "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9", + "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b", + "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e", + "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a", + "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2", + "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67", + "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685", + "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f", + "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7", + "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717", + "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5", + "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b", + "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef", + "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489", + "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b", + "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e", + "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926", + "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271", + "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54", + "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026", + "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad", + "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533", + "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6", + "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8", + "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26", + "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b", + "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8", + "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9", + "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271", + "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003", + "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a", + "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0", + "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d", + "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a", + "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0", + "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9", + "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f", + "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33", + "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a", + "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6", + "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201", + "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336", + "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce", + "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4", + "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9", + "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07" + ], + "version": "==6.1.1" }, "docutils": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" ], - "version": "==0.16" + "version": "==0.17.1" }, "epicscorelibs": { "hashes": [ - "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", - "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", - "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", - "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", - "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", - "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", - "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", - "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", - "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", - "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", - "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", - "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", - "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", - "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", - "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", - "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", - "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", - "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", - "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", - "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", - "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", - "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", - "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", - "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", - "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" - ], - "version": "==7.0.6.99.1.0" + "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13", + "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995", + "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5", + "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35", + "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313", + "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef", + "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808", + "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b", + "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761", + "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7", + "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11", + "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f", + "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623", + "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6", + "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b", + "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f", + "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439", + "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda", + "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69", + "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05", + "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53", + "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7", + "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328", + "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985", + "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e", + "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67", + "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455", + "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b" + ], + "version": "==7.0.6.99.2.0" }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "flake8": { "hashes": [ - "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", - "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" ], - "version": "==3.9.1" + "version": "==4.0.1" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.3" }, "imagesize": { "hashes": [ @@ -313,11 +323,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb", - "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], "markers": "python_version < '3.8' and python_version < '3.8'", - "version": "==4.6.0" + "version": "==4.0.1" }, "iniconfig": { "hashes": [ @@ -328,67 +338,69 @@ }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", + "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" ], - "version": "==2.11.3" + "version": "==3.0.2" }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -397,86 +409,91 @@ ], "version": "==0.6.1" }, - "nose": { + "nose2": { "hashes": [ - "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", - "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", - "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + "sha256:886ba617a96de0130c54b24479bd5c2d74d5c940d40f3809c3a275511a0c4a60", + "sha256:aa620e759f2c5018d9ba041340391913e282ecebd3c392027f1575847b093ec6" ], - "version": "==1.3.7" + "version": "==0.10.0" }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" - ], - "version": "==1.20.2" + "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb", + "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939", + "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc", + "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced", + "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441", + "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b", + "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166", + "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640", + "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13", + "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8", + "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5", + "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611", + "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76", + "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c", + "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7", + "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a", + "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5", + "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee", + "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449", + "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02", + "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a", + "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815", + "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593", + "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e", + "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa", + "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954", + "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354", + "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9", + "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a", + "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089" + ], + "version": "==1.21.5" }, "p4p": { "hashes": [ - "sha256:0143a3a3d29bf03f7c828c28d025db81961e9933c750bd9d3d8e8b59cb8b71a3", - "sha256:160aa3dd5ca55583e929bcc939e0479dfca655fba440dc9b931373e733848959", - "sha256:1c688e35e30294c79bfebbda8c460ffb5486cdb64ccbeae93060e6de667942a4", - "sha256:1d58c9149b85e0fd19e00167fb047a04c1e649d394cf8fdcb7a172be73de3f6d", - "sha256:25e98f73ae4cebc3c305f2b11b0e6689d002594fda9330bf99c6414d0a01753f", - "sha256:2b4e193737cb69a20fbed2f86360275ce5dcce5ca83439e1f98b3af5f1aed1cc", - "sha256:30d0e083b5dfecee7693d197125daac3e5da9466e50dea0d01d5356abf5c6143", - "sha256:34ab524b4ce32dc67ef33421db644d6713ed677f601cbbef7007957f9cb6eb9c", - "sha256:3e257761347bf14e66f7a07bb4fda803c1293c3ca8b26baf5818ad66e2d46abb", - "sha256:3ec002d4e04ea5c99b33840f71c1042c33d4b13b20e32980b7a0a7eb71195ec4", - "sha256:79bd0bcb586d043a3565238be2f002ddf125bb710b4a46b330632a706d36ba08", - "sha256:7a004c518ba6abbb4ff96647e5ced0e1329d965448cbdc23709a8b6a3d040b26", - "sha256:977dc659a16481603716c655b7cae406e79d7788bd387b64994c198d3a27e19f", - "sha256:99c9dc61af4e8323bb77418d0c868717a838d424081156ac31b95957965f901f", - "sha256:a922287fd85973bbc900f629f5d475fe9be37eb2d3929fe81405c3ebcd950807", - "sha256:bf58b00da603378027cb1af265d0d0ccfa40cdc02263bdebbb4f56837d809512", - "sha256:c20592bb15a087add723d84484d95a9e428045a20afc7a791ccdad3e48113a39", - "sha256:c46c615896fa0daea5a5942c4b7b51be2bf3f8dfdcc2c05e256f2c957e3353eb", - "sha256:c745d18146bf48762806d724b8f265c046196ddb0a1dd15c7aee0919362cbb63", - "sha256:c9d5769c31b2700ece9c04aa8c0fc2efe95af8d473b35307d03210ff31d28831", - "sha256:cfd500cfa8d3f227704e8f4ff6bafee66bc5459fc56ece7792c334b92be2d35a", - "sha256:dda7008629e8232a372b798197dcebc5583eadc0ed643568543ee4afd0387928", - "sha256:f014907d2fd51ce132a29b1e869ea4058ead0cc14bcc2ed1690a08752c72ba62", - "sha256:f4677018469c7b98434dd465f1210f2ebd13a23d7084187666393a45b2ff7f26", - "sha256:f4ce7f6dd0e45f1acc05e0a633b3e18045b576c43394790719dcafb71a5daa13" - ], - "version": "==3.5.4" + "sha256:06c95f97b531e6dee05cf1080192c0310b72bcc6bb1a5da5bbe0295567abd1ae", + "sha256:0a817f4385c1d119f68bf701b4a73fdad7a273dfa7c7825db63930eda64ce8e7", + "sha256:0fc35bb3ad6b8e2c48d5f94ca1a18d60a763993384d03204d814ac63b7aa7ee1", + "sha256:1779a995e07ba561b8bb0b9474b9043861fc304c19afdcfc552fb758b783277f", + "sha256:178bd8e66b6771be6d270230c8169420ed05c5fb506fb05fc1983b2db469119b", + "sha256:270966fb4ecbf532a09c375c23404d171bb2d05169c7f2d00aa74d791091c21f", + "sha256:2951deb6d551d1a91535a7eaf91962834404218b97cfd5439b22ed9c71dd1214", + "sha256:325f888902e03a443090bd19fd8bd3f8a064989a9cdab0389fb60d6b461a189d", + "sha256:38c849f27d0acd042113cbde4d4d9682b1c45c62ca9c569201e70f128268b5d2", + "sha256:3950bef6486b2cf5690a7f0efbea233540582ebb5ac72736ff76e55048b5bed0", + "sha256:3da53fcc002f2d7b855f7fa075f74cba3fbbeb41f0a81958f45559334440819b", + "sha256:7466c57f66ba39f5c6c17425b0003d5eda2be98ddc5342aca99c02986a0bf185", + "sha256:84dcd9ca598665069bfdb3df1bef3fce98ea1a7ca5ef377cc6f55b1391daec1d", + "sha256:8f7e0577628c15e10ffd8da21e3699391bf5a25a28c5a531bccf614618bbfe3d", + "sha256:98325753e7d38dac9e0ad643814e37162e451c55bf9cd3ff3c5b94c2e6aff1df", + "sha256:9dc3cb1119efd1c29cf5e7a20ecd17dc184fdf66fa7b39e87d10344ecb8001fd", + "sha256:a4f65b20fccc77608591c90675bcc22aaaf128f20b9a648aea00afd592bccfe9", + "sha256:a5fddf5eedfed9d577f651ad53947d31738d893a485e3981ce710ca7c63ab547", + "sha256:b338048f1e96f4c3de6d58a668eeab3667276879db5589838118c174272358bd", + "sha256:b3dd72c29d1a89e9f96de0b3d67fc2dd824acfb026f3ff1603a024d229f7f404", + "sha256:bae9918e7793198b20507cc4a7b4ec4428c51ee33b28db96232ac178ea12400f", + "sha256:d714d0bf14ed9e0ca7cc5e9d4e6cb390c1ddcc3d8b7aeff23c09b0b3f6635e2c", + "sha256:d72f2951878c53e2f3f2e349bc8788f3e11a980e1f044f9c69153494318b340a", + "sha256:e69211b5f9e6aa93cf552491144aec561c3cdf6bf59c1f4013329556f13461f0", + "sha256:fd787fa25b5536d054bebc5e15f80da1948d0d6e9fe83848050eb557f5d35fc8" + ], + "version": "==3.5.5" }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" ], - "version": "==20.9" + "version": "==21.2" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.13.1" + "version": "==1.0.0" }, "ply": { "hashes": [ @@ -494,24 +511,24 @@ }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" ], - "version": "==2.7.0" + "version": "==2.8.0" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" ], - "version": "==2.3.1" + "version": "==2.4.0" }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], - "version": "==2.9.0" + "version": "==2.10.0" }, "pyparsing": { "hashes": [ @@ -522,25 +539,24 @@ }, "pytest": { "hashes": [ - "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", - "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], - "version": "==6.2.3" + "version": "==6.2.5" }, "pytest-asyncio": { "hashes": [ - "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", - "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" + "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4", + "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d" ], - "markers": "python_version > '3.0'", - "version": "==0.14.0" + "version": "==0.17.2" }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], - "version": "==2.11.1" + "version": "==3.0.0" }, "pytest-flake8": { "hashes": [ @@ -551,17 +567,17 @@ }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" ], - "version": "==2021.1" + "version": "==2021.3" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "version": "==2.25.1" + "version": "==2.26.0" }, "setuptools-dso": { "hashes": [ @@ -570,6 +586,13 @@ ], "version": "==2.1" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "version": "==1.16.0" + }, "snowballstemmer": { "hashes": [ "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", @@ -586,25 +609,24 @@ }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c", + "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851" ], - "version": "==4.1.2" + "version": "==4.3.2" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", - "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", + "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" ], - "version": "==0.5.2" + "version": "==1.0.0" }, "sphinx-rtd-theme-github-versions": { "hashes": [ - "sha256:ad2822c08b0f075c051e25b9d291152d846c21ed052db3b458d5ef30efac59b3", - "sha256:f84b45baf536c01c2e62f26e8206dd69f0375d9c9b3769d56295292e6e00f43b" + "sha256:0df27ae9a9cd902468c808dbee5a43f4db8ce43cbcf2ecc78d2fe47698bb0ded", + "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07" ], - "markers": "python_version > '3.0'", - "version": "==0.2" + "version": "==1.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -655,27 +677,33 @@ ], "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + ], + "version": "==1.2.2" + }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], - "version": "==3.10.0.0" + "version": "==4.0.1" }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], - "version": "==1.26.5" + "version": "==1.26.7" }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], - "version": "==3.4.1" + "version": "==3.6.0" } } } diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 7efab9e0..20eb6de3 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -267,7 +267,7 @@ All functions return a wrapped `ProcessDeviceSupportIn` or mbbOut(name, *options, **fields) Create ``mbbi`` and ``mbbo`` records. Up to 16 options can be specified as - either an option name or a tuple of tw fields. The name or first field of + either an option name or a tuple of two fields. The name or first field of the tuple names the option, and the second optional field is the option severity. For example:: @@ -308,13 +308,28 @@ All functions return a wrapped `ProcessDeviceSupportIn` or if given, or defaults to ``'FLOAT'``. -The following function generates a specialised record. +The following functions generates specialised records. .. function:: Action(name, **fields) Creates a record (using `boolOut`) which will always call the `on_update` - method when processed. Used for action records. The `on_update` keyword - should always be passed. + method when processed. Used for action records. Either the `on_update` or + `on_update_name` keyword should always be passed. + +.. function:: + longStringIn(name, **fields) + longStringOut(name, **fields) + + Creates a Waveform record specialised to contain Python (Unicode) strings. + Only accepts string values, and only returns string values. `initial_value` + and ``length`` keywords accepted as per Waveform records. + + If no ``length`` or `initial_value` is provided a default length of 256 is used. + + .. note:: + If you wish to store byte strings, use a ``WaveformIn/Out`` record + + The following functions manage record names. The record device name must be @@ -328,8 +343,7 @@ record creation function. Sets up the prefix part of the record name, referred to here as the "device" part. This function must be called before creating any records. Note that - only this function need be used, the three other functions below are - entirely optional. + only this function need be used, function below is entirely optional. .. function:: UnsetDevice() diff --git a/softioc/__init__.py b/softioc/__init__.py index b9c186b0..d826e87d 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -18,10 +18,10 @@ # Need to do this before calling anything in device.py iocshRegisterCommon() for dbd in ('base.dbd', 'PVAServerRegister.dbd', 'qsrv.dbd'): - dbLoadDatabase(dbd, os.path.join(path.base_path, "dbd"), None) -dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None) + dbLoadDatabase(dbd, os.path.join(path.base_path, 'dbd'), None) +dbLoadDatabase('devIocStats.dbd', os.path.dirname(__file__), None) if registerRecordDeviceDriver(pdbbase): raise RuntimeError('Error registering') -__all__ = ["__version__"] +__all__ = ['__version__'] diff --git a/softioc/builder.py b/softioc/builder.py index a7c7bd1b..679c4c6c 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -7,7 +7,8 @@ InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) -from . import pythonSoftIoc # noqa +from . import device, pythonSoftIoc # noqa + PythonDevice = pythonSoftIoc.PythonDevice() @@ -64,7 +65,7 @@ def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields): 'EI', 'NI', 'TE', 'EL', 'TV', 'TT', 'FT', 'FF'] # 8-15 # All the severity strings supported by SV -_severityStrings = ["NO_ALARM", "MINOR", "MAJOR", "INVALID"] +_severityStrings = ['NO_ALARM', 'MINOR', 'MAJOR', 'INVALID'] # Converts a list of (option [,severity]) values or tuples into field settings # suitable for mbbi and mbbo records. @@ -78,7 +79,7 @@ def process_value(prefix, value, option, severity=None): severity = _severityStrings[severity] fields[prefix + 'SV'] = severity # zip() silently ignores extra values in options, so explicitly check length - assert len(options) <= 16, "May not specify more than 16 enum values" + assert len(options) <= 16, 'May not specify more than 16 enum values' for prefix, (value, option) in zip(_mbbPrefixes, enumerate(options)): if isinstance(option, tuple): # The option is tuple consisting of the option name and an optional @@ -108,31 +109,28 @@ def Action(name, **fields): return boolOut(name, always_update = True, **fields) -# Converts numpy character code to FTVL value. -NumpyCharCodeToFtvl = { - # The following type codes are supported directly: - 'B': 'UCHAR', # ubyte - 'b': 'CHAR', # byte - 'H': 'USHORT', # ushort - 'h': 'SHORT', # short - 'i': 'LONG', # intc - 'L': 'ULONG', # uint - 'l': 'LONG', # int_ - 'f': 'FLOAT', # single - 'd': 'DOUBLE', # float_ - 'S': 'STRING', # str_ - - # The following type codes are weakly supported by pretending that - # they're related types. - '?': 'CHAR', # bool_ - 'p': 'LONG', # intp - 'I': 'ULONG', # uintc - 'P': 'ULONG', # uintp - - # The following type codes are not supported at all: - # q longlong Q ulonglong g longfloat - # F csingle D complex_ G clongfloat - # O object_ U unicode_ V void +# Converts numpy dtype name to FTVL value. +NumpyDtypeToDbf = { + 'int8': 'CHAR', + 'uint8': 'UCHAR', + 'int16': 'SHORT', + 'uint16': 'USHORT', + 'int32': 'LONG', + 'uint32': 'ULONG', + 'float32': 'FLOAT', + 'float64': 'DOUBLE', +} + +# Coverts FTVL string to numpy type +DbfStringToNumpy = { + 'CHAR': 'int8', + 'UCHAR': 'uint8', + 'SHORT': 'int16', + 'USHORT': 'uint16', + 'LONG': 'int32', + 'ULONG': 'uint32', + 'FLOAT': 'float32', + 'DOUBLE': 'float64', } @@ -146,29 +144,49 @@ def _waveform(value, fields): assert not value, 'Can\'t specify initial value twice!' value = (fields.pop('initial_value'),) + # Datatype can be specified as keyword argument, taken from FTVL, or derived + # from the initial value + if 'datatype' in fields: + assert 'FTVL' not in fields, \ + 'Can\'t specify FTVL and datatype together' + datatype = fields.pop('datatype') + if datatype == int or datatype == 'int': + # Convert Python int to 32-bit integer, it's all we can handle + datatype = numpy.dtype('int32') + else: + datatype = numpy.dtype(datatype) + elif 'FTVL' in fields: + datatype = numpy.dtype(DbfStringToNumpy[fields['FTVL']]) + else: + # No datatype specified, will have to infer from initial value + datatype = None + if value: - # If a value is specified it should be the *only* non keyword - # argument. + # If a value is specified it should be the *only* non keyword argument. value, = value - value = numpy.array(value) - fields['initial_value'] = value - - # Pick up default length and datatype from initial value - length = len(value) - FTVL = NumpyCharCodeToFtvl[value.dtype.char] + initial_value = device._require_waveform(value, datatype) + length = fields.pop('length', len(initial_value)) + + # Special case for [u]int64: if the initial value comes in as 64 bit + # integers we cannot represent that, so recast it as [u]int32 + if datatype is None: + if initial_value.dtype == numpy.int64: + initial_value = numpy.require(initial_value, numpy.int32) + elif initial_value.dtype == numpy.uint64: + initial_value = numpy.require(initial_value, numpy.uint32) else: - # No value specified, so require length and datatype to be specified. + initial_value = numpy.array([], dtype = datatype) length = fields.pop('length') - FTVL = 'FLOAT' + datatype = initial_value.dtype - datatype = fields.pop('datatype', None) - if datatype is not None: - assert 'FTVL' not in fields, \ - 'Can\'t specify FTVL and datatype together' - FTVL = NumpyCharCodeToFtvl[numpy.dtype(datatype).char] + assert length > 0, 'Array cannot be of zero length' + + fields['initial_value'] = initial_value + fields['_wf_nelm'] = length + fields['_wf_dtype'] = datatype fields['NELM'] = length - fields.setdefault('FTVL', FTVL) + fields['FTVL'] = NumpyDtypeToDbf[datatype.name] def Waveform(name, *value, **fields): @@ -182,6 +200,32 @@ def WaveformOut(name, *value, **fields): return PythonDevice.waveform_out(name, **fields) +def _long_string(fields): + if 'length' in fields: + length = fields.pop('length') + elif 'initial_value' in fields: + length = len(fields['initial_value'].encode(errors = 'replace')) + 1 + else: + # Default length of 256 + length = 256 + + fields.setdefault('initial_value', '') + fields['_wf_nelm'] = length + fields['_wf_dtype'] = numpy.dtype('uint8') + + fields['NELM'] = length + fields['FTVL'] = 'UCHAR' + + +def longStringIn(name, **fields): + _long_string(fields) + return _in_record('long_stringin', name, **fields) + +def longStringOut(name, **fields): + _long_string(fields) + return PythonDevice.long_stringout(name, **fields) + + # ---------------------------------------------------------------------------- # Support routines for builder @@ -227,6 +271,7 @@ def UnsetDevice(): 'stringIn', 'stringOut', 'mbbIn', 'mbbOut', 'Waveform', 'WaveformOut', + 'longStringIn', 'longStringOut', 'Action', # Other builder support functions 'LoadDatabase', diff --git a/softioc/device.dbd b/softioc/device.dbd index 6c01d590..766d508e 100644 --- a/softioc/device.dbd +++ b/softioc/device.dbd @@ -10,5 +10,7 @@ device(waveform, INST_IO, devPython_waveform, "Python") device(mbbi, INST_IO, devPython_mbbi, "Python") device(mbbo, INST_IO, devPython_mbbo, "Python") -# Waveforms as "out" devices need special treatment. +# A variety of specialised versions of waveform need special treatment. device(waveform, INST_IO, devPython_waveform_out, "PythonWfOut") +device(waveform, INST_IO, devPython_long_stringin, "PythonLongStringIn") +device(waveform, INST_IO, devPython_long_stringout, "PythonLongStringOut") diff --git a/softioc/device.py b/softioc/device.py index 7d125345..3ff8d254 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -1,11 +1,11 @@ import os import time -import inspect +import ctypes from ctypes import * import numpy from . import alarm -from .fields import DbfCodeToNumpy, DbrToDbfCode +from . import fields from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field from .device_core import DeviceSupportCore, RecordLookup @@ -33,19 +33,46 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): # For some record types we want to return a different return code either # from record init or processing - _epics_rc = EPICS_OK + _epics_rc_ = EPICS_OK - # Default implementations of read and write, overwritten where necessary. + + # Most subclasses (all except waveforms) define a ctypes constructor for the + # underlying EPICS compatible value. + def _value_to_epics(self, value): + return self._ctype_(value) + + def _epics_to_value(self, epics): + return epics.value + + def _default_value(self): + return self._ctype_() + + def _compare_values(self, value1, value2): + return value1.value == value2.value + + + # This method is called during Out record processing to return the + # underlying value in EPICS format. def _read_value(self, record): - return getattr(record, 'VAL') + return record.read_val() + + # This method is called during In record processing to update the + # underlying value (the value must be in EPICS compatible format). This is + # also called during Out record initialisation and value reversion when + # required. def _write_value(self, record, value): - setattr(record, 'VAL', value) + record.write_val(value) class ProcessDeviceSupportIn(ProcessDeviceSupportCore): _link_ = 'INP' def __init__(self, name, **kargs): + if 'initial_value' in kargs: + value = self._value_to_epics(kargs.pop('initial_value')) + else: + value = self._default_value() + # We implement update locking via a simple trick which relies on the # Python global interpreter lock: this ensures that assigning or # reading a single value is atomic. We therefore cluster all our @@ -53,28 +80,26 @@ def __init__(self, name, **kargs): # to be processed. # The tuple contains everything needed to be written: the value, # severity, alarm and optional timestamp. - self._value = ( - kargs.pop('initial_value', self._default_), - alarm.NO_ALARM, alarm.UDF_ALARM, None) + self._value = (value, alarm.NO_ALARM, alarm.UDF_ALARM, None) self.__super.__init__(name, **kargs) - def _process(self, record, _value=None): + def _process(self, record): # For input process we copy the value stored in the instance to the # record. The alarm status is also updated, and a custom timestamp # can also be set. - if _value is None: - _value = self._value - value, severity, alarm, timestamp = _value + value, severity, alarm, timestamp = self._value self._write_value(record, value) self.process_severity(record, severity, alarm) if timestamp is not None: record.TIME = timestamp - return self._epics_rc + record.UDF = 0 + return self._epics_rc_ def set(self, value, severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): '''Updates the stored value and triggers an update. The alarm severity and timestamp can also be specified if appropriate.''' + value = self._value_to_epics(value) self._value = (value, severity, alarm, timestamp) self.trigger() @@ -86,7 +111,7 @@ def set_alarm(self, severity, alarm, timestamp=None): def get(self): '''Returns the last written value.''' - return self._value[0] + return self._epics_to_value(self._value[0]) class ProcessDeviceSupportOut(ProcessDeviceSupportCore): @@ -107,101 +132,65 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = kargs.pop('initial_value', None) self.__enable_write = True + + if 'initial_value' in kargs: + self._value = self._value_to_epics(kargs.pop('initial_value')) + else: + self._value = None + self.__super.__init__(name, **kargs) def init_record(self, record): '''Special record initialisation for out records only: implements special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' - if self._value is not None: + if self._value is None: + # Cannot set in __init__ (like we do for In records), as we want + # the record alarm status to be set if no value was provided + # Probably related to PythonSoftIOC issue #53 + self._value = self._default_value() + else: self._write_value(record, self._value) if 'MLST' in self._fields_: record.MLST = self._value record.TIME = time.time() record.UDF = 0 recGblResetAlarms(record) - return self._epics_rc + return self._epics_rc_ def _process(self, record): '''Processing suitable for output records. Performs immediate value validation and asynchronous update notification.''' value = self._read_value(record) - if numpy.all(value == self._value) and not self.__always_update: + if not self.__always_update and \ + self._compare_values(value, self._value): # If the value isn't making a change then don't do anything. return EPICS_OK + + python_value = self._epics_to_value(value) if self.__enable_write and self.__validate and \ - not self.__validate(self, value): - # Asynchronous validation rejects value. It's up to the - # validation routine to do any logging. In this case restore the - # last good value. - if self._value is not None: - self._write_value(record, self._value) + not self.__validate(self, python_value): + # Asynchronous validation rejects value, so restore the last good + # value. + self._write_value(record, self._value) return EPICS_ERROR + else: + # Value is good. Hang onto it, let users know the value has changed + self._value = value + record.UDF = 0 + if self.__on_update and self.__enable_write: + dispatcher(self.__on_update, python_value) + return EPICS_OK - self._value = value - if self.__on_update and self.__enable_write: - dispatcher(self.__on_update, value) - return EPICS_OK - - - NumpyCharCodeToDbr = { - # The following type codes are supported directly: - 'S': 0, # DBR_STRING str_ - 'h': 1, # DBR_SHORT short = int16 - 'f': 2, # DBR_FLOAT single = float32 - 'b': 4, # DBR_CHAR byte = int8 - 'i': 5, # DBR_LONG intc = int32 - 'd': 6, # DBR_DOUBLE float_ = float64 - # These are supported as related types - 'H': 1, # DBR_SHORT ushort = uint16 - '?': 4, # DBR_CHAR bool_ - 'B': 4, # DBR_CHAR ubyte = uint8 - 'I': 5, # DBR_LONG uintc = uint32 - } - if numpy.int_().itemsize == 4: - NumpyCharCodeToDbr.update({'l': 5, 'L': 5}) # int_, uint - - # Converts a Python value into a form suitable for sending over channel - # access. Derived from the corresponding cothread implementation. Returns - # computed dbrcode, waveform length, raw data pointer and pointer to - # underlying data (for lifetime management). - def value_to_dbr(self, value): - # First convert the data directly into an array. This will help in - # subsequent processing: this does most of the type coercion. - value = numpy.require(value, requirements = 'C') - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t put multidimensional arrays!' - - if value.dtype.char == 'S': - # Need special processing to hack the array so that strings are - # actually 40 characters long. - new_value = numpy.empty(value.shape, 'S40') - new_value[:] = value - value = new_value - try: - dbrtype = self.NumpyCharCodeToDbr[value.dtype.char] - except KeyError: - # One more special case. caput() of a list of integers on a 64-bit - # system will fail at this point because they were automatically - # converted to 64-bit integers. Catch this special case and fix it - # up by silently converting to 32-bit integers. Not really the - # right thing to do (as data can be quietly lost), but the - # alternative isn't nice to use either. - if value.dtype.char == 'l': - value = numpy.require(value, dtype = numpy.int32) - dbrtype = 5 - else: - raise - - return dbrtype, len(value), value.ctypes.data, value + def _value_to_dbr(self, value): + return self._dbf_type_, 1, addressof(value), value def set(self, value, process=True): '''Special routine to set the value directly.''' + value = self._value_to_epics(value) try: _record = self._record except AttributeError: @@ -209,24 +198,30 @@ def set(self, value, process=True): # initialisation occurs self._value = value else: - datatype, length, data, array = self.value_to_dbr(value) + # The array parameter is used to keep the raw pointer alive + dbf_code, length, data, array = self._value_to_dbr(value) self.__enable_write = process - db_put_field( - _record.NAME, DbrToDbfCode[datatype], data, length) + db_put_field(_record.NAME, dbf_code, data, length) self.__enable_write = True def get(self): - return self._value + if self._value is None: + # Before startup complete if no value set return default value + value = self._default_value() + else: + value = self._value + return self._epics_to_value(value) -def _Device(Base, record_type, mlst=False, default=0, convert=True): +def _Device(Base, record_type, ctype, dbf_type, epics_rc, mlst = False): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type _device_name_ = 'devPython_' + record_type - _default_ = default _fields_ = ['UDF', 'VAL'] - _epics_rc = EPICS_OK if convert else NO_CONVERT + _epics_rc_ = epics_rc + _ctype_ = staticmethod(ctype) + _dbf_type_ = dbf_type if mlst: _fields_.append('MLST') @@ -236,20 +231,59 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(type, **kargs): - return _Device(_In, type, **kargs) +def _Device_In(*args, **kargs): + return _Device(_In, mlst = False, *args, **kargs) + +def _Device_Out(*args, **kargs): + return _Device(_Out, mlst = True, *args, **kargs) + +longin = _Device_In('longin', c_int32, fields.DBF_LONG, EPICS_OK) +longout = _Device_Out('longout', c_int32, fields.DBF_LONG, EPICS_OK) +bi = _Device_In('bi', c_uint16, fields.DBF_CHAR, NO_CONVERT) +bo = _Device_Out('bo', c_uint16, fields.DBF_CHAR, NO_CONVERT) +mbbi = _Device_In('mbbi', c_uint16, fields.DBF_SHORT, NO_CONVERT) +mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT) + + +def _string_at(value, count): + # Need string_at() twice to ensure string is size limited *and* null + # terminated. + value = ctypes.string_at(ctypes.string_at(value, count)) + # Convert bytes to unicode string + return value.decode(errors = 'replace') + +class EpicsString: + _fields_ = ['UDF', 'VAL'] + _epics_rc_ = EPICS_OK + _ctype_ = c_char * 40 + _dbf_type_ = fields.DBF_STRING + + def _value_to_epics(self, value): + # It's a little odd: we can't simply construct a value from the byte + # string, but we can update the array in an existing value. + # Value being written must be a string, and will be automatically null + # terminated where possible. + result = self._ctype_() + result.value = value.encode() + return result + + def _epics_to_value(self, epics): + return _string_at(epics, 40) + + def _read_value(self, record): + # For strings we need to take a copy of the value read + result = self._ctype_() + result.value = record.read_val().value + return result + -def _Device_Out(type, convert=True, mlst=True): - return _Device(_Out, type, convert=convert, mlst=mlst, default=None) +class stringin(EpicsString, ProcessDeviceSupportIn): + _record_type_ = 'stringin' + _device_name_ = 'devPython_stringin' -longin = _Device_In('longin') -longout = _Device_Out('longout') -bi = _Device_In('bi', convert=False) -bo = _Device_Out('bo', convert=False) -stringin = _Device_In('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False) -mbbi = _Device_In('mbbi', convert=False) -mbbo = _Device_Out('mbbo', convert=False) +class stringout(EpicsString, ProcessDeviceSupportOut): + _record_type_ = 'stringout' + _device_name_ = 'devPython_stringout' dset_process_linconv = ( @@ -263,26 +297,37 @@ def _Device_Out(type, convert=True, mlst=True): class ai(ProcessDeviceSupportIn): _record_type_ = 'ai' _device_name_ = 'devPython_ai' - _default_ = 0.0 _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv - _epics_rc = NO_CONVERT + _epics_rc_ = NO_CONVERT + _ctype_ = c_double + _dbf_type_ = fields.DBF_DOUBLE def _process(self, record): - _value = self._value - self.__super._process(record, _value) # Because we're returning NO_CONVERT we need to do the .UDF updating # ourself (otherwise the record support layer does this). - record.UDF = int(numpy.isnan(_value[0])) - return NO_CONVERT + record.UDF = int(numpy.isnan(self._value[0])) + return self.__super._process(record) class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv - _epics_rc = NO_CONVERT + _epics_rc_ = NO_CONVERT + _ctype_ = c_double + _dbf_type_ = fields.DBF_DOUBLE + +def _require_waveform(value, dtype): + if isinstance(value, bytes): + # Special case hack for byte arrays. Surprisingly tricky: + value = numpy.frombuffer(value, dtype = numpy.uint8) + value = numpy.require(value, dtype = dtype) + if value.shape == (): + value.shape = (1,) + assert value.ndim == 1, 'Can\'t write multidimensional arrays' + return value class WaveformBase(ProcessDeviceSupportCore): @@ -293,70 +338,88 @@ class WaveformBase(ProcessDeviceSupportCore): # NELM Length of allocated array in number of elements # NORD Currently reported length of array (0 <= NORD <= NELM) _fields_ = ['UDF', 'FTVL', 'BPTR', 'NELM', 'NORD'] - # Allow set() to be called before init_record: - dtype = None + + + def __init__(self, name, _wf_nelm, _wf_dtype, **kargs): + self._dtype = _wf_dtype + self._nelm = _wf_nelm + self.__super.__init__(name, **kargs) def init_record(self, record): - self.dtype = DbfCodeToNumpy[record.FTVL] + self._dbf_type_ = record.FTVL return self.__super.init_record(record) def _read_value(self, record): nord = record.NORD - result = numpy.empty(nord, dtype = self.dtype) + result = numpy.empty(nord, dtype = self._dtype) memmove( result.ctypes.data_as(c_void_p), record.BPTR, - self.dtype.itemsize * nord) + self._dtype.itemsize * nord) return result def _write_value(self, record, value): - value = numpy.require(value, dtype = self.dtype) - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t write multidimensional arrays' - - nelm = record.NELM nord = len(value) - if nord > nelm: - nord = nelm memmove( record.BPTR, value.ctypes.data_as(c_void_p), - self.dtype.itemsize * nord) + self._dtype.itemsize * nord) record.NORD = nord + def _compare_values(self, value, other): + return numpy.array_equal(value, other) + + def _value_to_epics(self, value): + # Ensure we always convert incoming value into numpy array, regardless + # of whether the record has been initialised or not + value = _require_waveform(value, self._dtype) + # Because arrays are mutable values it's ever so easy to accidentially + # call set() with a value which subsequently changes. To avoid this + # common class of bug, at the cost of duplicated code and data, here we + # ensure a copy is taken of the value. + assert len(value) <= self._nelm, 'Value too long for waveform' + return +value + + def _epics_to_value(self, value): + return value + + def _value_to_dbr(self, value): + return self._dbf_type_, len(value), value.ctypes.data, value + class waveform(WaveformBase, ProcessDeviceSupportIn): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform' - _default_ = () - # Because arrays are mutable values it's ever so easy to accidentially call - # set() with a value which subsequently changes. To avoid this common class - # of bug, at the cost of duplicated code and data, here we ensure a copy is - # taken of the value. - def set(self, value, - severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): - '''Updates the stored value and triggers an update. The alarm - severity and timestamp can also be specified if appropriate.''' - if isinstance(value, str): - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.fromstring(value + '\0', dtype = 'uint8') - - value = numpy.require(value, dtype = self.dtype) - self._value = (+value, severity, alarm, timestamp) - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t write multidimensional arrays' +class waveform_out(WaveformBase, ProcessDeviceSupportOut): + _record_type_ = 'waveform' + _device_name_ = 'devPython_waveform_out' - self.trigger() +class LongStringBase(WaveformBase): + _dtype = numpy.dtype('uint8') -class waveform_out(WaveformBase, ProcessDeviceSupportOut): + def _value_to_epics(self, value): + value = value.encode(errors = 'replace') + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. + value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) + # Ensure string isn't too long to fit into waveform + assert len(value) <= self._nelm, 'Value too long for waveform' + return value + + def _epics_to_value(self, value): + return _string_at(value.ctypes, len(value)) + + +class long_stringin(LongStringBase, ProcessDeviceSupportIn): _record_type_ = 'waveform' - _device_name_ = 'devPython_waveform_out' + _device_name_ = 'devPython_long_stringin' + +class long_stringout(LongStringBase, ProcessDeviceSupportOut): + _record_type_ = 'waveform' + _device_name_ = 'devPython_long_stringout' # Ensure the .dbd file is loaded. -dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) +dbLoadDatabase('device.dbd', os.path.dirname(__file__), None) diff --git a/softioc/fields.py b/softioc/fields.py index d44dfb69..63fa476e 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -26,6 +26,7 @@ # downstream for no good purpose -- for example, enums are of type DBF_ULONG, # but this cannot be written with caput.) DbfCodeToCtypes = { + DBF_STRING: c_char * 40, DBF_CHAR: c_byte, DBF_UCHAR: c_ubyte, DBF_SHORT: c_int16, @@ -41,19 +42,6 @@ DBF_NOACCESS: c_void_p, } -# Mapping for record field type to numpy type. -DbfCodeToNumpy = { - DBF_STRING: numpy.dtype('S40'), - DBF_CHAR: numpy.dtype('int8'), - DBF_UCHAR: numpy.dtype('uint8'), - DBF_SHORT: numpy.dtype('int16'), - DBF_USHORT: numpy.dtype('uint16'), - DBF_LONG: numpy.dtype('int32'), - DBF_ULONG: numpy.dtype('uint32'), - DBF_FLOAT: numpy.dtype('float32'), - DBF_DOUBLE: numpy.dtype('float64'), -} - # Mapping from basic DBR_ codes to DBF_ values DbrToDbfCode = { DBR_STRING: DBF_STRING, @@ -73,7 +61,7 @@ def __init__(self, record_type, fields): self.fields = get_field_offsets(record_type) missing = set(fields) - set(self.fields) assert not missing, \ - "Fields not supported by %s: %s" % (record_type, sorted(missing)) + 'Fields not supported by %s: %s' % (record_type, sorted(missing)) def __call__(self, record): '''Converts a raw pointer to a record structure into a _Record object @@ -98,9 +86,11 @@ def __getattr__(self, field): if field == 'TIME': return self.__get_time(address) elif field_type == DBF_STRING: - return string_at(cast(address, c_char_p)).decode() + raw_string = string_at(cast(address, c_char_p)) + return raw_string.decode(errors = 'replace') elif field_type in [DBF_INLINK, DBF_OUTLINK]: - return cast(address, POINTER(c_char_p))[0].decode() + raw_string = cast(address, POINTER(c_char_p))[0] + return raw_string.decode(errors = 'replace') else: ctypes_type = DbfCodeToCtypes[field_type] return cast(address, POINTER(ctypes_type))[0] @@ -121,6 +111,23 @@ def __setattr__(self, field, value): ctypes_type = DbfCodeToCtypes[field_type] cast(address, POINTER(ctypes_type))[0] = value + # Directly write EPICS compatible value to .VAL field + def write_val(self, value): + offset, size, field_type = self.fields['VAL'] + address = c_void_p(offset + self.record.value) + + ctypes_type = DbfCodeToCtypes[field_type] + assert sizeof(value) == size, \ + 'Incompatible field and value: %d %d' % (sizeof(value), size) + cast(address, POINTER(ctypes_type))[0] = value + + def read_val(self): + offset, size, field_type = self.fields['VAL'] + address = c_void_p(offset + self.record.value) + ctypes_type = DbfCodeToCtypes[field_type] + return cast(address, POINTER(ctypes_type)).contents + + def __get_time(self, address): timestamp = cast(address, POINTER(ca_timestamp))[0] timestamp.secs += EPICS_epoch diff --git a/softioc/imports.py b/softioc/imports.py index eb525fb8..699f2302 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -12,19 +12,19 @@ # These are in the extension def get_DBF_values(): - """Return {DBF_name: DBF_int_value} mapping""" + '''Return {DBF_name: DBF_int_value} mapping''' return _extension.get_DBF_values() def get_field_offsets(record_type): - """Return {field_name: (offset, size, field_type)}""" + '''Return {field_name: (offset, size, field_type)}''' return _extension.get_field_offsets(record_type) def db_put_field(name, dbr_type, pbuffer, length): - """Put field where pbuffer is void* pointer. Returns RC""" + '''Put field where pbuffer is void* pointer. Returns RC''' return _extension.db_put_field(name, dbr_type, pbuffer, length) def install_pv_logging(acf_file): - """Install pv logging""" + '''Install pv logging''' _extension.install_pv_logging(acf_file) def expect_success(status, function, args): diff --git a/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py index 84880c3e..8d79627f 100644 --- a/softioc/pythonSoftIoc.py +++ b/softioc/pythonSoftIoc.py @@ -23,8 +23,8 @@ def __init__(self, builder, device, name, **fields): # remaining arguments are passed to the builder. It's a shame we # have to maintain this separately from the corresponding device list. DeviceKeywords = [ - 'on_update', 'on_update_name', 'validate', - 'initial_value', 'always_update'] + 'on_update', 'on_update_name', 'validate', 'always_update', + 'initial_value', '_wf_nelm', '_wf_dtype'] device_kargs = {} for keyword in DeviceKeywords: if keyword in fields: @@ -92,6 +92,12 @@ def __init_class__(cls): cls.waveform_out = cls.makeRecord( epicsdbbuilder.records.waveform, device.waveform_out, 'PythonWfOut') + cls.long_stringin = cls.makeRecord( + epicsdbbuilder.records.waveform, device.long_stringin, + 'PythonLongStringIn') + cls.long_stringout = cls.makeRecord( + epicsdbbuilder.records.waveform, device.long_stringout, + 'PythonLongStringOut') class makeRecord: def __init__(self, builder, record, dtyp = 'Python'): diff --git a/softioc/softioc.py b/softioc/softioc.py index c9214142..017db501 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -18,7 +18,7 @@ def epicsAtPyExit(): def iocInit(dispatcher=None): - """This must be called exactly once after loading all EPICS database files. + '''This must be called exactly once after loading all EPICS database files. After this point the EPICS IOC is running and serving PVs. Args: @@ -28,7 +28,7 @@ def iocInit(dispatcher=None): See Also: `softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications - """ + ''' if dispatcher is None: # Fallback to cothread import cothread @@ -263,7 +263,7 @@ def _add_records_from_file(dirname, file, substitutions): # possibly can put them out of order, but it works well enough for # devIocStats with open(os.path.join(dirname, file)) as f: - lines, include_subs = [], "" + lines, include_subs = [], '' for line in f.readlines(): line = line.rstrip() if line.startswith('substitute'): @@ -274,7 +274,7 @@ def _add_records_from_file(dirname, file, substitutions): # include "iocQueue.db" subs = substitutions if substitutions and include_subs: - subs = substitutions + ", " + include_subs + subs = substitutions + ', ' + include_subs else: subs = substitutions + include_subs _add_records_from_file(dirname, line.split('"')[1], subs) diff --git a/tests/conftest.py b/tests/conftest.py index 3479fe5c..94faccf8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,14 @@ import pytest +# Must import softioc before epicsdbbuilder +from softioc.device_core import RecordLookup +from epicsdbbuilder import ResetRecords + +requires_cothread = pytest.mark.skipif( + sys.platform.startswith("win"), reason="Cothread doesn't work on windows" +) + class SubprocessIOC: def __init__(self, ioc_py): self.pv_prefix = "".join( @@ -59,3 +67,29 @@ def asyncio_ioc_override(): yield ioc ioc.kill() aioca_cleanup() + +def _clear_records(): + # Remove any records created at epicsdbbuilder layer + ResetRecords() + # And at pythonSoftIoc level + # TODO: Remove this hack and use use whatever comes out of + # https://github.com/dls-controls/pythonSoftIOC/issues/56 + RecordLookup._RecordDirectory.clear() + +@pytest.fixture +def clear_records(): + """Fixture to delete all records before and after a test.""" + _clear_records() + yield + _clear_records() + +def enable_code_coverage(): + """Ensure code coverage works as expected for `multiprocesses` tests. + This method should be called just before any `multiprocess.Process` call + is made.""" + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() diff --git a/tests/expected_records.db b/tests/expected_records.db index c210edc1..ab764e2f 100644 --- a/tests/expected_records.db +++ b/tests/expected_records.db @@ -53,10 +53,11 @@ record(longout, "TS-DI-TEST-01:LONGOUT") record(waveform, "TS-DI-TEST-01:LONGSTRING") { field(DISP, "1") - field(DTYP, "Python") + field(DTYP, "PythonLongStringIn") field(FTVL, "UCHAR") field(INP, "@TS-DI-TEST-01:LONGSTRING") field(NELM, "256") + field(PINI, "YES") field(SCAN, "I/O Intr") } @@ -97,6 +98,7 @@ record(waveform, "TS-DI-TEST-01:SIN") field(FTVL, "DOUBLE") field(INP, "@TS-DI-TEST-01:SIN") field(NELM, "1024") + field(PINI, "YES") field(SCAN, "I/O Intr") } @@ -145,9 +147,10 @@ record(waveform, "TS-DI-TEST-01:WAVEFORM2") { field(DISP, "1") field(DTYP, "Python") - field(FTVL, "FLOAT") + field(FTVL, "DOUBLE") field(INP, "@TS-DI-TEST-01:WAVEFORM2") field(NELM, "10") + field(PINI, "YES") field(SCAN, "I/O Intr") } diff --git a/tests/sim_records.py b/tests/sim_records.py index 3009e356..ee712601 100644 --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -22,67 +22,54 @@ def on_update(value): def on_update_name(value, name): print('on_update', name, ':', repr(value)) -t_ai = aIn('AI', initial_value=12.34) -t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False) -t_longin = longIn('LONGIN', initial_value=33) -t_stringin = stringIn('STRINGIN', initial_value="Testing string") -t_mbbi = mbbIn( - 'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"), - initial_value=2) - -t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name) -t_boolout = boolOut( - 'BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update) -t_longout = longOut('LONGOUT', initial_value=2008, on_update=on_update) -t_stringout = stringOut( - 'STRINGOUT', initial_value='watevah', on_update=on_update) -t_mbbo = mbbOut( - 'MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1) - -def update_sin_wf(value): - print('update_sin_wf', value) - sin_wf.set(numpy.sin( - numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get()))) -sin_wf = Waveform('SIN', datatype = float, length = 1024) -# Check we can update its value before iocInit as per #22 -sin_wf.set([1, 2, 3]) -sin_len = longOut( - 'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf) -sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) - - -wf_len = 32 -wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) -t_waveform_in = Waveform('WAVEFORM', wf) -t_waveform_out = WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) -t_waveform_in2 = Waveform('WAVEFORM2', length = 10) - -t_longstring_in = Waveform('LONGSTRING', length = 256, datatype = numpy.uint8) - - -def Update(): - t_ai.set(3.14159) - t_boolin.set(True) - t_longin.set(365) - t_stringin.set('Another different string') - t_mbbi.set(0) - -def UpdateOut(): - t_ao.set(3.14159) - t_boolout.set(True) - t_longout.set(365) - t_stringout.set('Another different string') - t_mbbo.set(2) +# The publicly accessible records. +t_ai = None +t_ao = None + +def create_records(): + global t_ai, t_ao + + t_ai = aIn('AI', initial_value=12.34) + + boolIn('BOOLIN', 'True', 'False', initial_value=False) + longIn('LONGIN', initial_value=33) + stringIn('STRINGIN', initial_value="Testing string") + mbbIn( + 'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"), + initial_value=2) + + t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name) + + boolOut('BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update) + longOut('LONGOUT', initial_value=2008, on_update=on_update) + stringOut('STRINGOUT', initial_value='watevah', on_update=on_update) + mbbOut('MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1) + + def update_sin_wf(value): + print('update_sin_wf', value) + sin_wf.set(numpy.sin( + numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get()))) + sin_wf = Waveform('SIN', datatype = float, length = 1024) + # Check we can update its value before iocInit as per #22 + sin_wf.set([1, 2, 3]) + sin_len = longOut( + 'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf) + sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) + + + wf_len = 32 + wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) + Waveform('WAVEFORM', wf) + WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) + Waveform('WAVEFORM2', length = 10) + + longStringIn('LONGSTRING', length = 256) + +create_records() softioc.devIocStats(ioc_name) -__all__ = [ - 't_ai', 't_boolin', 't_longin', 't_stringin', 't_mbbi', - 't_ao', 't_boolout', 't_longout', 't_stringout', 't_mbbo', - 't_waveform_in', 't_waveform_in2', 't_waveform_out', 't_longstring_in', - 'wf_len', - 'Update', 'UpdateOut' -] +__all__ = ['t_ai', 't_ao', ] if __name__ == "__main__": WriteRecords("expected_records.db") diff --git a/tests/test_record_values.py b/tests/test_record_values.py new file mode 100644 index 00000000..17742c00 --- /dev/null +++ b/tests/test_record_values.py @@ -0,0 +1,797 @@ +import multiprocessing +from typing import List +import numpy +import pytest +import asyncio + +from enum import Enum +from math import isnan, inf, nan + +from conftest import requires_cothread, enable_code_coverage + +from softioc import asyncio_dispatcher, builder, softioc +from softioc.pythonSoftIoc import RecordWrapper + +# Test file for anything related to valid record values getting/setting + + +# Test parameters +DEVICE_NAME = "RECORD-VALUE-TESTS" +TIMEOUT = 5 # Seconds + +VERY_LONG_STRING = "This is a fairly long string, the kind that someone " \ + "might think to put into a record that can theoretically hold a huge " \ + "string and so lets test it and prove that shall we?" + + + +def record_func_names(fixture_value): + """Provide a nice name for the record_func fixture""" + return fixture_value.__name__ + + +@pytest.fixture( + params=[ + builder.aIn, + builder.aOut, + builder.longIn, + builder.longOut, + builder.boolIn, + builder.boolOut, + builder.Action, + builder.stringIn, + builder.stringOut, + builder.mbbIn, + builder.mbbOut, + builder.WaveformIn, + builder.WaveformOut, + builder.longStringIn, + builder.longStringOut, + ], + ids=record_func_names, +) +def record_func(request): + """The list of record creation functions""" + return request.param + +# A list of all In records, used to filter out various tests +in_records = [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn, + builder.longStringIn, +] + +def record_values_names(fixture_value): + """Provide a nice name for the tests in the record_values fixture""" + return ( + fixture_value[1].__name__ + + "-" + + type(fixture_value[2]).__name__ + + "-" + + str(fixture_value[3]) + ) + + +record_values_list = [ + ("aIn_float", builder.aIn, 5.5, 5.5, float), + ("aOut_float", builder.aOut, 5.5, 5.5, float), + ("aIn_int", builder.aIn, 3, 3.0, float), + ("aOut_int", builder.aOut, 3, 3.0, float), + ("aIn_inf", builder.aIn, inf, inf, float), + ("aOut_inf", builder.aOut, inf, inf, float), + ("aIn_neginf", builder.aIn, -inf, -inf, float), + ("aOut_neginf", builder.aOut, -inf, -inf, float), + ("aIn_nan", builder.aIn, nan, nan, float), + ("aOut_nan", builder.aOut, nan, nan, float), + ("longIn_int", builder.longIn, 5, 5, int), + ("longOut_int", builder.longOut, 5, 5, int), + ("boolIn_int", builder.boolIn, 1, 1, int), + ("boolOut_int", builder.boolOut, 1, 1, int), + ("boolIn_true", builder.boolIn, True, 1, int), + ("boolOut_true", builder.boolOut, True, 1, int), + ("boolIn_false", builder.boolIn, False, 0, int), + ("boolOut_false", builder.boolOut, False, 0, int), + ("Action_int", builder.Action, 1, 1, int), + ("Action_true", builder.Action, True, 1, int), + ("Action_false", builder.Action, False, 0, int), + ("mbbIn_int", builder.mbbIn, 1, 1, int), + ("mbbOut_int", builder.mbbOut, 1, 1, int), + ("strIn_abc", builder.stringIn, "abc", "abc", str), + ("strOut_abc", builder.stringOut, "abc", "abc", str), + ("strIn_empty", builder.stringIn, "", "", str), + ("strOut_empty", builder.stringOut, "", "", str), + ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 + ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 + ( + "wIn_list_int", + builder.WaveformIn, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wOut_list_int", + builder.WaveformOut, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wIn_list_float", + builder.WaveformIn, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_list_float", + builder.WaveformOut, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_int", + builder.WaveformIn, + 567, + numpy.array([567.], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_int", + builder.WaveformOut, + 567, + numpy.array([567.], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_float", + builder.WaveformIn, + 12.345, + numpy.array([12.345], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_float", + builder.WaveformOut, + 12.345, + numpy.array([12.345], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_bytes", + builder.WaveformIn, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 + ), + numpy.ndarray, + ), + ( + "wOut_bytes", + builder.WaveformOut, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 + ), + numpy.ndarray, + ), + ( + "longStringIn_str", + builder.longStringIn, + "ABC", + "ABC", + str, + ), + ( + "longStringOut_str", + builder.longStringOut, + "ABC", + "ABC", + str, + ), + ( + "longStringIn_long_str", + builder.longStringIn, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), + ( + "longStringOut_long_str", + builder.longStringOut, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), + ( + "longStringIn_unicode", + builder.longStringIn, + "%a€b", + "%a€b", + str + ), + ( + "longStringOut_unicode", + builder.longStringOut, + "%a€b", + "%a€b", + str + ), +] + + +@pytest.fixture(params=record_values_list, ids=record_values_names) +def record_values(request): + """A list of parameters for record value setting/getting tests. + + Fields are: + - Record builder function + - Input value passed to .set()/initial_value/caput + - Expected output value after doing .get()/caget + - Expected type of the output value from .get()/caget""" + return request.param # One item from the params list + + + +def record_value_asserts( + creation_func, actual_value, expected_value, expected_type +): + """Asserts that the expected value and expected type are matched with + the actual value. Handles both scalar and waveform data""" + try: + if type(expected_value) == float and isnan(expected_value): + assert isnan(actual_value) # NaN != Nan, so needs special case + elif creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(actual_value, expected_value) + assert type(actual_value) == expected_type + else: + assert actual_value == expected_value + assert type(actual_value) == expected_type + except AssertionError as e: + msg = ( + "Failed with parameters: " + + str(creation_func) + + ", " + + str(actual_value) + + ", " + + str(expected_value) + + ", " + + str(expected_type) + ) + # Add the parameters into the exception message. Seems to be the + # cleanest place to add it, as doing a print() or a logging.error() + # both cause the message to appear in a separate section of the pytest + # results, which can be difficult to spot + e.args += (msg,) + raise e + + +class SetValueEnum(Enum): + """Enum to control when and how the record's value should be set""" + + INITIAL_VALUE = 1 + SET_BEFORE_INIT = 2 + SET_AFTER_INIT = 3 + NO_SET = 4 + CAPUT = 5 + + +class GetValueEnum(Enum): + """Enum to control whether values are retrieved using .get() or caget()""" + + GET = 1 + CAGET = 2 + + +def run_ioc(record_configurations: list, conn, set_enum, get_enum): + """Creates a record and starts the IOC. `initial_value` will be set on + the record at different times based on the `set_enum` parameter.""" + + builder.SetDeviceName(DEVICE_NAME) + + records: List[RecordWrapper] = [] + + # Create records from the given list + for configuration in record_configurations: + kwarg = {} + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + if set_enum == SetValueEnum.INITIAL_VALUE: + kwarg.update({"initial_value": initial_value}) + elif creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + # Related to this issue: + # https://github.com/dls-controls/pythonSoftIOC/issues/37 + + out_rec = creation_func(record_name, **kwarg) + + if set_enum == SetValueEnum.SET_BEFORE_INIT: + out_rec.set(initial_value) + + records.append(out_rec) + + dispatcher = asyncio_dispatcher.AsyncioDispatcher() + builder.LoadDatabase() + softioc.iocInit(dispatcher) + conn.send("IOC started") + + # Record list and record config list should always be in line + for record, configuration in zip(records, record_configurations): + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + if set_enum == SetValueEnum.SET_AFTER_INIT: + record.set(initial_value) + + for record in records: + if get_enum == GetValueEnum.GET: + conn.send(record.get()) + # Some tests do a caput and require another .get() + if set_enum == SetValueEnum.CAPUT: + if conn.poll(TIMEOUT): + val = conn.recv() + if val is not None: + conn.send(record.get()) + + conn.close() + + # Keep process alive while main thread works. + # This is most applicable to CAGET tests. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), dispatcher.loop + ).result() + + +def run_test_function( + record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum +): + """Run the test function using multiprocessing and check returned value is + expected value. set_enum and get_enum determine when the record's value is + set and how the value is retrieved, respectively.""" + + parent_conn, child_conn = multiprocessing.Pipe() + + enable_code_coverage() + + ioc_process = multiprocessing.Process( + target=run_ioc, + args=(record_configurations, child_conn, set_enum, get_enum), + ) + + ioc_process.start() + + # Wait for message that IOC has started + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC process did not start before TIMEOUT expired") + + try: + # Cannot do these imports before the subprocess starts, as cothread + # isn't threadsafe (in the way we require) + from cothread import Yield + from cothread.catools import caget, caput, _channel_cache + from cothread.dbr import DBR_CHAR_STR + + # cothread remembers connected IOCs. As we potentially restart the same + # named IOC multiple times, we have to purge the cache else the + # result from caget/caput cache would be a DisconnectError during the + # second test + _channel_cache.purge() + + for configuration in record_configurations: + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + # Infer some required keywords from parameters + kwargs = {} + put_kwarg = {} + if creation_func in [builder.longStringIn, builder.longStringOut]: + kwargs.update({"datatype": DBR_CHAR_STR}) + + if (creation_func in [builder.WaveformIn, builder.WaveformOut] + and type(initial_value) is bytes): + # There's a bug in caput that means DBR_CHAR_STR doesn't + # truncate the array of the target record, meaning .get() + # returns all NELM rather than just NORD elements. Instead we + # encode the data ourselves + initial_value = numpy.frombuffer( + initial_value, dtype = numpy.uint8) + + if set_enum == SetValueEnum.CAPUT: + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC did not provide initial record value") + caput( + DEVICE_NAME + ":" + record_name, + initial_value, + wait=True, + **kwargs, + **put_kwarg, + ) + + if get_enum == GetValueEnum.GET: + parent_conn.send("Do another get!") + # Ensure IOC process has time to execute. + # I saw failures on MacOS where it appeared the IOC had not + # processed the put'ted value as the caget returned the same + # value as was originally passed in. + Yield(timeout=TIMEOUT) + + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + rec_val = parent_conn.recv() + else: + pytest.fail("IOC did not provide record value in queue") + else: + rec_val = caget( + DEVICE_NAME + ":" + record_name, + timeout=TIMEOUT, + **kwargs, + ) + # '+' operator used to convert cothread's types into Python + # native types e.g. "+ca_int" -> int + rec_val = +rec_val + + if ( + creation_func in [builder.WaveformOut, builder.WaveformIn] + and expected_value.dtype in [numpy.float64, numpy.int32] + ): + print( + "caget cannot distinguish between a waveform with 1" + "element and a scalar value, and so always returns a " + "scalar. Therefore we skip this check.") + continue + + + record_value_asserts( + creation_func, rec_val, expected_value, expected_type + ) + finally: + # Purge cache to suppress spurious "IOC disconnected" exceptions + _channel_cache.purge() + + ioc_process.terminate() + ioc_process.join(timeout=TIMEOUT) + if ioc_process.exitcode is None: + pytest.fail("Process did not terminate") + + +def skip_long_strings(record_values): + if ( + record_values[0] in [builder.stringIn, builder.stringOut] + and len(record_values[1]) > 40 + ): + pytest.skip("CAPut blocks strings > 40 characters.") + + +class TestGetValue: + """Tests that use .get() to check whether values applied with .set(), + initial_value, or caput return the expected value""" + + def test_value_pre_init_set(self, clear_records, record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() before IOC initialisation occurs""" + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = record_values + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func(record_name, **kwarg) + out_rec.set(initial_value) + + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + def test_value_pre_init_initial_value(self, clear_records, record_values): + """Test that records provide the expected values on get calls when using + initial_value and .get() before IOC initialisation occurs""" + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = record_values + + out_rec = creation_func(record_name, initial_value=initial_value) + + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + @requires_cothread + def test_value_post_init_set(self): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and .get() after initialisation""" + run_test_function( + record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_initial_value(self): + """Test that records provide the expected values on get calls when using + initial_value during record creation and .get() after IOC initialisation + """ + run_test_function( + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_set_after_init(self): + """Test that records provide the expected values on get calls when using + .set() and .get() after IOC initialisation""" + run_test_function( + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_caput(self): + """Test that records provide the expected values on get calls when using + caput and .get() after IOC initialisation""" + + # Various conditions mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + if ( + item[1] in [builder.stringIn, builder.stringOut] + and len(item[2]) > 40 + ): + # caput blocks strings longer than 40 characters + continue + + if item[1] not in in_records: + # In records block caput + filtered_list.append(item) + + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET) + + +@requires_cothread +class TestCagetValue: + """Tests that use Caget to check whether values applied with .set(), + initial_value, or caput return the expected value""" + + def test_value_post_init_set(self): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and caget after initialisation""" + + # Various conditions mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + # .set() on In records doesn't update correctly. + # pythonSoftIOC issue #67 + if item[1] not in in_records: + filtered_list.append(item) + + run_test_function( + filtered_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET + ) + + @requires_cothread + def test_value_post_init_initial_value(self): + """Test that records provide the expected values on get calls when using + initial_value during record creation and caget after IOC initialisation + """ + + run_test_function( + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET + ) + + @requires_cothread + def test_value_post_init_set_after_init(self): + """Test that records provide the expected values on get calls when using + .set() and caget after IOC initialisation""" + + run_test_function( + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET + ) + + def test_value_post_init_caput(self): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and caget after initialisation""" + + # Various conditions (e.g.) mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + # In records block caputs + if item[1] not in in_records: + filtered_list.append(item) + + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) + + +default_values_list = [ + ("default_aOut", builder.aOut, None, 0.0, float), + ("default_aIn", builder.aIn, None, 0.0, float), + ("default_longOut", builder.longOut, None, 0, int), + ("default_longIn", builder.longIn, None, 0, int), + ("default_boolOut", builder.boolOut, None, 0, int), + ("default_boolIn", builder.boolIn, None, 0, int), + ("default_Action", builder.Action, None, 0, int), + ("default_stringOut", builder.stringOut, None, "", str), + ("default_stringIn", builder.stringIn, None, "", str), + ("default_mbbOut", builder.mbbOut, None, 0, int), + ("default_mbbIn", builder.mbbIn, None, 0, int), + ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), + ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), + ("default_longStringOut", builder.longStringOut, None, "", str), + ("default_longStringIn", builder.longStringIn, None, "", str), +] + + +class TestDefaultValue: + """Tests related to default values""" + + @pytest.mark.parametrize( + "creation_func,expected_value,expected_type", + [ + (builder.aOut, 0.0, float), + (builder.aIn, 0.0, float), + (builder.longOut, 0, int), + (builder.longIn, 0, int), + (builder.boolOut, 0, int), + (builder.boolIn, 0, int), + (builder.Action, 0, int), + (builder.stringOut, "", str), + (builder.stringIn, "", str), + (builder.mbbOut, 0, int), + (builder.mbbIn, 0, int), + (builder.WaveformOut, numpy.empty(0), numpy.ndarray), + (builder.WaveformIn, numpy.empty(0), numpy.ndarray), + (builder.longStringOut, "", str), + (builder.longStringIn, "", str), + ], + ) + def test_value_default_pre_init( + self, creation_func, expected_value, expected_type, clear_records + ): + """Test that the correct default values are returned from .get() (before + record initialisation) when no initial_value or .set() is done""" + # Out records do not have default values until records are initialized + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + @requires_cothread + def test_value_default_post_init(self): + """Test that records return the expected default value from .get after + they are initialised, having never had a value set""" + + run_test_function( + default_values_list, SetValueEnum.NO_SET, GetValueEnum.GET + ) + + @requires_cothread + def test_value_default_post_init_caget(self): + """Test that records return the expected default value from CAGet after + they are initialised, having never had a value set""" + + run_test_function( + default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET + ) + + +class TestNoneValue: + """Various tests regarding record value of None""" + + # We expect using None as a value will trigger one of these exceptions + expected_exceptions = (ValueError, TypeError, AttributeError) + + + @pytest.fixture + def record_func_reject_none(self, record_func): + """Parameterized fixture for all records that reject None""" + if record_func in [builder.WaveformIn, builder.WaveformOut]: + pytest.skip("None is accepted by Waveform records, as numpy " + "treats it as NaN") + return record_func + + def test_value_none_rejected_initial_value( + self, clear_records, record_func_reject_none + ): + """Test setting \"None\" as the initial_value raises an exception""" + + kwarg = {} + if record_func_reject_none in [ + builder.WaveformIn, + builder.WaveformOut, + ]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises(self.expected_exceptions): + record_func_reject_none("SOME-NAME", initial_value=None, **kwarg) + + def test_value_none_rejected_set_before_init( + self, clear_records, record_func_reject_none + ): + """Test that setting \"None\" using .set() raises an exception""" + + kwarg = {} + if record_func_reject_none in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises(self.expected_exceptions): + record = record_func_reject_none("SOME-NAME", **kwarg) + record.set(None) + + def none_value_test_func(self, record_func, queue): + """Start the IOC and catch the expected exception""" + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + record = record_func("SOME-NAME", **kwarg) + + builder.LoadDatabase() + softioc.iocInit() + + try: + record.set(None) + except Exception as e: + queue.put(e) + + queue.put(Exception("FAIL:Test did not raise exception during .set()")) + + @requires_cothread + def test_value_none_rejected_set_after_init(self, record_func_reject_none): + """Test that setting \"None\" using .set() after IOC init raises an + exception""" + queue = multiprocessing.Queue() + + enable_code_coverage() + + process = multiprocessing.Process( + target=self.none_value_test_func, + args=(record_func_reject_none, queue), + ) + + process.start() + + try: + exception = queue.get(timeout=5) + + assert isinstance(exception, self.expected_exceptions) + finally: + process.terminate() + process.join(timeout=3) diff --git a/tests/test_records.py b/tests/test_records.py index 1d7af3d8..95d91013 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,23 +1,55 @@ +import multiprocessing +import numpy import os import pytest +import asyncio -from softioc import builder +from conftest import requires_cothread, _clear_records, enable_code_coverage -import sim_records +from softioc import asyncio_dispatcher, builder, softioc +# Test file for miscellaneous tests related to records + +# Test parameters +DEVICE_NAME = "RECORD-TESTS" +TIMEOUT = 5 # Seconds + +def test_records(tmp_path, clear_records): + # Ensure we definitely unload all records that may be hanging over from + # previous tests, then create exactly one instance of expected records. + from sim_records import create_records + _clear_records() + create_records() -def test_records(tmp_path): path = str(tmp_path / "records.db") builder.WriteRecords(path) expected = os.path.join(os.path.dirname(__file__), "expected_records.db") assert open(path).readlines()[5:] == open(expected).readlines() + def test_enum_length_restriction(): with pytest.raises(AssertionError): builder.mbbIn( - "ManyLabels", "one", "two", "three", "four", "five", "six", - "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", - "fourteen", "fifteen", "sixteen", "seventeen") + "ManyLabels", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + ) + def test_DISP_defaults_on(): """Test that all IN record types have DISP=1 set by default""" @@ -45,9 +77,172 @@ def test_DISP_defaults_on(): # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 1 + def test_DISP_can_be_overridden(): """Test that DISP can be forced off for In records""" record = builder.longIn("DISP-OFF", DISP=0) # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 + +def test_waveform_disallows_zero_length(clear_records): + """Test that WaveformIn/Out records throw an exception when being + initialized with a zero length array""" + with pytest.raises(AssertionError): + builder.WaveformIn("W_IN", []) + with pytest.raises(AssertionError): + builder.WaveformOut("W_OUT", []) + +def test_waveform_disallows_too_long_values(clear_records): + """Test that Waveform and longString records throw an exception when + an overlong value is written""" + w_in = builder.WaveformIn("W_IN", [1, 2, 3]) + w_out = builder.WaveformOut("W_OUT", [1, 2, 3]) + + ls_in = builder.longStringIn("LS_IN", initial_value="ABC") + ls_out = builder.longStringOut("LS_OUT", initial_value="ABC") + + with pytest.raises(AssertionError): + w_in.set([1, 2, 3, 4]) + with pytest.raises(AssertionError): + w_out.set([1, 2, 3, 4]) + with pytest.raises(AssertionError): + ls_in.set("ABCD") + with pytest.raises(AssertionError): + ls_out.set("ABCD") + +def validate_fixture_names(params): + """Provide nice names for the out_records fixture in TestValidate class""" + return params[0].__name__ +class TestValidate: + """Tests related to the validate callback""" + + @pytest.fixture( + params=[ + (builder.aOut, 7.89, 0), + (builder.boolOut, 1, 0), + (builder.Action, 1, 0), + (builder.longOut, 7, 0), + (builder.stringOut, "HI", ""), + (builder.mbbOut, 2, 0), + (builder.WaveformOut, [10, 11, 12], []), + (builder.longStringOut, "A LONGER HELLO", ""), + ], + ids=validate_fixture_names + ) + def out_records(self, request): + """The list of Out records and an associated value to set """ + return request.param + + def validate_always_pass(self, record, new_val): + """Validation method that always allows changes""" + return True + + def validate_always_fail(self, record, new_val): + """Validation method that always rejects changes""" + return False + + def validate_ioc_test_func(self, record_func, queue, validate_pass: bool): + """Start the IOC with the specified validate method""" + + builder.SetDeviceName(DEVICE_NAME) + + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + kwarg.update( + { + "validate": self.validate_always_pass + if validate_pass + else self.validate_always_fail + } + ) + + record_func("VALIDATE-RECORD", **kwarg) + + dispatcher = asyncio_dispatcher.AsyncioDispatcher() + builder.LoadDatabase() + softioc.iocInit(dispatcher) + + queue.put("IOC ready") + + # Keep process alive while main thread works. + # This is most applicable to CAGET tests. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), dispatcher.loop + ).result() + + def validate_test_runner( + self, + creation_func, + new_value, + expected_value, + validate_pass: bool): + + queue = multiprocessing.Queue() + + enable_code_coverage() + + process = multiprocessing.Process( + target=self.validate_ioc_test_func, + args=(creation_func, queue, validate_pass), + ) + + process.start() + + try: + queue.get(timeout=5) # Get the expected IOC initialised message + + from cothread.catools import caget, caput, _channel_cache + + # See other places in this file for why we call it + _channel_cache.purge() + + kwargs = {} + if creation_func in [builder.longStringIn, builder.longStringOut]: + from cothread.dbr import DBR_CHAR_STR + kwargs.update({"datatype": DBR_CHAR_STR}) + + put_ret = caput( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + new_value, + wait=True, + **kwargs, + ) + assert put_ret.ok, "caput did not succeed" + + ret_val = caget( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + timeout=3, + **kwargs + ) + + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(ret_val, expected_value) + else: + assert ret_val == expected_value + + finally: + process.terminate() + process.join(timeout=3) + + + @requires_cothread + def test_validate_allows_updates(self, out_records): + """Test that record values are updated correctly when validate + method allows it """ + + creation_func, value, _ = out_records + + self.validate_test_runner(creation_func, value, value, True) + + + @requires_cothread + def test_validate_blocks_updates(self, out_records): + """Test that record values are not updated when validate method + always blocks updates""" + + creation_func, value, default = out_records + + self.validate_test_runner(creation_func, value, default, False)