From b2d98778e7cc6eb9942a4ea53322f81b05f32eee Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 20 Feb 2026 16:40:47 -0800 Subject: [PATCH 001/104] ENH: try uv and standardize pyproject.toml entrypoint stuff --- .gitignore | 6 +- .pre-commit-config.yaml | 45 +- .python-version | 1 + MANIFEST.in | 4 - dev-requirements.txt | 3 - docs-requirements.txt | 4 - pcdswidgets/entrypoint_widgets.py | 28 ++ pyproject.toml | 69 +-- requirements.txt | 3 - uv.lock | 702 ++++++++++++++++++++++++++++++ 10 files changed, 799 insertions(+), 66 deletions(-) create mode 100644 .python-version delete mode 100644 dev-requirements.txt delete mode 100644 docs-requirements.txt create mode 100644 pcdswidgets/entrypoint_widgets.py delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 9a1abc8..8a51acd 100644 --- a/.gitignore +++ b/.gitignore @@ -76,9 +76,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -# pyenv -.python-version - # celery beat schedule file celerybeat-schedule @@ -106,3 +103,6 @@ venv.bak/ # mypy .mypy_cache/ + +# setuptools_scm +*/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 918edd7..e71a018 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,28 +6,25 @@ exclude: | )$ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: no-commit-to-branch - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-ast - - id: check-case-conflict - - id: check-json - - id: check-merge-conflict - - id: check-symlinks - - id: check-xml - - id: check-yaml - exclude: '^(conda-recipe/meta.yaml)$' - - id: debug-statements +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: no-commit-to-branch + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-xml + - id: check-yaml + exclude: '^(conda-recipe/meta.yaml)$' + - id: debug-statements -- repo: https://github.com/pycqa/flake8.git - rev: 6.0.0 - hooks: - - id: flake8 - -- repo: https://github.com/timothycrosley/isort - rev: 5.12.0 - hooks: - - id: isort +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.3 + hooks: + - id: ruff # run the linter + args: [ --fix ] # and the safe fixes + - id: ruff-format # run the formatter diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..fdcfcfd --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 17eb881..165fd4d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,7 +5,3 @@ include CONTRIBUTING.rst include LICENSE.md include MANIFEST.in include README.md - -include dev-requirements.txt -include docs-requirements.txt -include requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index c9d6678..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-qt -pytest-timeout diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index 7df0a57..0000000 --- a/docs-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -docs-versions-menu -sphinx -sphinx_rtd_theme -sphinxcontrib-jquery diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py new file mode 100644 index 0000000..26f019c --- /dev/null +++ b/pcdswidgets/entrypoint_widgets.py @@ -0,0 +1,28 @@ +""" +Helper module for creating the [project.entry-points."pydm.widget"] +section in pyproject.toml + +python -m pcdswidgets.entrypoint_widgets +""" + +import inspect + +import pcdswidgets.eps_byteindicator +import pcdswidgets.table +import pcdswidgets.vacuum + +INCLUDE_MODULES = [pcdswidgets.eps_byteindicator, pcdswidgets.table, pcdswidgets.vacuum] + + +def main(): + lines = set() + for module in INCLUDE_MODULES: + for name, obj in inspect.getmembers(module, inspect.isclass): + if hasattr(obj, "_qt_designer_"): + lines.add(f'{name} = "{obj.__module__}:{name}"') + for line in sorted(lines): + print(line) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 006e0d0..91ad876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,17 @@ build-backend = "setuptools.build_meta" requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2",] [project] -classifiers = [ "Development Status :: 2 - Pre-Alpha", "Natural Language :: English", "Programming Language :: Python :: 3",] +classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Python :: 3",] description = "LCLS PyDM Widget Library" -dynamic = [ "version", "readme", "dependencies", "optional-dependencies",] +dynamic = [ "version", "readme" ] keywords = [] name = "pcdswidgets" -requires-python = ">=3.9" +requires-python = ">=3.12" +dependencies = [ + "pydm>=1.9.0", + "pyqt5>=5.15.11", + "qtpy>=2.4.3", +] [[project.authors]] name = "SLAC National Accelerator Laboratory" @@ -24,28 +29,40 @@ file = "LICENSE.md" write_to = "pcdswidgets/_version.py" [project.entry-points."pydm.widget"] -SymbolBase = "pcdswidgets.vacuum.base:PCDSSymbolBase" -FilterSortWidgetTable = "pcdswidgets.table:FilterSortWidgetTable" -PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" -PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" -PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" ApertureValve = "pcdswidgets.vacuum.valves:ApertureValve" -FastShutter = "pcdswidgets.vacuum.valves:FastShutter" -NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" -ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" -RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" -ControlValve = "pcdswidgets.vacuum.valves:ControlValve" +ColdCathodeGauge = "pcdswidgets.vacuum.gauges:ColdCathodeGauge" ControlOnlyValveNC = "pcdswidgets.vacuum.valves:ControlOnlyValveNC" ControlOnlyValveNO = "pcdswidgets.vacuum.valves:ControlOnlyValveNO" -IonPump = "pcdswidgets.vacuum.pumps:IonPump" -TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" -ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" +ControlValve = "pcdswidgets.vacuum.valves:ControlValve" +EPSByteIndicator = "pcdswidgets.eps_byteindicator:EPSByteIndicator" +FastShutter = "pcdswidgets.vacuum.valves:FastShutter" +FilterSortWidgetTable = "pcdswidgets.table:FilterSortWidgetTable" GetterPump = "pcdswidgets.vacuum.pumps:GetterPump" -RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" HotCathodeGauge = "pcdswidgets.vacuum.gauges:HotCathodeGauge" -ColdCathodeGauge = "pcdswidgets.vacuum.gauges:ColdCathodeGauge" +IonPump = "pcdswidgets.vacuum.pumps:IonPump" +NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" +PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" +PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" +PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" +ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" RGA = "pcdswidgets.vacuum.others:RGA" -EPSByteIndicator = "pcdswidgets.eps_byteindicator:EPSByteIndicator" +RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" +RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" +ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" +TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" + +[project.optional-dependencies] +docs = [ + "docs-versions-menu>=0.5.2", + "sphinx>=9.1.0", + "sphinx-rtd-theme>=3.1.0", + "sphinxcontrib-jquery>=4.1", +] +test = [ + "pytest>=9.0.2", + "pytest-qt>=4.5.0", + "pytest-timeout>=2.4.0", +] [tool.setuptools.packages.find] where = [ ".",] @@ -56,11 +73,13 @@ namespaces = false file = "README.md" content-type = "text/markdown" -[tool.setuptools.dynamic.dependencies] -file = [ "requirements.txt",] -[tool.setuptools.dynamic.optional-dependencies.test] -file = "dev-requirements.txt" +[tool.ruff] +line-length = 88 +exclude = [".git", "__pycache__", "build", "dist", "pcdswidgets/_version.py"] + +[tool.ruff.lint] +select = ["C", "E", "F", "W", "B", "I"] -[tool.setuptools.dynamic.optional-dependencies.doc] -file = "docs-requirements.txt" +[tool.ruff.lint.pydocstyle] +convention = "numpy" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e4e865c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pydm>=1.9.0 -qtpy -PyQt5 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c8e58d9 --- /dev/null +++ b/uv.lock @@ -0,0 +1,702 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docs-versions-menu" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyparsing" }, + { name = "setuptools" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/b7/33bb6d00593ce0a7df5d4b13871b798d1cc8fd798933f036b058568dbe2d/docs_versions_menu-0.5.2.tar.gz", hash = "sha256:8c6eae5836fb63e4f9700387385c5074dac8187609538422dab5fa39de110a73", size = 782903, upload-time = "2023-04-20T03:41:47.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/23/89993b2e9895add5e76a3f38ceb3c06dc2f1d0733937568085a36a56b21b/docs_versions_menu-0.5.2-py3-none-any.whl", hash = "sha256:8e331e2e9b2c9d3a7b2a7c8325a7e7ed7070b144cd3ef0f701172468af7e3cdf", size = 533159, upload-time = "2023-04-20T03:41:45.019Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "entrypoints" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/8d/a7121ffe5f402dc015277d2d31eb82d2187334503a011c18f2e78ecbb9b2/entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", size = 13974, upload-time = "2022-02-02T21:30:28.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f", size = 5294, upload-time = "2022-02-02T21:30:26.024Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pcdswidgets" +source = { editable = "." } +dependencies = [ + { name = "pydm" }, + { name = "pyqt5" }, + { name = "qtpy" }, +] + +[package.optional-dependencies] +docs = [ + { name = "docs-versions-menu" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-jquery" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-qt" }, + { name = "pytest-timeout" }, +] + +[package.metadata] +requires-dist = [ + { name = "docs-versions-menu", marker = "extra == 'docs'", specifier = ">=0.5.2" }, + { name = "pydm", specifier = ">=1.9.0" }, + { name = "pyqt5", specifier = ">=5.15.11" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2" }, + { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.5.0" }, + { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, + { name = "qtpy", specifier = ">=2.4.3" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=9.1.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=3.1.0" }, + { name = "sphinxcontrib-jquery", marker = "extra == 'docs'", specifier = ">=4.1" }, +] +provides-extras = ["docs", "test"] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydm" +version = "1.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "entrypoints" }, + { name = "numpy" }, + { name = "pyepics" }, + { name = "pyqtgraph" }, + { name = "qtpy" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/f7/f561e3bc7341f688855d78aaa007e7d96853ad2de42c644a0bfc7a078152/pydm-1.28.2.tar.gz", hash = "sha256:1768a06997686bd4d3d899e52047054452646a2381e2c495719bf710fe45ab92", size = 18208518, upload-time = "2026-02-10T20:41:46.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/7a/4fdbf3ebc35f045c564141ca69b648c83c053fc38dac300c668b6e7b3939/pydm-1.28.2-py3-none-any.whl", hash = "sha256:e5d9c48adf50c423c32b05e7fc480b66b0996fea04bb1d80c793cda793583e7e", size = 769506, upload-time = "2026-02-10T20:41:44.149Z" }, +] + +[[package]] +name = "pyepics" +version = "3.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/56/b7edf871ec2d81ecc600a7687cf9c536759f31ea482e8aec453c6dd12d21/pyepics-3.5.9.tar.gz", hash = "sha256:78222c1a8aff55bc7a93bdcb6eea9cb544fa8b9122daed1e7ea5b5e87269d45c", size = 6149589, upload-time = "2025-12-17T17:16:33.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/83/7dafb09fbc3efe9d00c4667d22b32b53d08e8a676fa164c6dd8f5debe85e/pyepics-3.5.9-py3-none-any.whl", hash = "sha256:b9863cc55a58542f0a28ad04621d4471f649e9cacfa4ccf346a58d6ba158640c", size = 5332286, upload-time = "2025-12-17T17:16:31.93Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.18" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, + { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, +] + +[[package]] +name = "pyqtgraph" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-qt" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] From fd1b76e500a9a5df754a530fa01a7dd68e37325f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 26 Feb 2026 18:36:13 -0800 Subject: [PATCH 002/104] ENH: checkpoint, the builder builds something close to useful --- .gitignore | 4 + MANIFEST.in | 2 +- pcdswidgets/builder/build.py | 144 ++++ pcdswidgets/builder/designer_widget.py | 65 ++ pcdswidgets/builder/ui/__init__.py | 0 pcdswidgets/builder/ui/positioner.ui | 639 +++++++++++++++++ pcdswidgets/builder/ui/positioner_row.ui | 664 ++++++++++++++++++ .../builder/ui/positioner_row_tc_interlock.ui | 273 +++++++ pcdswidgets/builder/ui/smaract_open_loop.ui | 440 ++++++++++++ pcdswidgets/builder/ui_base_widget.j2 | 88 +++ pcdswidgets/entrypoint_widgets.py | 8 +- pcdswidgets/motion/__init__.py | 3 + pcdswidgets/motion/motors.py | 8 + pyproject.toml | 5 +- 14 files changed, 2339 insertions(+), 4 deletions(-) create mode 100644 pcdswidgets/builder/build.py create mode 100644 pcdswidgets/builder/designer_widget.py create mode 100644 pcdswidgets/builder/ui/__init__.py create mode 100644 pcdswidgets/builder/ui/positioner.ui create mode 100644 pcdswidgets/builder/ui/positioner_row.ui create mode 100644 pcdswidgets/builder/ui/positioner_row_tc_interlock.ui create mode 100644 pcdswidgets/builder/ui/smaract_open_loop.ui create mode 100644 pcdswidgets/builder/ui_base_widget.j2 create mode 100644 pcdswidgets/motion/__init__.py create mode 100644 pcdswidgets/motion/motors.py diff --git a/.gitignore b/.gitignore index 8a51acd..25ef066 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,7 @@ venv.bak/ # setuptools_scm */_version.py + +# pyuic5 +**/ui/*.py +!**/ui/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in index 165fd4d..cf1ec58 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include pcdswidgets/_version.py +include **/*.ui include AUTHORS.rst include CONTRIBUTING.rst diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py new file mode 100644 index 0000000..06e9633 --- /dev/null +++ b/pcdswidgets/builder/build.py @@ -0,0 +1,144 @@ +import os +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from collections import defaultdict + +from jinja2 import Environment, PackageLoader + + +def build_uic(designer_ui: str): + """ + Use the standard uic parser to create a .py file with a .ui file's widget layouts. + + The files are named systematically with patterns like: + some_name.ui -> some_name_form.py + """ + form_output = f"{os.path.splitext(designer_ui)[0]}_form.py" + subprocess.run(f"pyuic5 -o {form_output} {designer_ui}".split(" ")) + + +def build_base_widget(designer_ui: str): + """ + Create a .py file with a suitable base widget for inclusion in designer. + + The files are named systematically with patterns like: + some_name.ui -> some_name_base.py + + The base widget will have the following properties: + - Imports from the uic output for its widget layouts + - Has appropriately-named properties for any expected macros in the pydm source file + - Has type hints to help the IDE keep track of which child widgets are available + + See ui_base_widget.j2, which is the jinja template for these output files. + """ + # Parse the .ui file and collect information about each widget + # Need a name to class mapping for the IDE type hints + widget_name_to_class: dict[str, str] = {} + # Need to keep track of which widget properties have macros + # widget_macros[widget_name][property_name] == "${MACRO} in context" + widget_macros: dict[str, dict[str, str | list[str]]] = {} + + tree = ET.parse(designer_ui) + for widget in tree.iter("widget"): + name = widget.attrib["name"] + cls = widget.attrib["class"] + widget_name_to_class[name] = cls + for prop in widget.findall("property"): + # Looking for string and stringlist only + str_node = prop.find("string") + if str_node is not None and str_node.text is not None: + # We have simple text! + if "${" in str_node.text: + try: + widget_macros[name][prop.attrib["name"]] = str_node.text + except KeyError: + widget_macros[name] = {prop.attrib["name"]: str_node.text} + continue + strlist_node = prop.find("stringlist") + if strlist_node is not None: + # We have a list of strings! Some may have macros. + all_str_nodes = strlist_node.findall("string") + all_str_literals = [node.text for node in all_str_nodes if node.text is not None] + for text in all_str_literals: + if "${" in text: + try: + widget_macros[name][prop.attrib["name"]] = all_str_literals + except KeyError: + widget_macros[name] = {prop.attrib["name"]: all_str_literals} + + # Need to get the name of the form class, which is "Ui_" and the name of the top-level widget + # Usually this ends up being "Ui_Form" with default naming but the user can change this + top_level_widget = tree.find("widget") + if top_level_widget is None: + raise RuntimeError("No top level widget in ui file") + clsname = f"Ui_{top_level_widget.attrib["name"]}" + + # We're done parsing, now we bring the info into a good form for the jinja template + ui_name = os.path.basename(designer_ui) + macro_set: set[str] = set() + widget_set: set[str] = set() + macro_to_widget: dict[str, list[str]] = defaultdict(list) + widget_to_macro: dict[str, list[str]] = {} + widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] = defaultdict(list) + widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] = defaultdict(list) + + for widget_name, prop_info in widget_macros.items(): + macros_here = set() + str_opts: list[tuple[str, str]] = [] + list_opts: list[tuple[str, list[str]]] = [] + for prop_name, value_with_macro in prop_info.items(): + if isinstance(value_with_macro, str): + str_opts.append((prop_name, value_with_macro)) + macros_here.update(_get_macros(value_with_macro)) + elif isinstance(value_with_macro, list): + list_opts.append((prop_name, value_with_macro)) + for val in value_with_macro: + macros_here.update(_get_macros(val)) + else: + raise TypeError(f"Invalid macro type: {value_with_macro}") + macro_set.update(macros_here) + widget_set.add(widget_name) + for macro in macro_set: + macro_to_widget[macro].append(widget_name) + widget_to_macro[widget_name] = sorted(macros_here) + widget_to_pre_templ_strs[widget_name].extend(str_opts) + widget_to_pre_templ_lists[widget_name].extend(list_opts) + + macro_names = sorted(macro_set) + widget_names = sorted(widget_set) + + # And last, standard jinja stuff + jinja_template = "ui_base_widget.j2" + env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) + template = env.get_template(jinja_template) + jinja_output = template.render( + jinja_template=jinja_template, + ui_name=ui_name, + clsname=clsname, + macro_names=macro_names, + widget_names=widget_names, + widget_name_to_class=widget_name_to_class, + macro_to_widget=macro_to_widget, + widget_to_macro=widget_to_macro, + widget_to_pre_templ_strs=widget_to_pre_templ_strs, + widget_to_pre_templ_lists=widget_to_pre_templ_lists, + ) + dst_path = designer_ui.removesuffix(".ui") + "_base.py" + with open(dst_path, "w") as fd: + fd.write(jinja_output) + + +macro_re = re.compile(r"\${(\S+)}") + + +def _get_macros(text_with_macro_sub: str) -> list[str]: + return macro_re.findall(text_with_macro_sub) + + +if __name__ == "__main__": + print(sys.argv) + designer_ui = sys.argv[1] + build_uic(designer_ui) + build_base_widget(designer_ui) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py new file mode 100644 index 0000000..db0a412 --- /dev/null +++ b/pcdswidgets/builder/designer_widget.py @@ -0,0 +1,65 @@ +""" +Helper for using designer to layout widgets. +""" + +from string import Template +from typing import ClassVar, Protocol + +from qtpy.QtWidgets import QWidget + + +class _UiForm(Protocol): + def setupUi(self, Form): ... + + def retranslateUi(self, Form): ... + + +class DesignerWidget(QWidget): + """Helper class for converting pydm displays for embedding to standalone widgets.""" + + # Loaded from uic + ui_form: ClassVar[type[_UiForm]] + # Macro name to widget names that include that macro + _macro_to_widget: ClassVar[dict[str, list[str]]] + # Widget name to required macros: all must be non-empty before updating + _widget_to_macro: ClassVar[dict[str, list[str]]] + # Widget name to per-property template to fill + _widget_to_pre_template: ClassVar[dict[str, list[tuple[str, str | list[str]]]]] + # Current values for each macro + _macro_values: dict[str, str] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ui_form.setupUi(self, self) # type: ignore + + def retranslateUi(self, *args, **kwargs): + """Required function for setupUi to work in __init__""" + self.ui_form.retranslateUi(self, *args, **kwargs) # type: ignore + + def _get_macro(self, macro_name: str) -> str: + return self._macro_values[macro_name] + + def _set_macro(self, macro_name: str, value: str): + self._macro_values[macro_name] = value + self._updates_for_macro(macro_name) + + def _updates_for_macro(self, macro_name: str): + for widget_name in self._macro_to_widget[macro_name]: + self._update_widget_for_macros(widget_name) + + def _update_widget_for_macros(self, widget_name: str): + needed_macros = self._widget_to_macro[widget_name] + if not all(self._macro_values[macro_name] for macro_name in needed_macros): + # Skip! Not ready! + return + widget = getattr(self, widget_name) + if not isinstance(widget, QWidget): + raise TypeError(f"{widget_name} is not a widget: {widget}") + for prop, templ in self._widget_to_pre_template[widget_name]: + if isinstance(templ, str): + value = Template(templ).substitute(self._macro_values) + elif isinstance(templ, list): + value = [Template(tp).substitute(self._macro_values) for tp in templ] + else: + raise TypeError(f"Unexpected template type, should be str or stringlist: {templ}") + widget.setProperty(prop, value) diff --git a/pcdswidgets/builder/ui/__init__.py b/pcdswidgets/builder/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/builder/ui/positioner.ui b/pcdswidgets/builder/ui/positioner.ui new file mode 100644 index 0000000..aec9dee --- /dev/null +++ b/pcdswidgets/builder/ui/positioner.ui @@ -0,0 +1,639 @@ + + + Form + + + + 0 + 0 + 400 + 125 + + + + + 0 + 0 + + + + + 400 + 125 + + + + + 650 + 200 + + + + Form + + + + 1 + + + QLayout::SetMinimumSize + + + 1 + + + 1 + + + 0 + + + 1 + + + + + + 0 + 0 + + + + QFrame::Box + + + QFrame::Sunken + + + + 1 + + + 1 + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 75 + 0 + + + + + 300 + 16777215 + + + + + 12 + 75 + true + + + + + + + false + + + ca://${MOTOR}.DESC + + + PyDMLabel::String + + + + + + + + + 0 + + + 0 + + + + + false + + + + 0 + 0 + + + + + 25 + 0 + + + + + 25 + 16777215 + + + + + + + false + + + ca://${MOTOR}.MOVN + + + false + + + true + + + + + + + + + 7 + + + 7 + + + 0 + + + 7 + + + + + + 15 + 15 + + + + + + + false + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 300 + 16777215 + + + + + 14 + 50 + false + + + + + + + Qt::AlignCenter + + + ca://${MOTOR}.RBV + + + PyDMLabel::Decimal + + + + + + + + 15 + 15 + + + + + + + false + + + + + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + + 60 + 16777215 + + + + + + + background-color: rgb(170, 0, 0); + + + Stop + + + ca://${MOTOR}.STOP + + + 1 + + + + + + + + + 30 + + + 30 + + + + + false + + + + 0 + 0 + + + + + 150 + 0 + + + + + 300 + 16777215 + + + + + 12 + + + + + + + 12 + + + Qt::AlignCenter + + + 4 + + + false + + + false + + + ca://${MOTOR}.VAL + + + PyDMLineEdit::Decimal + + + + + + + + + 7 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 60 + 0 + + + + + 150 + 16777215 + + + + + 10 + + + + + + + 0 + + + false + + + true + + + false + + + false + + + + + + ca://${MOTOR}.EGU + + + + + + + + + + + + 3 + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + 35 + 0 + + + + + 35 + 16777215 + + + + + + + << + + + ca://${MOTOR}.TWR + + + 1 + + + true + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + + 200 + 16777215 + + + + + 12 + + + + + + + Qt::AlignCenter + + + 4 + + + false + + + false + + + ca://${MOTOR}.TWV + + + + + + + + 0 + 0 + + + + + 35 + 0 + + + + + 35 + 16777215 + + + + + + + >> + + + ca://${MOTOR}.TWF + + + 1 + + + true + + + + + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 60 + 16777215 + + + + + + + + motor-expert-screen ${MOTOR} + + + + + + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMByteIndicator + QWidget +
pydm.widgets.byte
+
+ + PyDMLineEdit + QLineEdit +
pydm.widgets.line_edit
+
+ + PyDMPushButton + QPushButton +
pydm.widgets.pushbutton
+
+ + PyDMShellCommand + QPushButton +
pydm.widgets.shell_command
+
+
+ + +
diff --git a/pcdswidgets/builder/ui/positioner_row.ui b/pcdswidgets/builder/ui/positioner_row.ui new file mode 100644 index 0000000..070217e --- /dev/null +++ b/pcdswidgets/builder/ui/positioner_row.ui @@ -0,0 +1,664 @@ + + + Form + + + + 0 + 0 + 753 + 55 + + + + + 0 + 0 + + + + + 700 + 45 + + + + + 1000 + 55 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 11 + + + + QFrame::Box + + + QFrame::Sunken + + + + 1 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 35 + 0 + + + + + + + >> + + + false + + + false + + + + + + ca://${MOTOR}.TWF + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 15 + 15 + + + + + + + false + + + true + + + + + + ca://${MOTOR}.HLS + + + + 255 + 165 + 0 + + + + false + + + false + + + false + + + 1 + + + + Bit 0 + + + + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 125 + 16777215 + + + + + 10 + 75 + true + + + + + + + false + + + ca://${MOTOR}.DESC + + + PyDMLabel::String + + + + + + + + 75 + 0 + + + + + 75 + 16777215 + + + + + 10 + 50 + false + + + + + + + 0 + + + false + + + true + + + false + + + false + + + ca://${MOTOR}.EGU + + + + + + + + + + false + + + + motor-expert-screen ${MOTOR} + + + + + + + + + 60 + 0 + + + + + 150 + 16777215 + + + + + 11 + + + + + + + 3 + + + false + + + false + + + false + + + false + + + ca://${MOTOR}.TWV + + + + + + + + 0 + 0 + + + + + 15 + 15 + + + + + + + false + + + true + + + + + + ca://${MOTOR}.LLS + + + + 255 + 165 + 0 + + + + false + + + false + + + false + + + 1 + + + 0 + + + + Bit 0 + + + + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 14 + 50 + false + + + + + + + Qt::AlignCenter + + + 3 + + + false + + + false + + + false + + + true + + + ca://${MOTOR}.RBV + + + PyDMLabel::Decimal + + + + + + + false + + + + 0 + 0 + + + + + 15 + 15 + + + + + 15 + 15 + + + + + + + false + + + true + + + ca://${MOTOR}.MOVN + + + false + + + false + + + true + + + 1 + + + 0 + + + + Bit 0 + + + + + + + + + 35 + 0 + + + + + + + << + + + false + + + false + + + + + + ca://${MOTOR}.TWR + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 40 + 0 + + + + + 40 + 16777215 + + + + + + + background-color: rgb(170, 0, 0); + + + Stop + + + ca://${MOTOR}.STOP + + + 1 + + + + + + + false + + + + 0 + 0 + + + + + 75 + 0 + + + + + 11 + + + + + + + 3 + + + false + + + false + + + false + + + false + + + ca://${MOTOR}.VAL + + + PyDMLineEdit::Default + + + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMByteIndicator + QWidget +
pydm.widgets.byte
+
+ + PyDMLineEdit + QLineEdit +
pydm.widgets.line_edit
+
+ + PyDMPushButton + QPushButton +
pydm.widgets.pushbutton
+
+ + PyDMShellCommand + QPushButton +
pydm.widgets.shell_command
+
+
+ + +
diff --git a/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui b/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui new file mode 100644 index 0000000..ab0b01b --- /dev/null +++ b/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui @@ -0,0 +1,273 @@ + + + Form + + + + 0 + 0 + 725 + 100 + + + + + 725 + 100 + + + + + 825 + 100 + + + + Form + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + {} + + + ./positioner_row.ui + + + true + + + true + + + false + + + + + + + 6 + + + + + + 10 + + + + Motor temperature: + + + + + + + + 100 + 16777215 + + + + + 10 + + + + + + + 1 + + + false + + + false + + + false + + + true + + + + + + ca://${MOTOR}:ILOCK:TC_TEMP_RBV + + + false + + + + + + + + 10 + + + + Interlock: + + + + + + + + 15 + 15 + + + + + 15 + 15 + + + + + 1 + + + + + + + false + + + true + + + + + + ca://${MOTOR}:ILOCK:ACTIVE_RBV + + + + 255 + 0 + 0 + + + + + 0 + 255 + 0 + + + + false + + + false + + + false + + + QTabWidget::West + + + 1 + + + 0 + + + + Bit 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMByteIndicator + QWidget +
pydm.widgets.byte
+
+ + PyDMEmbeddedDisplay + QFrame +
pydm.widgets.embedded_display
+
+
+ + +
diff --git a/pcdswidgets/builder/ui/smaract_open_loop.ui b/pcdswidgets/builder/ui/smaract_open_loop.ui new file mode 100644 index 0000000..eae3206 --- /dev/null +++ b/pcdswidgets/builder/ui/smaract_open_loop.ui @@ -0,0 +1,440 @@ + + + Form + + + + 0 + 0 + 609 + 36 + + + + + 0 + 0 + + + + + 550 + 33 + + + + + 800 + 45 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 5 + + + 5 + + + 5 + + + + + + + + true + + + false + + + false + + + + + + + edm -eolc -x -m MOTOR=${MOTOR} /reg/g/pcds/epics/ioc/common/smaract/R1.0.8/motorScreens/mcs2_openloop.edl + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 12 + 75 + true + + + + + + + 0 + + + false + + + true + + + false + + + true + + + ca://${MOTOR}.DESC + + + PyDMLabel::String + + + + + + + false + + + + 0 + 0 + + + + + 100 + 0 + + + + + 16777215 + 16777215 + + + + + 12 + + + + + + + 12 + + + 0 + + + false + + + true + + + false + + + true + + + ca://${MOTOR}:TOTAL_STEP_COUNT + + + PyDMLineEdit::Decimal + + + + + + + + 32 + 0 + + + + + + + >> + + + false + + + false + + + ca://${MOTOR}:STEP_FORWARD.PROC + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + true + + + false + + + + + + + + 50 + 0 + + + + + 75 + 16777215 + + + + + 12 + + + + + + + 0 + + + false + + + true + + + false + + + false + + + ca://${MOTOR}:STEP_COUNT + + + + + + + + 40 + 0 + + + + + 40 + 16777215 + + + + + + + background-color: rgb(170, 0, 0); + + + Stop + + + false + + + false + + + ca://${MOTOR}.STOP + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 32 + 0 + + + + + + + << + + + false + + + false + + + ca://${MOTOR}:STEP_REVERSE.PROC + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + true + + + false + + + + + + + + 75 + 16777215 + + + + (steps) + + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMLineEdit + QLineEdit +
pydm.widgets.line_edit
+
+ + PyDMPushButton + QPushButton +
pydm.widgets.pushbutton
+
+ + PyDMShellCommand + QPushButton +
pydm.widgets.shell_command
+
+
+ + +
diff --git a/pcdswidgets/builder/ui_base_widget.j2 b/pcdswidgets/builder/ui_base_widget.j2 new file mode 100644 index 0000000..0eadbb6 --- /dev/null +++ b/pcdswidgets/builder/ui_base_widget.j2 @@ -0,0 +1,88 @@ +""" +Generated by jinja from {{ jinja_template }} with: +ui_name = {{ ui_name }} +clsname = {{ clsname }} +macro_names = {{ macro_names }} +widget_names = {{ widget_names }} +widget_name_to_class = {{ widget_name_to_class }} + +Other long required variables: +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" + +from qtpy.QtWidgets import * +from pydm.widgets import * + +from pcdswidgets.builder.designer_widget import DesignerWidget +from .{{ ui_name.removesuffix(".ui") }}_form import {{ clsname }} + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class {{ ui_name.removesuffix(".ui").title() }}WidgetBase(DesignerWidget): +{% for widget in widget_names %} + {{ widget }}: {{ widget_name_to_class[widget] }} +{% endfor %} + + ui_form = {{ clsname }} + _macro_to_widget = { +{% for macro in macro_names %} + "{{ macro }}": [ +{% for widget in macro_to_widget[macro] %} + "{{ widget }}", +{% endfor %} + ], +{% endfor %} + } + _widget_to_macro = { +{% for widget in widget_names %} + "{{ widget }}": [ +{% for macro in widget_to_macro[widget] %} + "{{ macro }}", +{% endfor %} + ], +{% endfor %} + } + _widget_to_pre_template = { +{% for widget in widget_names %} + "{{ widget }}": [ +{% for prop, value in widget_to_pre_templ_strs[widget] %} + ("{{ prop }}", "{{ value }}"), +{% endfor %} +{% for prop, value_list in widget_to_pre_templ_lists[widget] %} + ( + "{{ prop }}", + [ +{% for value in value_list %} + "{{ value }}", +{% endfor %} + ] + ), +{% endfor %} + ], +{% endfor %} + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { +{% for macro in macro_names %} + "{{ macro }}": "", +{% endfor %} + } + +{% for macro in macro_names %} + def get_{{ macro.lower() }}(self) -> str: + return self._get_macro("{{ macro }}") + + def set_{{ macro.lower() }}(self, value: str) -> None: + self._set_macro("{{ macro }}", value) + + {{ macro.lower() }} = pyqtProperty(str, get_{{ macro.lower() }}, set_{{ macro.lower() }}) +{% endfor %} diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index 26f019c..26fc220 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -8,10 +8,16 @@ import inspect import pcdswidgets.eps_byteindicator +import pcdswidgets.motion import pcdswidgets.table import pcdswidgets.vacuum -INCLUDE_MODULES = [pcdswidgets.eps_byteindicator, pcdswidgets.table, pcdswidgets.vacuum] +INCLUDE_MODULES = [ + pcdswidgets.eps_byteindicator, + pcdswidgets.motion, + pcdswidgets.table, + pcdswidgets.vacuum, +] def main(): diff --git a/pcdswidgets/motion/__init__.py b/pcdswidgets/motion/__init__.py new file mode 100644 index 0000000..a6fbb8b --- /dev/null +++ b/pcdswidgets/motion/__init__.py @@ -0,0 +1,3 @@ +from .motors import PositionerWidget + +__all__ = ["PositionerWidget"] diff --git a/pcdswidgets/motion/motors.py b/pcdswidgets/motion/motors.py new file mode 100644 index 0000000..a6b8514 --- /dev/null +++ b/pcdswidgets/motion/motors.py @@ -0,0 +1,8 @@ +from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase + + +class PositionerWidget(PositionerWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pyproject.toml b/pyproject.toml index 91ad876..bf0a4f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" +PositionerWidget = "pcdswidgets.motion.motors:PositionerWidget" ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" @@ -75,8 +76,8 @@ content-type = "text/markdown" [tool.ruff] -line-length = 88 -exclude = [".git", "__pycache__", "build", "dist", "pcdswidgets/_version.py"] +line-length = 120 +exclude = [".git", "__pycache__", "build", "dist", "*/_version.py", "**/ui/*.py"] [tool.ruff.lint] select = ["C", "E", "F", "W", "B", "I"] From 2af82d3a68cb42f80616dfeaf8b791e90d794ba5 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 10:19:02 -0800 Subject: [PATCH 003/104] ENH: add Makefile for convenient rebuild/iteration on layouts --- Makefile | 17 +++++++++++++++++ pcdswidgets/builder/build.py | 12 ++++++++---- pcdswidgets/builder/ui_base_widget.j2 | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3db2cfb --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: all clean + +UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) +PY_FORMS := $(UI_SOURCE:.ui=_form.py) +PY_BASE := $(UI_SOURCE:.ui=_base.py) + +all: $(PY_FORMS) $(PY_BASE) + +clean: + rm $(PY_FORMS) + rm $(PY_BASE) + +$(PY_FORMS): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswidgets/builder/*.j2) + python -m pcdswidgets.builder.build uic $(@:_form.py=.ui) + +$(PY_BASE): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswidgets/builder/*.j2) + python -m pcdswidgets.builder.build base $(@:_base.py=.ui) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 06e9633..6b9f4f1 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -138,7 +138,11 @@ def _get_macros(text_with_macro_sub: str) -> list[str]: if __name__ == "__main__": - print(sys.argv) - designer_ui = sys.argv[1] - build_uic(designer_ui) - build_base_widget(designer_ui) + mode = sys.argv[1] + designer_ui = sys.argv[2] + if mode == "uic": + build_uic(designer_ui) + elif mode == "base": + build_base_widget(designer_ui) + else: + raise ValueError(f"Invalid mode {mode}, must be uic or base") diff --git a/pcdswidgets/builder/ui_base_widget.j2 b/pcdswidgets/builder/ui_base_widget.j2 index 0eadbb6..b2f569f 100644 --- a/pcdswidgets/builder/ui_base_widget.j2 +++ b/pcdswidgets/builder/ui_base_widget.j2 @@ -27,7 +27,7 @@ except ImportError: class {{ ui_name.removesuffix(".ui").title() }}WidgetBase(DesignerWidget): {% for widget in widget_names %} - {{ widget }}: {{ widget_name_to_class[widget] }} + {{ widget }}: "{{ widget_name_to_class[widget] }}" {% endfor %} ui_form = {{ clsname }} From baa28cd4c2cd5cfdc9e2115bfa84d5b485cb09ad Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 11:19:52 -0800 Subject: [PATCH 004/104] ENH: implement more automation for setting up designer entrypoints --- Makefile | 5 ++++- pcdswidgets/entrypoint_widgets.py | 28 ++++++++++++++++++++++++---- pcdswidgets/motion/__init__.py | 4 +--- pcdswidgets/motion/motors.py | 24 ++++++++++++++++++++++++ pyproject.toml | 7 ++++++- uv.lock | 15 ++++++++++++++- 6 files changed, 73 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 3db2cfb..a8d0102 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) PY_FORMS := $(UI_SOURCE:.ui=_form.py) PY_BASE := $(UI_SOURCE:.ui=_base.py) -all: $(PY_FORMS) $(PY_BASE) +all: $(PY_FORMS) $(PY_BASE) pyproject.toml clean: rm $(PY_FORMS) @@ -15,3 +15,6 @@ $(PY_FORMS): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswi $(PY_BASE): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswidgets/builder/*.j2) python -m pcdswidgets.builder.build base $(@:_base.py=.ui) + +pyproject.toml: $(shell find . -name "*.py") + python -m pcdswidgets.entrypoint_widgets diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index 26fc220..b51630d 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -6,6 +6,11 @@ """ import inspect +from pathlib import Path +from typing import cast + +import tomlkit as tk +import tomlkit.items as tki import pcdswidgets.eps_byteindicator import pcdswidgets.motion @@ -21,13 +26,28 @@ def main(): - lines = set() + key_val: list[tuple[str, str]] = [] for module in INCLUDE_MODULES: for name, obj in inspect.getmembers(module, inspect.isclass): if hasattr(obj, "_qt_designer_"): - lines.add(f'{name} = "{obj.__module__}:{name}"') - for line in sorted(lines): - print(line) + key_val.append((name, f"{obj.__module__}:{name}")) + key_val.sort() + + pyproj = Path(__file__).parent.parent / "pyproject.toml" + if not pyproj.exists(): + raise RuntimeError(f"Project file {pyproj} missing?") + with open(pyproj, "r") as fd: + toml_doc = tk.parse(fd.read()) + + project_table = cast(tki.Table, toml_doc["project"]) + entrypoint_table = cast(tki.Table, project_table["entry-points"]) + widget_table = cast(tki.Table, entrypoint_table["pydm.widget"]) + widget_table.clear() + for key, value in key_val: + widget_table[key] = value + + with open(pyproj, "w") as fd: + tk.dump(toml_doc, fd) if __name__ == "__main__": diff --git a/pcdswidgets/motion/__init__.py b/pcdswidgets/motion/__init__.py index a6fbb8b..502cdb4 100644 --- a/pcdswidgets/motion/__init__.py +++ b/pcdswidgets/motion/__init__.py @@ -1,3 +1 @@ -from .motors import PositionerWidget - -__all__ = ["PositionerWidget"] +from .motors import * # noqa: F403 diff --git a/pcdswidgets/motion/motors.py b/pcdswidgets/motion/motors.py index a6b8514..f05f233 100644 --- a/pcdswidgets/motion/motors.py +++ b/pcdswidgets/motion/motors.py @@ -1,4 +1,7 @@ from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase +from pcdswidgets.builder.ui.positioner_row_base import Positioner_RowWidgetBase +from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import Positioner_Row_Tc_InterlockWidgetBase +from pcdswidgets.builder.ui.smaract_open_loop_base import Smaract_Open_LoopWidgetBase class PositionerWidget(PositionerWidgetBase): @@ -6,3 +9,24 @@ class PositionerWidget(PositionerWidgetBase): "group": "PCDS Motion", "is_container": False, } + + +class PositionerRowWidget(Positioner_RowWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } + + +class PositionerRowTcInterlockWidget(Positioner_Row_Tc_InterlockWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } + + +class SmaractOpenLoopWidget(Smaract_Open_LoopWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pyproject.toml b/pyproject.toml index bf0a4f1..b38f862 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,12 +44,15 @@ NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" +PositionerRowTcInterlockWidget = "pcdswidgets.motion.motors:PositionerRowTcInterlockWidget" +PositionerRowWidget = "pcdswidgets.motion.motors:PositionerRowWidget" PositionerWidget = "pcdswidgets.motion.motors:PositionerWidget" ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" +SmaractOpenLoopWidget = "pcdswidgets.motion.motors:SmaractOpenLoopWidget" TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] @@ -64,6 +67,9 @@ test = [ "pytest-qt>=4.5.0", "pytest-timeout>=2.4.0", ] +dev = [ + "tomlkit>=0.14.0", +] [tool.setuptools.packages.find] where = [ ".",] @@ -74,7 +80,6 @@ namespaces = false file = "README.md" content-type = "text/markdown" - [tool.ruff] line-length = 120 exclude = [".git", "__pycache__", "build", "dist", "*/_version.py", "**/ui/*.py"] diff --git a/uv.lock b/uv.lock index c8e58d9..c079b2b 100644 --- a/uv.lock +++ b/uv.lock @@ -324,6 +324,9 @@ dependencies = [ ] [package.optional-dependencies] +dev = [ + { name = "tomlkit" }, +] docs = [ { name = "docs-versions-menu" }, { name = "sphinx" }, @@ -348,8 +351,9 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'docs'", specifier = ">=9.1.0" }, { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=3.1.0" }, { name = "sphinxcontrib-jquery", marker = "extra == 'docs'", specifier = ">=4.1" }, + { name = "tomlkit", marker = "extra == 'dev'", specifier = ">=0.14.0" }, ] -provides-extras = ["docs", "test"] +provides-extras = ["docs", "test", "dev"] [[package]] name = "pluggy" @@ -683,6 +687,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From a42684e9cae668bc7ae60e105b3840921562b309 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 13:27:33 -0800 Subject: [PATCH 005/104] FIX/ENH: resolve circular imports, make widget discovery even more automatic --- Makefile | 15 +++-- .../builder/ui/positioner_row_tc_interlock.ui | 66 ++++++++----------- pcdswidgets/entrypoint_widgets.py | 38 +++++++---- pcdswidgets/motion/__init__.py | 1 - pcdswidgets/motion/motors.py | 32 --------- .../positioner_row_tc_interlock_widget.py | 8 +++ pcdswidgets/motion/positioner_row_widget.py | 8 +++ pcdswidgets/motion/positioner_widget.py | 8 +++ .../motion/smaract_open_loop_widget.py | 8 +++ pyproject.toml | 8 +-- 10 files changed, 99 insertions(+), 93 deletions(-) delete mode 100644 pcdswidgets/motion/motors.py create mode 100644 pcdswidgets/motion/positioner_row_tc_interlock_widget.py create mode 100644 pcdswidgets/motion/positioner_row_widget.py create mode 100644 pcdswidgets/motion/positioner_widget.py create mode 100644 pcdswidgets/motion/smaract_open_loop_widget.py diff --git a/Makefile b/Makefile index a8d0102..40f6d46 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,23 @@ .PHONY: all clean UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) -PY_FORMS := $(UI_SOURCE:.ui=_form.py) +PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(shell find pcdswidgets -name "*.py")) +JINJA_SOURCE := $(wildcard pcdswidgets/builder/*.j2) + +PY_FORM := $(UI_SOURCE:.ui=_form.py) PY_BASE := $(UI_SOURCE:.ui=_base.py) -all: $(PY_FORMS) $(PY_BASE) pyproject.toml +all: $(PY_FORM) $(PY_BASE) pyproject.toml clean: - rm $(PY_FORMS) + rm $(PY_FORM) rm $(PY_BASE) -$(PY_FORMS): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswidgets/builder/*.j2) +$(PY_FORM): $(UI_SOURCE) $(PY_SOURCE) $(JINJA_SOURCE) python -m pcdswidgets.builder.build uic $(@:_form.py=.ui) -$(PY_BASE): $(UI_SOURCE) $(wildcard pcdswidgets/builder/*.py) $(wildcard pcdswidgets/builder/*.j2) +$(PY_BASE): $(UI_SOURCE) $(PY_SOURCE) $(JINJA_SOURCE) python -m pcdswidgets.builder.build base $(@:_base.py=.ui) -pyproject.toml: $(shell find . -name "*.py") +pyproject.toml: $(PY_SOURCE) python -m pcdswidgets.entrypoint_widgets diff --git a/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui b/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui index ab0b01b..296e499 100644 --- a/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui +++ b/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui @@ -66,24 +66,12 @@ 5 - + - - {} - - - ./positioner_row.ui - - - true - - - true - - - false + + ${MOTOR} @@ -120,28 +108,28 @@ - + 1 - + false - + false - + false - + true - + - + ca://${MOTOR}:ILOCK:TC_TEMP_RBV - + false @@ -180,51 +168,51 @@ - + false - + true - + - + ca://${MOTOR}:ILOCK:ACTIVE_RBV - + 255 0 0 - + 0 255 0 - + false - + false - + false - + QTabWidget::West - + 1 - + 0 - + Bit 0 @@ -263,9 +251,9 @@
pydm.widgets.byte
- PyDMEmbeddedDisplay - QFrame -
pydm.widgets.embedded_display
+ PositionerRowWidget + QWidget +
pcdswidgets.motion.positioner_row_widget
diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index b51630d..7ad3327 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -5,30 +5,32 @@ python -m pcdswidgets.entrypoint_widgets """ +import importlib import inspect +import pkgutil from pathlib import Path -from typing import cast +from types import ModuleType +from typing import Iterator, cast import tomlkit as tk import tomlkit.items as tki -import pcdswidgets.eps_byteindicator -import pcdswidgets.motion -import pcdswidgets.table -import pcdswidgets.vacuum +SKIP_WIDGETS = [ + "PCDSSymbolBase", +] -INCLUDE_MODULES = [ - pcdswidgets.eps_byteindicator, - pcdswidgets.motion, - pcdswidgets.table, - pcdswidgets.vacuum, +SKIP_MODULES = [ + ".tests", + ".demo", ] def main(): key_val: list[tuple[str, str]] = [] - for module in INCLUDE_MODULES: + for module in iter_submodules(): for name, obj in inspect.getmembers(module, inspect.isclass): + if name in SKIP_WIDGETS: + continue if hasattr(obj, "_qt_designer_"): key_val.append((name, f"{obj.__module__}:{name}")) key_val.sort() @@ -50,5 +52,19 @@ def main(): tk.dump(toml_doc, fd) +def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: + """Recursively yield all submodules of a package.""" + if any(mod in package for mod in SKIP_MODULES): + return + module = importlib.import_module(package) + yield module + try: + for _, modname, _ in pkgutil.walk_packages(module.__path__, module.__name__ + "."): + if "__main__" not in modname: + yield from iter_submodules(modname) + except AttributeError: + ... + + if __name__ == "__main__": main() diff --git a/pcdswidgets/motion/__init__.py b/pcdswidgets/motion/__init__.py index 502cdb4..e69de29 100644 --- a/pcdswidgets/motion/__init__.py +++ b/pcdswidgets/motion/__init__.py @@ -1 +0,0 @@ -from .motors import * # noqa: F403 diff --git a/pcdswidgets/motion/motors.py b/pcdswidgets/motion/motors.py deleted file mode 100644 index f05f233..0000000 --- a/pcdswidgets/motion/motors.py +++ /dev/null @@ -1,32 +0,0 @@ -from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase -from pcdswidgets.builder.ui.positioner_row_base import Positioner_RowWidgetBase -from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import Positioner_Row_Tc_InterlockWidgetBase -from pcdswidgets.builder.ui.smaract_open_loop_base import Smaract_Open_LoopWidgetBase - - -class PositionerWidget(PositionerWidgetBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - } - - -class PositionerRowWidget(Positioner_RowWidgetBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - } - - -class PositionerRowTcInterlockWidget(Positioner_Row_Tc_InterlockWidgetBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - } - - -class SmaractOpenLoopWidget(Smaract_Open_LoopWidgetBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - } diff --git a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py new file mode 100644 index 0000000..0fc43cf --- /dev/null +++ b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py @@ -0,0 +1,8 @@ +from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import Positioner_Row_Tc_InterlockWidgetBase + + +class PositionerRowTcInterlockWidget(Positioner_Row_Tc_InterlockWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pcdswidgets/motion/positioner_row_widget.py b/pcdswidgets/motion/positioner_row_widget.py new file mode 100644 index 0000000..403ede6 --- /dev/null +++ b/pcdswidgets/motion/positioner_row_widget.py @@ -0,0 +1,8 @@ +from pcdswidgets.builder.ui.positioner_row_base import Positioner_RowWidgetBase + + +class PositionerRowWidget(Positioner_RowWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pcdswidgets/motion/positioner_widget.py b/pcdswidgets/motion/positioner_widget.py new file mode 100644 index 0000000..a6b8514 --- /dev/null +++ b/pcdswidgets/motion/positioner_widget.py @@ -0,0 +1,8 @@ +from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase + + +class PositionerWidget(PositionerWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pcdswidgets/motion/smaract_open_loop_widget.py b/pcdswidgets/motion/smaract_open_loop_widget.py new file mode 100644 index 0000000..14f0c5b --- /dev/null +++ b/pcdswidgets/motion/smaract_open_loop_widget.py @@ -0,0 +1,8 @@ +from pcdswidgets.builder.ui.smaract_open_loop_base import Smaract_Open_LoopWidgetBase + + +class SmaractOpenLoopWidget(Smaract_Open_LoopWidgetBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + } diff --git a/pyproject.toml b/pyproject.toml index b38f862..8c086c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,15 +44,15 @@ NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" -PositionerRowTcInterlockWidget = "pcdswidgets.motion.motors:PositionerRowTcInterlockWidget" -PositionerRowWidget = "pcdswidgets.motion.motors:PositionerRowWidget" -PositionerWidget = "pcdswidgets.motion.motors:PositionerWidget" +PositionerRowTcInterlockWidget = "pcdswidgets.motion.positioner_row_tc_interlock_widget:PositionerRowTcInterlockWidget" +PositionerRowWidget = "pcdswidgets.motion.positioner_row_widget:PositionerRowWidget" +PositionerWidget = "pcdswidgets.motion.positioner_widget:PositionerWidget" ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" -SmaractOpenLoopWidget = "pcdswidgets.motion.motors:SmaractOpenLoopWidget" +SmaractOpenLoopWidget = "pcdswidgets.motion.smaract_open_loop_widget:SmaractOpenLoopWidget" TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] From 2f858115937da2e9b41a91cc9f100124eb491e16 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 13:31:36 -0800 Subject: [PATCH 006/104] FIX: don't re-make when just the version updates --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 40f6d46..820e2be 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) -PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(shell find pcdswidgets -name "*.py")) +PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) JINJA_SOURCE := $(wildcard pcdswidgets/builder/*.j2) PY_FORM := $(UI_SOURCE:.ui=_form.py) From d6b41c2a035e5f7176a2e4cdba2e244fef5a83bc Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 13:51:36 -0800 Subject: [PATCH 007/104] MNT: normalize generated base class names --- pcdswidgets/builder/build.py | 6 ++++-- pcdswidgets/builder/ui_base_widget.j2 | 9 +++++---- pcdswidgets/motion/positioner_row_tc_interlock_widget.py | 4 ++-- pcdswidgets/motion/positioner_row_widget.py | 4 ++-- pcdswidgets/motion/smaract_open_loop_widget.py | 4 ++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 6b9f4f1..def8188 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -73,10 +73,11 @@ def build_base_widget(designer_ui: str): top_level_widget = tree.find("widget") if top_level_widget is None: raise RuntimeError("No top level widget in ui file") - clsname = f"Ui_{top_level_widget.attrib["name"]}" + form_cls = f"Ui_{top_level_widget.attrib["name"]}" # We're done parsing, now we bring the info into a good form for the jinja template ui_name = os.path.basename(designer_ui) + base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "WidgetBase" macro_set: set[str] = set() widget_set: set[str] = set() macro_to_widget: dict[str, list[str]] = defaultdict(list) @@ -116,7 +117,8 @@ def build_base_widget(designer_ui: str): jinja_output = template.render( jinja_template=jinja_template, ui_name=ui_name, - clsname=clsname, + form_cls=form_cls, + base_cls=base_cls, macro_names=macro_names, widget_names=widget_names, widget_name_to_class=widget_name_to_class, diff --git a/pcdswidgets/builder/ui_base_widget.j2 b/pcdswidgets/builder/ui_base_widget.j2 index b2f569f..ecc086a 100644 --- a/pcdswidgets/builder/ui_base_widget.j2 +++ b/pcdswidgets/builder/ui_base_widget.j2 @@ -1,7 +1,8 @@ """ Generated by jinja from {{ jinja_template }} with: ui_name = {{ ui_name }} -clsname = {{ clsname }} +form_cls = {{ form_cls }} +base_cls = {{ base_cls }} macro_names = {{ macro_names }} widget_names = {{ widget_names }} widget_name_to_class = {{ widget_name_to_class }} @@ -17,7 +18,7 @@ from qtpy.QtWidgets import * from pydm.widgets import * from pcdswidgets.builder.designer_widget import DesignerWidget -from .{{ ui_name.removesuffix(".ui") }}_form import {{ clsname }} +from .{{ ui_name.removesuffix(".ui") }}_form import {{ form_cls }} try: from qtpy.QtCore import pyqtProperty @@ -25,12 +26,12 @@ except ImportError: from qtpy.QtCore import Property as pyqtProperty # type: ignore -class {{ ui_name.removesuffix(".ui").title() }}WidgetBase(DesignerWidget): +class {{ base_cls }}(DesignerWidget): {% for widget in widget_names %} {{ widget }}: "{{ widget_name_to_class[widget] }}" {% endfor %} - ui_form = {{ clsname }} + ui_form = {{ form_cls }} _macro_to_widget = { {% for macro in macro_names %} "{{ macro }}": [ diff --git a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py index 0fc43cf..8a7955b 100644 --- a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py +++ b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py @@ -1,7 +1,7 @@ -from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import Positioner_Row_Tc_InterlockWidgetBase +from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import PositionerRowTcInterlockWidgetBase -class PositionerRowTcInterlockWidget(Positioner_Row_Tc_InterlockWidgetBase): +class PositionerRowTcInterlockWidget(PositionerRowTcInterlockWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pcdswidgets/motion/positioner_row_widget.py b/pcdswidgets/motion/positioner_row_widget.py index 403ede6..917a526 100644 --- a/pcdswidgets/motion/positioner_row_widget.py +++ b/pcdswidgets/motion/positioner_row_widget.py @@ -1,7 +1,7 @@ -from pcdswidgets.builder.ui.positioner_row_base import Positioner_RowWidgetBase +from pcdswidgets.builder.ui.positioner_row_base import PositionerRowWidgetBase -class PositionerRowWidget(Positioner_RowWidgetBase): +class PositionerRowWidget(PositionerRowWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pcdswidgets/motion/smaract_open_loop_widget.py b/pcdswidgets/motion/smaract_open_loop_widget.py index 14f0c5b..6217942 100644 --- a/pcdswidgets/motion/smaract_open_loop_widget.py +++ b/pcdswidgets/motion/smaract_open_loop_widget.py @@ -1,7 +1,7 @@ -from pcdswidgets.builder.ui.smaract_open_loop_base import Smaract_Open_LoopWidgetBase +from pcdswidgets.builder.ui.smaract_open_loop_base import SmaractOpenLoopWidgetBase -class SmaractOpenLoopWidget(Smaract_Open_LoopWidgetBase): +class SmaractOpenLoopWidget(SmaractOpenLoopWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, From 65aece191b6ab2b8b7610c2c5d88daeb43d91098 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 14:21:24 -0800 Subject: [PATCH 008/104] REF: reduce complexity of build function by splitting it up --- pcdswidgets/builder/build.py | 175 ++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 66 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index def8188..fb9bb8c 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -1,3 +1,4 @@ +import dataclasses import os import re import subprocess @@ -33,12 +34,55 @@ def build_base_widget(designer_ui: str): See ui_base_widget.j2, which is the jinja template for these output files. """ - # Parse the .ui file and collect information about each widget + # Parse the file + ui_info = get_ui_info(designer_ui) + + # Bring the info into a good form for the jinja template + ui_name = os.path.basename(designer_ui) + base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "WidgetBase" + info_for_jinja = process_widget_macros(ui_info) + + macro_names = sorted(info_for_jinja.macro_set) + widget_names = sorted(info_for_jinja.widget_set) + + # Fill the template + jinja_template = "ui_base_widget.j2" + env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) + template = env.get_template(jinja_template) + jinja_output = template.render( + jinja_template=jinja_template, + ui_name=ui_name, + form_cls=ui_info.form_cls, + base_cls=base_cls, + macro_names=macro_names, + widget_names=widget_names, + widget_name_to_class=ui_info.widget_name_to_class, + macro_to_widget=info_for_jinja.macro_to_widget, + widget_to_macro=info_for_jinja.widget_to_macro, + widget_to_pre_templ_strs=info_for_jinja.widget_to_pre_templ_strs, + widget_to_pre_templ_lists=info_for_jinja.widget_to_pre_templ_lists, + ) + dst_path = designer_ui.removesuffix(".ui") + "_base.py" + with open(dst_path, "w") as fd: + fd.write(jinja_output) + + +@dataclasses.dataclass +class UiInfo: + """Information parsed from a .ui file.""" + + widget_name_to_class: dict[str, str] + widget_macros: dict[str, dict[str, str | list[str]]] + form_cls: str + + +def get_ui_info(designer_ui: str) -> UiInfo: + """Parse a .ui file and collect information about each widget.""" # Need a name to class mapping for the IDE type hints widget_name_to_class: dict[str, str] = {} # Need to keep track of which widget properties have macros # widget_macros[widget_name][property_name] == "${MACRO} in context" - widget_macros: dict[str, dict[str, str | list[str]]] = {} + widget_macros: dict[str, dict[str, str | list[str]]] = defaultdict(dict) tree = ET.parse(designer_ui) for widget in tree.iter("widget"): @@ -46,46 +90,66 @@ def build_base_widget(designer_ui: str): cls = widget.attrib["class"] widget_name_to_class[name] = cls for prop in widget.findall("property"): - # Looking for string and stringlist only - str_node = prop.find("string") - if str_node is not None and str_node.text is not None: - # We have simple text! - if "${" in str_node.text: - try: - widget_macros[name][prop.attrib["name"]] = str_node.text - except KeyError: - widget_macros[name] = {prop.attrib["name"]: str_node.text} - continue - strlist_node = prop.find("stringlist") - if strlist_node is not None: - # We have a list of strings! Some may have macros. - all_str_nodes = strlist_node.findall("string") - all_str_literals = [node.text for node in all_str_nodes if node.text is not None] - for text in all_str_literals: - if "${" in text: - try: - widget_macros[name][prop.attrib["name"]] = all_str_literals - except KeyError: - widget_macros[name] = {prop.attrib["name"]: all_str_literals} + add_prop_to_widget_macros(widget_macros, name, prop) # Need to get the name of the form class, which is "Ui_" and the name of the top-level widget # Usually this ends up being "Ui_Form" with default naming but the user can change this top_level_widget = tree.find("widget") if top_level_widget is None: raise RuntimeError("No top level widget in ui file") - form_cls = f"Ui_{top_level_widget.attrib["name"]}" + form_cls = f"Ui_{top_level_widget.attrib['name']}" - # We're done parsing, now we bring the info into a good form for the jinja template - ui_name = os.path.basename(designer_ui) - base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "WidgetBase" - macro_set: set[str] = set() - widget_set: set[str] = set() - macro_to_widget: dict[str, list[str]] = defaultdict(list) - widget_to_macro: dict[str, list[str]] = {} - widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] = defaultdict(list) - widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] = defaultdict(list) - - for widget_name, prop_info in widget_macros.items(): + return UiInfo( + widget_name_to_class=widget_name_to_class, + widget_macros=widget_macros, + form_cls=form_cls, + ) + + +def add_prop_to_widget_macros(widget_macros: defaultdict[str, dict[str, str | list[str]]], name: str, prop: ET.Element): + """Incorporate a single property into the macros dict if there is a macro in it.""" + # Looking for string and stringlist only + str_node = prop.find("string") + if str_node is not None and str_node.text is not None: + # We have simple text! + if "${" in str_node.text: + widget_macros[name][prop.attrib["name"]] = str_node.text + return + strlist_node = prop.find("stringlist") + if strlist_node is not None: + # We have a list of strings! Some may have macros. + all_str_nodes = strlist_node.findall("string") + all_str_literals = [node.text for node in all_str_nodes if node.text is not None] + for text in all_str_literals: + if "${" in text: + widget_macros[name][prop.attrib["name"]] = all_str_literals + return + + +@dataclasses.dataclass +class InfoForJinja: + """Distilled widget and macro information for easily filling in the jinja template.""" + + macro_set: set[str] + widget_set: set[str] + macro_to_widget: dict[str, list[str]] + widget_to_macro: dict[str, list[str]] + widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] + widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] + + +def process_widget_macros(ui_info: UiInfo) -> InfoForJinja: + """Convert the raw ui info into a more useful form for filling the jinja template.""" + ij = InfoForJinja( + macro_set=set(), + widget_set=set(), + macro_to_widget=defaultdict(list), + widget_to_macro={}, + widget_to_pre_templ_strs=defaultdict(list), + widget_to_pre_templ_lists=defaultdict(list), + ) + + for widget_name, prop_info in ui_info.widget_macros.items(): macros_here = set() str_opts: list[tuple[str, str]] = [] list_opts: list[tuple[str, list[str]]] = [] @@ -99,43 +163,22 @@ def build_base_widget(designer_ui: str): macros_here.update(_get_macros(val)) else: raise TypeError(f"Invalid macro type: {value_with_macro}") - macro_set.update(macros_here) - widget_set.add(widget_name) - for macro in macro_set: - macro_to_widget[macro].append(widget_name) - widget_to_macro[widget_name] = sorted(macros_here) - widget_to_pre_templ_strs[widget_name].extend(str_opts) - widget_to_pre_templ_lists[widget_name].extend(list_opts) - - macro_names = sorted(macro_set) - widget_names = sorted(widget_set) - - # And last, standard jinja stuff - jinja_template = "ui_base_widget.j2" - env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) - template = env.get_template(jinja_template) - jinja_output = template.render( - jinja_template=jinja_template, - ui_name=ui_name, - form_cls=form_cls, - base_cls=base_cls, - macro_names=macro_names, - widget_names=widget_names, - widget_name_to_class=widget_name_to_class, - macro_to_widget=macro_to_widget, - widget_to_macro=widget_to_macro, - widget_to_pre_templ_strs=widget_to_pre_templ_strs, - widget_to_pre_templ_lists=widget_to_pre_templ_lists, - ) - dst_path = designer_ui.removesuffix(".ui") + "_base.py" - with open(dst_path, "w") as fd: - fd.write(jinja_output) + ij.macro_set.update(macros_here) + ij.widget_set.add(widget_name) + for macro in ij.macro_set: + ij.macro_to_widget[macro].append(widget_name) + ij.widget_to_macro[widget_name] = sorted(macros_here) + ij.widget_to_pre_templ_strs[widget_name].extend(str_opts) + ij.widget_to_pre_templ_lists[widget_name].extend(list_opts) + + return ij macro_re = re.compile(r"\${(\S+)}") def _get_macros(text_with_macro_sub: str) -> list[str]: + """Helper for getting the name of each macro in use in a macro string.""" return macro_re.findall(text_with_macro_sub) From 434d6558ff063e8235233fd918243592f44accb7 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 15:28:09 -0800 Subject: [PATCH 009/104] ENH: add designer dialog extensions for macro and rules to auto widgets --- pcdswidgets/builder/designer_widget.py | 98 +++++++++++++++++++++++++- pyproject.toml | 6 ++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index db0a412..0385519 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -3,9 +3,11 @@ """ from string import Template -from typing import ClassVar, Protocol +from typing import Any, ClassVar, Protocol -from qtpy.QtWidgets import QWidget +from pydm.widgets.base import PyDMPrimitiveWidget +from pydm.widgets.qtplugin_extensions import RulesExtension +from qtpy.QtWidgets import QAction, QDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget class _UiForm(Protocol): @@ -14,11 +16,13 @@ def setupUi(self, Form): ... def retranslateUi(self, Form): ... -class DesignerWidget(QWidget): +class DesignerWidget(QWidget, PyDMPrimitiveWidget): # type: ignore """Helper class for converting pydm displays for embedding to standalone widgets.""" # Loaded from uic ui_form: ClassVar[type[_UiForm]] + # Tells PyDM to include in designer + _qt_designer_: dict[str, Any] # Macro name to widget names that include that macro _macro_to_widget: ClassVar[dict[str, list[str]]] # Widget name to required macros: all must be non-empty before updating @@ -28,6 +32,18 @@ class DesignerWidget(QWidget): # Current values for each macro _macro_values: dict[str, str] + def __init_subclass__(cls): + super().__init_subclass__() + # Extend the _qt_designer_ marker if it exists to include a quick editor for macro vals + new_ext = [MacroEditExtension, RulesExtension] + try: + cls._qt_designer_["extensions"].extend(new_ext) + except (AttributeError, KeyError): + try: + cls._qt_designer_["extensions"] = new_ext + except AttributeError: + ... + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ui_form.setupUi(self, self) # type: ignore @@ -63,3 +79,79 @@ def _update_widget_for_macros(self, widget_name: str): else: raise TypeError(f"Unexpected template type, should be str or stringlist: {templ}") widget.setProperty(prop, value) + + +class MacroEditExtension: + """ + Adds helpful macro editing options in designer on double or right click. + + See the BasicSettingsExtension from PyDM + """ + + def __init__(self, widget: DesignerWidget): + self.widget = widget + self.edit_macros_action = QAction("&Edit Core Properties", self.widget) + self.edit_macros_action.triggered.connect(self.open_dialog) + + def actions(self) -> list[QAction]: + """ + PyDM checks this to decide which actions to prepent in designer. The first action is mapped to double-click. + """ + return [self.edit_macros_action] + + def open_dialog(self): + dialog = MacroValueEditor(self.widget, parent=None) + dialog.exec_() + + +class MacroValueEditor(QDialog): + """ + Dialog for MacroEditExtension + + See the BasicSettingsEditor from PyDM + """ + + def __init__(self, widget: DesignerWidget, parent: QWidget | None): + super().__init__(parent) + self.widget = widget + self.edit_widgets: dict[str, QLineEdit] = {} + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Widget Core Settings Editor") + outer_layout = QVBoxLayout() + outer_layout.setContentsMargins(5, 5, 5, 5) + outer_layout.setSpacing(5) + self.setLayout(outer_layout) + + edit_form_layout = QFormLayout() + outer_layout.addLayout(edit_form_layout) + + for macro_name, value in self.widget._macro_values.items(): + self.edit_widgets[macro_name] = QLineEdit() + self.edit_widgets[macro_name].setText(value) + edit_form_layout.addRow(macro_name.lower(), self.edit_widgets[macro_name]) + + button_layout = QHBoxLayout() + outer_layout.addLayout(button_layout) + + self.save_button = QPushButton("&Save") + self.save_button.setAutoDefault(True) + self.save_button.setDefault(True) + self.save_button.clicked.connect(self.save_changes) + update_button = QPushButton("&Update") + update_button.clicked.connect(self.save_changes) + cancel_button = QPushButton("&Cancel") + cancel_button.clicked.connect(self.cancel_changes) + button_layout.addWidget(cancel_button) + button_layout.addWidget(update_button) + button_layout.addWidget(self.save_button) + + def save_changes(self): + for macro_name, widget in self.edit_widgets.items(): + self.widget._set_macro(macro_name, widget.text()) + if self.sender() == self.save_button: + self.accept() + + def cancel_changes(self): + self.close() diff --git a/pyproject.toml b/pyproject.toml index 8c086c4..bb2cdd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,3 +89,9 @@ select = ["C", "E", "F", "W", "B", "I"] [tool.ruff.lint.pydocstyle] convention = "numpy" + +[tool.pyright.defineConstant] +PYQT5 = true +PYSIDE2 = false +PYQT6 = false +PYSIDE6 = false From 2d6b9d86e668e4ec904a9f768eff898f6622db88 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 16:02:31 -0800 Subject: [PATCH 010/104] ENH: add icons to the widgets I added recently --- pcdswidgets/motion/positioner_row_tc_interlock_widget.py | 3 +++ pcdswidgets/motion/positioner_row_widget.py | 3 +++ pcdswidgets/motion/positioner_widget.py | 3 +++ pcdswidgets/motion/smaract_open_loop_widget.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py index 8a7955b..a0406fb 100644 --- a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py +++ b/pcdswidgets/motion/positioner_row_tc_interlock_widget.py @@ -1,3 +1,5 @@ +from pydm.widgets.qtplugins import ifont + from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import PositionerRowTcInterlockWidgetBase @@ -5,4 +7,5 @@ class PositionerRowTcInterlockWidget(PositionerRowTcInterlockWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, + "icon": ifont.icon("arrows-alt-h"), } diff --git a/pcdswidgets/motion/positioner_row_widget.py b/pcdswidgets/motion/positioner_row_widget.py index 917a526..3fa4f68 100644 --- a/pcdswidgets/motion/positioner_row_widget.py +++ b/pcdswidgets/motion/positioner_row_widget.py @@ -1,3 +1,5 @@ +from pydm.widgets.qtplugins import ifont + from pcdswidgets.builder.ui.positioner_row_base import PositionerRowWidgetBase @@ -5,4 +7,5 @@ class PositionerRowWidget(PositionerRowWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, + "icon": ifont.icon("arrows-alt-h"), } diff --git a/pcdswidgets/motion/positioner_widget.py b/pcdswidgets/motion/positioner_widget.py index a6b8514..5b2578b 100644 --- a/pcdswidgets/motion/positioner_widget.py +++ b/pcdswidgets/motion/positioner_widget.py @@ -1,3 +1,5 @@ +from pydm.widgets.qtplugins import ifont + from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase @@ -5,4 +7,5 @@ class PositionerWidget(PositionerWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, + "icon": ifont.icon("expand-arrows-alt"), } diff --git a/pcdswidgets/motion/smaract_open_loop_widget.py b/pcdswidgets/motion/smaract_open_loop_widget.py index 6217942..d109666 100644 --- a/pcdswidgets/motion/smaract_open_loop_widget.py +++ b/pcdswidgets/motion/smaract_open_loop_widget.py @@ -1,3 +1,5 @@ +from pydm.widgets.qtplugins import ifont + from pcdswidgets.builder.ui.smaract_open_loop_base import SmaractOpenLoopWidgetBase @@ -5,4 +7,5 @@ class SmaractOpenLoopWidget(SmaractOpenLoopWidgetBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, + "icon": ifont.icon("arrows-alt-h"), } From 33fc0c1425f2497998ca538e7c6c71ccfdd8e495 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Feb 2026 16:19:47 -0800 Subject: [PATCH 011/104] DOC: rewrite the readme --- README.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82960de..dd0a99d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,84 @@ - [![Build Status](https://travis-ci.org/pcdshub/pcdswidgets.svg?branch=master)](https://travis-ci.org/pcdshub/pcdswidgets) # pcdswidgets LCLS PyDM Widget Library + +## Installation +### Prod +Pick your favorite: + +- pip install pcdswidgets +- conda install pcdswidgets + +### Dev +pip install -e . + + +## Adding Widgets +### Adding a Symbol-based Widget +This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. + +This will require at least some familiarity with Python and with the structure of this module. + +Largely: refer back to the existing widgets. + +The steps are: + +1. Create a new subclass of BaseSymbolIcon in the icons subfolder + - Define a path + - Implement draw_icon +2. Create a new subclass of PCDSSymbolBase + - Include your icon as self.icon + - Add relevant properties as needed, or inherit them from the existing mixins + - include the _qt_designer_ class attribute +3. make, to update pyproject.toml with new widget locations + +If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets. + + +### Adding a Composite Widget +This is how you would convert a .ui file with macro substitution that is normally used with PyDMEmbeddedDisplay into a designer widget served from here. + +This is not required, but you would do this to make your widget globally available and easier to add to screens. + +This requires only basic Python knowledge. + +The steps are: + +1. Create a widget as a PyDM screen + - Use qt designer to define the layout (saves a .ui file) + - Use PyDM macros to define user inputs +2. Try it! + - Use PyDMEmbeddedDisplay to include your widget in other screens + - Iterate, update the widget until you like it. +3. Bring it here + - Copy your .ui file in the pcdswidgets/builder/ui folder. +4. make + - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. +5. Create a widget class + - Look around for examples, e.g. pcdswidgets/motion/positioner_widget.py + - Keeping these in separate files can avoid circular import errors and lets us include widgets inside widgets + - Import from the _base module created from your .ui file and subclass +6. make, again + - This will include your widget in pyproject.toml + +If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets. + + +#### Widget Classes +The widget class looks something like: +``` +from pcdswidgets.builder.ui.my_widget_base import MyWidgetBase + + +class MyWidget(MyWidgetBase): + _qt_designer_ = { + "group": "My Category", + "is_container": False, + } +``` + +If you like, you can extend these classes to add additional python code to use at runtime. + + +#### Limitations +- Widgets that contain PyDMEmbeddedWidget are not supported: bootstrap these by turning the contents into widgets themselves. +- The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the widget class in python, giving your widgets more unique names will help give more useful type hints, automatically. From b9b205c91e6de89ff76fc17149c2cb4dc79d3a32 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:08:34 -0700 Subject: [PATCH 012/104] ENH: plausible starting point for ensuring pyuic compilation at package build time --- Makefile | 7 +++++-- pcdswidgets/setup.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 pcdswidgets/setup.py diff --git a/Makefile b/Makefile index 820e2be..9cd7bdb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean +.PHONY: all build clean UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) @@ -7,7 +7,10 @@ JINJA_SOURCE := $(wildcard pcdswidgets/builder/*.j2) PY_FORM := $(UI_SOURCE:.ui=_form.py) PY_BASE := $(UI_SOURCE:.ui=_base.py) -all: $(PY_FORM) $(PY_BASE) pyproject.toml +all: build pyproject.toml + +# make build is for pip, etc. so pyproject.toml doesn't change at build time +build: $(PY_FORM) $(PY_BASE) clean: rm $(PY_FORM) diff --git a/pcdswidgets/setup.py b/pcdswidgets/setup.py new file mode 100644 index 0000000..f6367a2 --- /dev/null +++ b/pcdswidgets/setup.py @@ -0,0 +1,17 @@ +import subprocess + +from setuptools import setup +from setuptools.command.build_py import build_py + + +class MakeBuildFirst(build_py): + """ + Use the instructions in the Makefile to generate needed .py files from the .ui source + """ + + def run(self, *args, **kwargs): + subprocess.check_call(("make", "build")) + return super().run(*args, **kwargs) + + +setup(cmdclass={"build_py": MakeBuildFirst}) From 41cd57befbb6eb73e6d3185718398851cf429801 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:26:30 -0700 Subject: [PATCH 013/104] TST: add some basic end to end tests for the builder --- pcdswidgets/tests/test_builder.py | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 pcdswidgets/tests/test_builder.py diff --git a/pcdswidgets/tests/test_builder.py b/pcdswidgets/tests/test_builder.py new file mode 100644 index 0000000..526c62b --- /dev/null +++ b/pcdswidgets/tests/test_builder.py @@ -0,0 +1,50 @@ +import importlib +import inspect +from pathlib import Path + +import pytest + +from pcdswidgets.builder.designer_widget import DesignerWidget + +UI_SOURCES = sorted((Path(__file__).parent.parent / "builder" / "ui").glob("*.ui")) + + +@pytest.mark.parametrize("ui_source", UI_SOURCES) +def test_it_was_built(ui_source: Path): + """ + Check if the current clone has had .py files built from all the source .ui files. + """ + base_path = ui_source.parent / (ui_source.stem + "_base.py") + form_path = ui_source.parent / (ui_source.stem + "_form.py") + + assert base_path.exists() + assert form_path.exists() + + +@pytest.mark.parametrize("ui_source", UI_SOURCES) +def test_built_is_importable(ui_source: Path): + """ + Check if the .py files in the current clone have somewhat proper importable classes. + """ + base_module_name = "pcdswidgets.builder.ui." + ui_source.stem + "_base" + form_module_name = "pcdswidgets.builder.ui." + ui_source.stem + "_form" + + base_module = importlib.import_module(base_module_name) + form_module = importlib.import_module(form_module_name) + + base_classes = [] + for _, cls in inspect.getmembers(base_module, inspect.isclass): + if inspect.getmodule(cls) is base_module: + base_classes.append(cls) + + form_classes = [] + for _, cls in inspect.getmembers(form_module, inspect.isclass): + if inspect.getmodule(cls) is form_module: + form_classes.append(cls) + + assert len(base_classes) == 1 + assert isinstance(base_classes[0], DesignerWidget) + + assert len(form_classes) == 1 + assert hasattr(form_classes[0], "setupUi") + assert hasattr(form_classes[0], "retranslateUi") From 0ca4b74913bb0bdf3878c83fd7498ecd584c4ab8 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:28:14 -0700 Subject: [PATCH 014/104] TST: compare classes with issubclass, not isinstance --- pcdswidgets/tests/test_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/tests/test_builder.py b/pcdswidgets/tests/test_builder.py index 526c62b..299d311 100644 --- a/pcdswidgets/tests/test_builder.py +++ b/pcdswidgets/tests/test_builder.py @@ -43,7 +43,7 @@ def test_built_is_importable(ui_source: Path): form_classes.append(cls) assert len(base_classes) == 1 - assert isinstance(base_classes[0], DesignerWidget) + assert issubclass(base_classes[0], DesignerWidget) assert len(form_classes) == 1 assert hasattr(form_classes[0], "setupUi") From 40a72089db177b044618c549f16651ec1ed7608e Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:33:02 -0700 Subject: [PATCH 015/104] FIX: add missing __init__.py to make builder a real module --- pcdswidgets/builder/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pcdswidgets/builder/__init__.py diff --git a/pcdswidgets/builder/__init__.py b/pcdswidgets/builder/__init__.py new file mode 100644 index 0000000..e69de29 From d8cb67caa083d6ae3a664f786e8878e727f4854a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:36:10 -0700 Subject: [PATCH 016/104] BLD: rename docs to doc to match CI expectation --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bb2cdd4..d75a3c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SmaractOpenLoopWidget = "pcdswidgets.motion.smaract_open_loop_widget:SmaractOpen TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] -docs = [ +doc = [ "docs-versions-menu>=0.5.2", "sphinx>=9.1.0", "sphinx-rtd-theme>=3.1.0", From 8ef82242251e7b7bd25712ecbb2d940418634804 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:45:30 -0700 Subject: [PATCH 017/104] BLD: try to include the makefile too? Is that why it doesn't make? --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index cf1ec58..5f820fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include **/*.ui +include Makefile include AUTHORS.rst include CONTRIBUTING.rst include LICENSE.md From b0e09d323cba4d13645528311840dc4b43c20707 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:48:11 -0700 Subject: [PATCH 018/104] BLD: the Makefile didn't matter --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5f820fb..cf1ec58 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include **/*.ui -include Makefile include AUTHORS.rst include CONTRIBUTING.rst include LICENSE.md From b25a942cc1625a6395e8a84ae6713678beef182a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 14:57:49 -0700 Subject: [PATCH 019/104] BLD: setup.py was in the wrong directory... --- pcdswidgets/setup.py => setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pcdswidgets/setup.py => setup.py (100%) diff --git a/pcdswidgets/setup.py b/setup.py similarity index 100% rename from pcdswidgets/setup.py rename to setup.py From 556b02cd42b9546355ad60b4b6d230f65e35ca20 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 15:31:38 -0700 Subject: [PATCH 020/104] BLD: specify build dependencies required to make --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d75a3c3..85e7eee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2",] +requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2", "jinja2>=3", "pyqt5>=5.15.11",] [project] classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Python :: 3",] From 128838e59198b3bcd1d54d47e6decb4a427d2ad2 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 15:40:32 -0700 Subject: [PATCH 021/104] BLD: add build requirements to the conda recipe too --- conda-recipe/meta.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 64fc919..39d5cbf 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -19,6 +19,8 @@ requirements: - python >=3.9 - setuptools_scm - pip + - jinja2 >=3 + - pyqt >=5 run: - python >=3.9 - pydm >=1.9.0 From 7fe378f25b3aed4e7ad3aa85b6ac9472d12730a6 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 10 Mar 2026 15:42:53 -0700 Subject: [PATCH 022/104] MNT: uv.lock updated for docs -> doc --- uv.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index c079b2b..41f0e2c 100644 --- a/uv.lock +++ b/uv.lock @@ -327,7 +327,7 @@ dependencies = [ dev = [ { name = "tomlkit" }, ] -docs = [ +doc = [ { name = "docs-versions-menu" }, { name = "sphinx" }, { name = "sphinx-rtd-theme" }, @@ -341,19 +341,19 @@ test = [ [package.metadata] requires-dist = [ - { name = "docs-versions-menu", marker = "extra == 'docs'", specifier = ">=0.5.2" }, + { name = "docs-versions-menu", marker = "extra == 'doc'", specifier = ">=0.5.2" }, { name = "pydm", specifier = ">=1.9.0" }, { name = "pyqt5", specifier = ">=5.15.11" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2" }, { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.5.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, { name = "qtpy", specifier = ">=2.4.3" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = ">=9.1.0" }, - { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=3.1.0" }, - { name = "sphinxcontrib-jquery", marker = "extra == 'docs'", specifier = ">=4.1" }, + { name = "sphinx", marker = "extra == 'doc'", specifier = ">=9.1.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=3.1.0" }, + { name = "sphinxcontrib-jquery", marker = "extra == 'doc'", specifier = ">=4.1" }, { name = "tomlkit", marker = "extra == 'dev'", specifier = ">=0.14.0" }, ] -provides-extras = ["docs", "test", "dev"] +provides-extras = ["doc", "test", "dev"] [[package]] name = "pluggy" From 30d7139afa9bfec5c5159a7efb11a003b25f3d9c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 10:29:37 -0700 Subject: [PATCH 023/104] CI: try different way of running conda tests --- .github/workflows/standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index 588518d..e17c168 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -9,7 +9,7 @@ on: jobs: standard: - uses: pcdshub/pcds-ci-helpers/.github/workflows/python-standard.yml@master + uses: pcdshub/pcds-ci-helpers/.github/workflows/python-standard.yml@tst_from_installed secrets: inherit with: # The workflow needs to know the package name. This can be determined From d57d57fb46b7d3f0816b744892de5bc279f44ffe Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:29:21 -0700 Subject: [PATCH 024/104] TST: add tests that we included all the widgets in the entrypoint properly- doesn't quite work yet --- pcdswidgets/entrypoint_widgets.py | 52 ++++++++++++++------ pcdswidgets/tests/test_entrypoint_widgets.py | 31 ++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 pcdswidgets/tests/test_entrypoint_widgets.py diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index 7ad3327..b3a06e7 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -26,6 +26,13 @@ def main(): + key_val = get_widget_entrypoint_data() + widget_table, toml_doc = get_current_widget_table() + update_widget_table(widget_table, key_val) + write_pyproj(toml_doc) + + +def get_widget_entrypoint_data() -> list[tuple[str, str]]: key_val: list[tuple[str, str]] = [] for module in iter_submodules(): for name, obj in inspect.getmembers(module, inspect.isclass): @@ -34,8 +41,29 @@ def main(): if hasattr(obj, "_qt_designer_"): key_val.append((name, f"{obj.__module__}:{name}")) key_val.sort() + return key_val + + +def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: + """Recursively yield all submodules of a package.""" + if any(mod in package for mod in SKIP_MODULES): + return + module = importlib.import_module(package) + yield module + try: + for _, modname, _ in pkgutil.walk_packages(module.__path__, module.__name__ + "."): + if "__main__" not in modname: + yield from iter_submodules(modname) + except AttributeError: + ... + + +def get_pyproj_path() -> Path: + return Path(__file__).parent.parent / "pyproject.toml" + - pyproj = Path(__file__).parent.parent / "pyproject.toml" +def get_current_widget_table() -> tuple[tki.Table, tk.TOMLDocument]: + pyproj = get_pyproj_path() if not pyproj.exists(): raise RuntimeError(f"Project file {pyproj} missing?") with open(pyproj, "r") as fd: @@ -44,27 +72,21 @@ def main(): project_table = cast(tki.Table, toml_doc["project"]) entrypoint_table = cast(tki.Table, project_table["entry-points"]) widget_table = cast(tki.Table, entrypoint_table["pydm.widget"]) + + return widget_table, toml_doc + + +def update_widget_table(widget_table: tki.Table, key_val: list[tuple[str, str]]): widget_table.clear() for key, value in key_val: widget_table[key] = value + +def write_pyproj(toml_doc: tk.TOMLDocument): + pyproj = get_pyproj_path() with open(pyproj, "w") as fd: tk.dump(toml_doc, fd) -def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: - """Recursively yield all submodules of a package.""" - if any(mod in package for mod in SKIP_MODULES): - return - module = importlib.import_module(package) - yield module - try: - for _, modname, _ in pkgutil.walk_packages(module.__path__, module.__name__ + "."): - if "__main__" not in modname: - yield from iter_submodules(modname) - except AttributeError: - ... - - if __name__ == "__main__": main() diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py new file mode 100644 index 0000000..948c876 --- /dev/null +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -0,0 +1,31 @@ +from importlib.metadata import entry_points + +from pydm.config import ENTRYPOINT_WIDGET + +from pcdswidgets.entrypoint_widgets import get_current_widget_table, get_widget_entrypoint_data + + +def test_toml_has_all_widgets(): + """ + Ensure that all widgets are included in pyproject.toml, and in the generated order. + + If this fails, it's likely that we forgot to run the entrypoint_widgets generator program. + """ + name_and_entrypoint = get_widget_entrypoint_data() + current_table, _ = get_current_widget_table() + for (name, entrypoint), (key, value) in zip(name_and_entrypoint, current_table.items(), strict=True): + assert name == key + assert entrypoint == value + + +def test_entrypoint_has_all_widgets(): + """ + Ensure that all widgets are included in designer via the pydm designer plugin. + + If this fails, but test_toml_has_all_widgets is passing, it's likely because + pcdswidgets is not installed in dev mode. + """ + pydm_widgets = entry_points(group=ENTRYPOINT_WIDGET) + name_and_entrypoint = get_widget_entrypoint_data() + for name, entrypoint in name_and_entrypoint: + assert pydm_widgets.select(name=name)[name].value == entrypoint From f2bd11c48a8c479b9a96d3af89ff0cce98aec926 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:29:46 -0700 Subject: [PATCH 025/104] BLD: add tomlkit as a test dependency for the new test --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 85e7eee..bed0ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ test = [ "pytest>=9.0.2", "pytest-qt>=4.5.0", "pytest-timeout>=2.4.0", + "tomlkit>=0.14.0", ] dev = [ "tomlkit>=0.14.0", diff --git a/uv.lock b/uv.lock index 41f0e2c..82daa84 100644 --- a/uv.lock +++ b/uv.lock @@ -337,6 +337,7 @@ test = [ { name = "pytest" }, { name = "pytest-qt" }, { name = "pytest-timeout" }, + { name = "tomlkit" }, ] [package.metadata] @@ -352,6 +353,7 @@ requires-dist = [ { name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=3.1.0" }, { name = "sphinxcontrib-jquery", marker = "extra == 'doc'", specifier = ">=4.1" }, { name = "tomlkit", marker = "extra == 'dev'", specifier = ">=0.14.0" }, + { name = "tomlkit", marker = "extra == 'test'", specifier = ">=0.14.0" }, ] provides-extras = ["doc", "test", "dev"] From 3b0c16cde006b2eedb26abb9cbdde639ac281f5c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:36:39 -0700 Subject: [PATCH 026/104] FIX: not every widget was included --- pcdswidgets/entrypoint_widgets.py | 6 +++--- pyproject.toml | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index b3a06e7..3a167a4 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -33,14 +33,14 @@ def main(): def get_widget_entrypoint_data() -> list[tuple[str, str]]: - key_val: list[tuple[str, str]] = [] + key_val_set: set[tuple[str, str]] = set() for module in iter_submodules(): for name, obj in inspect.getmembers(module, inspect.isclass): if name in SKIP_WIDGETS: continue if hasattr(obj, "_qt_designer_"): - key_val.append((name, f"{obj.__module__}:{name}")) - key_val.sort() + key_val_set.add((name, f"{obj.__module__}:{name}")) + key_val = sorted(key_val_set) return key_val diff --git a/pyproject.toml b/pyproject.toml index bed0ef3..942f2da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ write_to = "pcdswidgets/_version.py" [project.entry-points."pydm.widget"] ApertureValve = "pcdswidgets.vacuum.valves:ApertureValve" +CapacitanceManometerGauge = "pcdswidgets.vacuum.gauges:CapacitanceManometerGauge" +ColdCathodeComboGauge = "pcdswidgets.vacuum.gauges:ColdCathodeComboGauge" ColdCathodeGauge = "pcdswidgets.vacuum.gauges:ColdCathodeGauge" ControlOnlyValveNC = "pcdswidgets.vacuum.valves:ControlOnlyValveNC" ControlOnlyValveNO = "pcdswidgets.vacuum.valves:ControlOnlyValveNO" @@ -38,6 +40,7 @@ EPSByteIndicator = "pcdswidgets.eps_byteindicator:EPSByteIndicator" FastShutter = "pcdswidgets.vacuum.valves:FastShutter" FilterSortWidgetTable = "pcdswidgets.table:FilterSortWidgetTable" GetterPump = "pcdswidgets.vacuum.pumps:GetterPump" +HotCathodeComboGauge = "pcdswidgets.vacuum.gauges:HotCathodeComboGauge" HotCathodeGauge = "pcdswidgets.vacuum.gauges:HotCathodeGauge" IonPump = "pcdswidgets.vacuum.pumps:IonPump" NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" From cddac2f940062d326d96b38c9d2c53932398bcf0 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:43:06 -0700 Subject: [PATCH 027/104] STY: automatic fixes from pre-commit run --all-files --- .python-version | 2 +- examples/basic_table.py | 14 +-- examples/show_icon.py | 6 +- pcdswidgets/eps_byteindicator.py | 19 ++--- pcdswidgets/icons/__init__.py | 70 +++++++++------ pcdswidgets/icons/base.py | 6 +- pcdswidgets/icons/demo/__main__.py | 3 +- pcdswidgets/icons/gauges.py | 10 ++- pcdswidgets/icons/others.py | 1 + pcdswidgets/icons/pumps.py | 32 +++---- pcdswidgets/icons/valves.py | 100 +++++++++++----------- pcdswidgets/table.py | 53 +++++------- pcdswidgets/tests/test_icons.py | 23 ++--- pcdswidgets/tests/vacuum/test_base.py | 46 ++++++---- pcdswidgets/tests/vacuum/test_mixins.py | 49 ++++++----- pcdswidgets/tests/vacuum/test_symbols.py | 5 +- pcdswidgets/vacuum/__init__.py | 58 ++++++++++--- pcdswidgets/vacuum/base.py | 103 +++++++++++++---------- pcdswidgets/vacuum/demo/__main__.py | 3 +- pcdswidgets/vacuum/gauges.py | 46 +++++----- pcdswidgets/vacuum/mixins.py | 44 +++++----- pcdswidgets/vacuum/pumps.py | 18 ++-- pcdswidgets/vacuum/valves.py | 92 ++++++++++---------- pcdswidgets/version.py | 5 +- 24 files changed, 453 insertions(+), 355 deletions(-) diff --git a/.python-version b/.python-version index fdcfcfd..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 \ No newline at end of file +3.12 diff --git a/examples/basic_table.py b/examples/basic_table.py index b1416ec..ffc0c75 100644 --- a/examples/basic_table.py +++ b/examples/basic_table.py @@ -7,29 +7,29 @@ class BasicTable(Display): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ui.example_table.add_filter( - 'Hide Negative Values', + "Hide Negative Values", self.neg_filter, active=True, ) self.ui.example_table.add_filter( - 'Hide Even Values', + "Hide Even Values", self.even_filter, active=False, ) self.ui.example_table.add_filter( - 'Hide Rows with 4 Character Names', + "Hide Rows with 4 Character Names", self.four_filter, active=False, ) def neg_filter(self, value_dict): - return value_dict['readback'] >= 0 + return value_dict["readback"] >= 0 def even_filter(self, value_dict): - return value_dict['readback'] % 2 + return value_dict["readback"] % 2 def four_filter(self, value_dict): - return len(value_dict['row_name']) != 4 + return len(value_dict["row_name"]) != 4 def ui_filename(self): - return os.path.join(os.path.dirname(__file__), 'basic_table.ui') + return os.path.join(os.path.dirname(__file__), "basic_table.ui") diff --git a/examples/show_icon.py b/examples/show_icon.py index bf51561..697bcd4 100644 --- a/examples/show_icon.py +++ b/examples/show_icon.py @@ -12,7 +12,7 @@ def screenshot(widget, filename): s = str(filename) + ".png" print("Filename: ", s) p = QWidget.grab(widget) - p.save(s, 'png') + p.save(s, "png") app = QApplication([]) @@ -22,8 +22,6 @@ def screenshot(widget, filename): tp = ic() tp.setFixedSize(64, 64) tp.show() - path = os.path.join(os.path.dirname(__file__), '..', 'docs', 'source', - '_static', "icons", - tp.__class__.__name__) + path = os.path.join(os.path.dirname(__file__), "..", "docs", "source", "_static", "icons", tp.__class__.__name__) screenshot(tp, path) diff --git a/pcdswidgets/eps_byteindicator.py b/pcdswidgets/eps_byteindicator.py index ac18f89..82a98c9 100644 --- a/pcdswidgets/eps_byteindicator.py +++ b/pcdswidgets/eps_byteindicator.py @@ -19,9 +19,8 @@ def update_indicators(self): else: value = int(self.value) >> self._shift - bits = [(value >> i) & 1 - for i in range(self._num_bits)] - for bit, indicator in zip(bits, self._indicators): + bits = [(value >> i) & 1 for i in range(self._num_bits)] + for bit, indicator in zip(bits, self._indicators, strict=False): if self._connected: if self._alarm_state == 3: c = self._invalid_color @@ -70,10 +69,9 @@ def label_change(self, new_labels): Callback function when the lables change """ - labels = parse_value_for_display(value=new_labels, precision=0, - display_format_type=DisplayFormat.String) + labels = parse_value_for_display(value=new_labels, precision=0, display_format_type=DisplayFormat.String) - labels = labels.split(';') + labels = labels.split(";") self.template_widget.numBits = len(labels) self.template_widget.labels = labels self.template_widget.update_indicators() @@ -97,12 +95,11 @@ def channel(self, ch): self._value_pv = ch + ":nFlags_RBV" self._label_pv = ch + ":sFlagDesc_RBV" - _value_channel = PyDMChannel(address=self._value_pv, - connection_slot=self.value_channel, - value_slot=self.value_change) + _value_channel = PyDMChannel( + address=self._value_pv, connection_slot=self.value_channel, value_slot=self.value_change + ) - _label_channel = PyDMChannel(address=self._label_pv, - value_slot=self.label_change) + _label_channel = PyDMChannel(address=self._label_pv, value_slot=self.label_change) _value_channel.connect() _label_channel.connect() diff --git a/pcdswidgets/icons/__init__.py b/pcdswidgets/icons/__init__.py index 15c8415..895e11b 100644 --- a/pcdswidgets/icons/__init__.py +++ b/pcdswidgets/icons/__init__.py @@ -1,26 +1,48 @@ -from .gauges import (CapManometerGaugeSymbolIcon, CathodeGaugeSymbolIcon, - ColdCathodeComboGaugeSymbolIcon, - ColdCathodeGaugeSymbolIcon, - HotCathodeComboGaugeSymbolIcon, HotCathodeGaugeSymbolIcon, - RoughGaugeSymbolIcon) +from .gauges import ( + CapManometerGaugeSymbolIcon, + CathodeGaugeSymbolIcon, + ColdCathodeComboGaugeSymbolIcon, + ColdCathodeGaugeSymbolIcon, + HotCathodeComboGaugeSymbolIcon, + HotCathodeGaugeSymbolIcon, + RoughGaugeSymbolIcon, +) from .others import RGASymbolIcon -from .pumps import (GetterPumpSymbolIcon, IonPumpSymbolIcon, - ScrollPumpSymbolIcon, TurboPumpSymbolIcon) -from .valves import (ApertureValveSymbolIcon, ControlOnlyValveSymbolIcon, - ControlValveSymbolIcon, FastShutterSymbolIcon, - NeedleValveSymbolIcon, PneumaticValveDASymbolIcon, - PneumaticValveNOSymbolIcon, PneumaticValveSymbolIcon, - ProportionalValveSymbolIcon, - RightAngleManualValveSymbolIcon) +from .pumps import GetterPumpSymbolIcon, IonPumpSymbolIcon, ScrollPumpSymbolIcon, TurboPumpSymbolIcon +from .valves import ( + ApertureValveSymbolIcon, + ControlOnlyValveSymbolIcon, + ControlValveSymbolIcon, + FastShutterSymbolIcon, + NeedleValveSymbolIcon, + PneumaticValveDASymbolIcon, + PneumaticValveNOSymbolIcon, + PneumaticValveSymbolIcon, + ProportionalValveSymbolIcon, + RightAngleManualValveSymbolIcon, +) -__all__ = ['RoughGaugeSymbolIcon', 'CathodeGaugeSymbolIcon', - 'HotCathodeGaugeSymbolIcon', 'ColdCathodeGaugeSymbolIcon', - 'IonPumpSymbolIcon', 'TurboPumpSymbolIcon', 'ScrollPumpSymbolIcon', - 'GetterPumpSymbolIcon', - 'PneumaticValveSymbolIcon', 'FastShutterSymbolIcon', - 'ApertureValveSymbolIcon', 'RightAngleManualValveSymbolIcon', - 'NeedleValveSymbolIcon', 'ProportionalValveSymbolIcon', - 'RGASymbolIcon', 'ControlValveSymbolIcon', - 'ControlOnlyValveSymbolIcon', 'PneumaticValveNOSymbolIcon', - 'PneumaticValveDASymbolIcon', 'CapManometerGaugeSymbolIcon', - 'HotCathodeComboGaugeSymbolIcon', 'ColdCathodeComboGaugeSymbolIcon'] +__all__ = [ + "RoughGaugeSymbolIcon", + "CathodeGaugeSymbolIcon", + "HotCathodeGaugeSymbolIcon", + "ColdCathodeGaugeSymbolIcon", + "IonPumpSymbolIcon", + "TurboPumpSymbolIcon", + "ScrollPumpSymbolIcon", + "GetterPumpSymbolIcon", + "PneumaticValveSymbolIcon", + "FastShutterSymbolIcon", + "ApertureValveSymbolIcon", + "RightAngleManualValveSymbolIcon", + "NeedleValveSymbolIcon", + "ProportionalValveSymbolIcon", + "RGASymbolIcon", + "ControlValveSymbolIcon", + "ControlOnlyValveSymbolIcon", + "PneumaticValveNOSymbolIcon", + "PneumaticValveDASymbolIcon", + "CapManometerGaugeSymbolIcon", + "HotCathodeComboGaugeSymbolIcon", + "ColdCathodeComboGaugeSymbolIcon", +] diff --git a/pcdswidgets/icons/base.py b/pcdswidgets/icons/base.py index 0e5a84d..93d77f2 100644 --- a/pcdswidgets/icons/base.py +++ b/pcdswidgets/icons/base.py @@ -1,8 +1,7 @@ from pydm.utilities import is_qt_designer, remove_protocol from qtpy.QtCore import Property, QEvent, QSize, Qt, Signal from qtpy.QtGui import QBrush, QColor, QPainter, QPen -from qtpy.QtWidgets import (QApplication, QStyle, QStyleOption, QToolTip, - QWidget) +from qtpy.QtWidgets import QApplication, QStyle, QStyleOption, QToolTip, QWidget from ..utils import find_ancestor_for_widget @@ -18,6 +17,7 @@ class BaseSymbolIcon(QWidget): parent : QWidget The parent widget for this widget. """ + clicked = Signal() def __init__(self, parent=None): @@ -66,7 +66,7 @@ def show_state_channel(self, event): if not p: return - state_suffix = getattr(p, '_state_suffix', None) + state_suffix = getattr(p, "_state_suffix", None) if not state_suffix: return diff --git a/pcdswidgets/icons/demo/__main__.py b/pcdswidgets/icons/demo/__main__.py index b0da870..db3b963 100644 --- a/pcdswidgets/icons/demo/__main__.py +++ b/pcdswidgets/icons/demo/__main__.py @@ -3,13 +3,14 @@ Invoke as e.g. "python -m pcdswidgets.icons.demo ControlValve" """ + import sys from qtpy.QtWidgets import QApplication from .. import * # noqa -cls = sys.argv[1] + 'SymbolIcon' +cls = sys.argv[1] + "SymbolIcon" app = QApplication([]) icon = globals()[cls]() icon.show() diff --git a/pcdswidgets/icons/gauges.py b/pcdswidgets/icons/gauges.py index 96aa962..413d766 100644 --- a/pcdswidgets/icons/gauges.py +++ b/pcdswidgets/icons/gauges.py @@ -46,6 +46,7 @@ class HotCathodeGaugeSymbolIcon(CathodeGaugeSymbolIcon): parent : QWidget The parent widget for the icon """ + def draw_icon(self, painter): super().draw_icon(painter) painter.drawLine(QPointF(0.3, 0.1), QPointF(0.3, 0.9)) @@ -62,9 +63,10 @@ class ColdCathodeGaugeSymbolIcon(CathodeGaugeSymbolIcon): parent : QWidget The parent widget for the icon """ + def draw_icon(self, painter): super().draw_icon(painter) - painter.drawArc(QRectF(0.25, 0.25, 0.5, 0.5), 45*16, 270*16) + painter.drawArc(QRectF(0.25, 0.25, 0.5, 0.5), 45 * 16, 270 * 16) class ColdCathodeComboGaugeSymbolIcon(CathodeGaugeSymbolIcon): @@ -76,9 +78,10 @@ class ColdCathodeComboGaugeSymbolIcon(CathodeGaugeSymbolIcon): parent : QWidget The parent widget for the icon """ + path = QPainterPath(QPointF(0.5, 0)) - path.lineTo(.933, .75) - path.lineTo(.067, .75) + path.lineTo(0.933, 0.75) + path.lineTo(0.067, 0.75) path.closeSubpath() def draw_icon(self, painter): @@ -95,6 +98,7 @@ class HotCathodeComboGaugeSymbolIcon(ColdCathodeComboGaugeSymbolIcon): parent : QWidget The parent widget for the icon """ + def draw_icon(self, painter): super().draw_icon(painter) painter.drawLine(QPointF(0.4, 0.30), QPointF(0.4, 0.65)) diff --git a/pcdswidgets/icons/others.py b/pcdswidgets/icons/others.py index d1ba542..ed16c58 100644 --- a/pcdswidgets/icons/others.py +++ b/pcdswidgets/icons/others.py @@ -13,6 +13,7 @@ class RGASymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + path = QPainterPath(QPointF(0, 0)) path.lineTo(1, 0) path.lineTo(1, 0.33) diff --git a/pcdswidgets/icons/pumps.py b/pcdswidgets/icons/pumps.py index 4cba42d..1baa99a 100644 --- a/pcdswidgets/icons/pumps.py +++ b/pcdswidgets/icons/pumps.py @@ -15,6 +15,7 @@ class ScrollPumpSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._center_brush = QBrush(QColor("transparent")) @@ -47,8 +48,7 @@ def draw_icon(self, painter): painter.drawArc(QRectF(0.3, 0.3, 0.4, 0.4), 90 * 16, -270 * 16) - arrow = QPolygonF( - [QPointF(-0.025, 0.0), QPointF(0.025, 0.0), QPointF(0.0, -0.025)]) + arrow = QPolygonF([QPointF(-0.025, 0.0), QPointF(0.025, 0.0), QPointF(0.0, -0.025)]) painter.setBrush(QBrush(QColor(0, 0, 0))) painter.drawPolygon(arrow.translated(circle_arrow_point)) @@ -72,22 +72,21 @@ def draw_icon(self, painter): curve_start = QPointF(0.5, 0.7) bend_angle = 25 curve_end_l = QPointF( - 0.4 * math.cos(math.radians(90 + bend_angle)) + 0.5, - -0.4 * math.sin(math.radians(90 + bend_angle)) + 0.5) + 0.4 * math.cos(math.radians(90 + bend_angle)) + 0.5, -0.4 * math.sin(math.radians(90 + bend_angle)) + 0.5 + ) c1 = QPointF(0.5, 0.4) path = QPainterPath(curve_start) path.quadTo(c1, curve_end_l) painter.drawPath(path) curve_end_r = QPointF( - 0.4 * math.cos(math.radians(90 - bend_angle)) + 0.5, - -0.4 * math.sin(math.radians(90 - bend_angle)) + 0.5) + 0.4 * math.cos(math.radians(90 - bend_angle)) + 0.5, -0.4 * math.sin(math.radians(90 - bend_angle)) + 0.5 + ) path = QPainterPath(curve_start) path.quadTo(c1, curve_end_r) painter.drawPath(path) # Draw the arrow end-caps painter.setBrush(QBrush(QColor(0, 0, 0))) - arrow = QPolygonF( - [QPointF(-0.025, 0.0), QPointF(0.025, 0.0), QPointF(0.0, 0.025)]) + arrow = QPolygonF([QPointF(-0.025, 0.0), QPointF(0.025, 0.0), QPointF(0.0, 0.025)]) painter.drawPolygon(arrow.translated(bottom_arrow_point)) t = QTransform() t.rotate(180.0 - 25.0) @@ -110,6 +109,7 @@ class TurboPumpSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._center_brush = QBrush(QColor("transparent")) @@ -165,13 +165,15 @@ def draw_icon(self, painter): top_arrow_point = QPointF(0.35, 0.15) arrow = QPolygonF( - [QPointF(-0.08, 0.0), - QPointF(-0.005, 0.0), - QPointF(-0.005, 0.15), - QPointF(0.005, 0.15), - QPointF(0.005, 0.0), - QPointF(0.08, 0.0), - QPointF(0.00, -0.08)] + [ + QPointF(-0.08, 0.0), + QPointF(-0.005, 0.0), + QPointF(-0.005, 0.15), + QPointF(0.005, 0.15), + QPointF(0.005, 0.0), + QPointF(0.08, 0.0), + QPointF(0.00, -0.08), + ] ) t = QTransform() diff --git a/pcdswidgets/icons/valves.py b/pcdswidgets/icons/valves.py index b8dce04..eb1da41 100644 --- a/pcdswidgets/icons/valves.py +++ b/pcdswidgets/icons/valves.py @@ -1,8 +1,7 @@ import math from qtpy.QtCore import Property, QLineF, QPointF, QRectF, Qt -from qtpy.QtGui import (QBrush, QColor, QPainterPath, QPen, QPolygonF, - QTransform) +from qtpy.QtGui import QBrush, QColor, QPainterPath, QPen, QPolygonF, QTransform from .base import BaseSymbolIcon @@ -16,6 +15,7 @@ class PneumaticValveSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -51,6 +51,7 @@ class FastShutterSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._arrow_brush = QBrush(QColor("transparent"), Qt.SolidPattern) @@ -79,12 +80,7 @@ def draw_icon(self, painter): painter.setPen(Qt.NoPen) painter.setBrush(self._arrow_brush) arrow = QPolygonF( - [QPointF(0.2, 0), - QPointF(0.2, 0.20), - QPointF(0.5, 0.40), - QPointF(0.8, 0.20), - QPointF(0.8, 0) - ] + [QPointF(0.2, 0), QPointF(0.2, 0.20), QPointF(0.5, 0.40), QPointF(0.8, 0.20), QPointF(0.8, 0)] ) painter.drawPolygon(arrow) @@ -107,6 +103,7 @@ class RightAngleManualValveSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def draw_icon(self, painter): path = QPainterPath(QPointF(0, 0)) path.lineTo(1, 1) @@ -127,6 +124,7 @@ class ApertureValveSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -163,6 +161,7 @@ class NeedleValveSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -191,13 +190,15 @@ def draw_icon(self, painter): top_arrow_point = QPointF(0.65, 0.36) arrow = QPolygonF( - [QPointF(-0.09, 0.0), - QPointF(-0.005, 0.0), - QPointF(-0.005, 0.8), - QPointF(0.005, 0.8), - QPointF(0.005, 0.0), - QPointF(0.09, 0.0), - QPointF(0.00, -0.25)] + [ + QPointF(-0.09, 0.0), + QPointF(-0.005, 0.0), + QPointF(-0.005, 0.8), + QPointF(0.005, 0.8), + QPointF(0.005, 0.0), + QPointF(0.09, 0.0), + QPointF(0.00, -0.25), + ] ) t = QTransform() @@ -219,6 +220,7 @@ class ProportionalValveSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -250,13 +252,15 @@ def draw_icon(self, painter): top_arrow_point = QPointF(0.65, 0.42) arrow = QPolygonF( - [QPointF(-0.07, 0.0), - QPointF(-0.005, 0.0), - QPointF(-0.005, 0.8), - QPointF(0.005, 0.8), - QPointF(0.005, 0.0), - QPointF(0.07, 0.0), - QPointF(0.00, -0.25)] + [ + QPointF(-0.07, 0.0), + QPointF(-0.005, 0.0), + QPointF(-0.005, 0.8), + QPointF(0.005, 0.8), + QPointF(0.005, 0.0), + QPointF(0.07, 0.0), + QPointF(0.00, -0.25), + ] ) t = QTransform() @@ -267,45 +271,43 @@ def draw_icon(self, painter): t_x = 0.4 t_y = 0.05 - painter.drawLines([QLineF(0.0+t_x, 0.0+t_y, 0.0+t_x, 0.2+t_y), - QLineF(0.0+t_x, 0.0+t_y, 0.1+t_x, 0.2+t_y), - QLineF(0.1+t_x, 0.2+t_y, 0.2+t_x, 0.0+t_y), - QLineF(0.2+t_x, 0.0+t_y, 0.2+t_x, 0.2+t_y)]) + painter.drawLines( + [ + QLineF(0.0 + t_x, 0.0 + t_y, 0.0 + t_x, 0.2 + t_y), + QLineF(0.0 + t_x, 0.0 + t_y, 0.1 + t_x, 0.2 + t_y), + QLineF(0.1 + t_x, 0.2 + t_y, 0.2 + t_x, 0.0 + t_y), + QLineF(0.2 + t_x, 0.0 + t_y, 0.2 + t_x, 0.2 + t_y), + ] + ) class ControlValveSymbolIcon(PneumaticValveSymbolIcon): """Icon for a Control Valve with readback""" + def draw_icon(self, painter): pen = painter.pen() - pen.setWidthF(pen.width()*2) + pen.setWidthF(pen.width() * 2) pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) # Circle parameters radius = 0.3 center = (0.5, 1 - radius) # Draw circle - painter.drawEllipse(QPointF(*center), - radius, radius) + painter.drawEllipse(QPointF(*center), radius, radius) # X pattern quad = math.cos(math.radians(45)) * radius - painter.drawLine(QLineF(center[0] + quad, - center[1] + quad, - center[0] - quad, - center[1] - quad)) - painter.drawLine(QLineF(center[0] + quad, - center[1] - quad, - center[0] - quad, - center[1] + quad)) + painter.drawLine(QLineF(center[0] + quad, center[1] + quad, center[0] - quad, center[1] - quad)) + painter.drawLine(QLineF(center[0] + quad, center[1] - quad, center[0] - quad, center[1] + quad)) # Interlock Icon square_dims = (0.4, 0.2) - painter.drawLine(QPointF(center[0], center[1] - radius), - QPointF(center[0], square_dims[1])) + painter.drawLine(QPointF(center[0], center[1] - radius), QPointF(center[0], square_dims[1])) painter.setBrush(self._interlock_brush) - painter.drawRect(QRectF((1 - square_dims[0])/2., 0, *square_dims)) + painter.drawRect(QRectF((1 - square_dims[0]) / 2.0, 0, *square_dims)) class ControlOnlyValveSymbolIcon(BaseSymbolIcon): """Icon for a Control Valve with no readback""" + def draw_icon(self, painter): path = QPainterPath(QPointF(0, 0.3)) path.lineTo(0, 0.9) @@ -324,6 +326,7 @@ class PneumaticValveNOSymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -368,6 +371,7 @@ class PneumaticValveDASymbolIcon(BaseSymbolIcon): parent : QWidget The parent widget for the icon """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern) @@ -405,13 +409,15 @@ def draw_icon(self, painter): length = 0.2 width = 0.02 rightward_arrow = QPolygonF( - [QPointF(tip_length, 0.0), - QPointF(0.0, -tip_width), - QPointF(0.0, -width), - QPointF(-length, -width), - QPointF(-length, width), - QPointF(0.0, width), - QPointF(0.0, tip_width)] + [ + QPointF(tip_length, 0.0), + QPointF(0.0, -tip_width), + QPointF(0.0, -width), + QPointF(-length, -width), + QPointF(-length, width), + QPointF(0.0, width), + QPointF(0.0, tip_width), + ] ) # Second arrow looks left point_left = QTransform() diff --git a/pcdswidgets/table.py b/pcdswidgets/table.py index 1f2388b..cade900 100644 --- a/pcdswidgets/table.py +++ b/pcdswidgets/table.py @@ -21,6 +21,7 @@ class FilterSortWidgetTable(QtWidgets.QTableWidget): This will allow you to sort or filter based on macros and based on the values in each pydm widget. """ + _qt_designer_ = { "group": "PCDS Utilities", "is_container": False, @@ -57,7 +58,7 @@ def __init__(self, *args, **kwargs): self._header_map = {} self._channels = [] self._filters = {} - self._initial_sort_header = 'index' + self._initial_sort_header = "index" self._initial_sort_ascend = True self._hide_headers = [] @@ -144,7 +145,7 @@ def reload_macros_file(self) -> None: macros = json.load(fd) self.set_macros(macros) except Exception: - logger.exception('') + logger.exception("") return def set_macros(self, macros_list: list[dict[str, str]]) -> None: @@ -161,11 +162,7 @@ def set_macros(self, macros_list: list[dict[str, str]]) -> None: have the same keys or this will not work properly. """ self._macros = macros_list - self._macro_headers = ( - list(self._macros[0].keys()) - if self._macros - else [] - ) + self._macro_headers = list(self._macros[0].keys()) if self._macros else [] self.reinit_table() def reinit_table(self) -> None: @@ -223,16 +220,16 @@ def add_row(self, macros: dict[str, str]) -> None: # Put the widget into the table self.setCellWidget(row_position, 0, widget) - self._header_map['widget'] = 0 + self._header_map["widget"] = 0 self.setRowHeight(row_position, widget.height()) # Put the index into the table item = ChannelTableWidgetItem( - header='index', + header="index", default=row_position, ) self.setItem(row_position, 1, item) - self._header_map['index'] = 1 + self._header_map["index"] = 1 # Put the macros into the table index = 2 for key, value in macros.items(): @@ -271,22 +268,22 @@ def contextMenuEvent(self, _event) -> None: On right click, create and open a settings menu. """ menu = QtWidgets.QMenu(parent=self) - configure_action = menu.addAction('Configure') + configure_action = menu.addAction("Configure") configure_action.setCheckable(True) configure_action.setChecked(self.configurable) configure_action.toggled.connect(self.request_configurable) - active_sort_action = menu.addAction('Active Re-sort') + active_sort_action = menu.addAction("Active Re-sort") active_sort_action.setCheckable(True) active_sort_action.setChecked(self.isSortingEnabled()) active_sort_action.toggled.connect(self.setSortingEnabled) - sort_menu = menu.addMenu('Sorting') + sort_menu = menu.addMenu("Sorting") for header_name in self._header_map.keys(): - if header_name == 'widget': + if header_name == "widget": continue if header_name in self.hide_headers_in_menu: continue inner_menu = sort_menu.addMenu(header_name.lower()) - asc = inner_menu.addAction('Ascending') + asc = inner_menu.addAction("Ascending") asc.triggered.connect( functools.partial( self.menu_sort, @@ -294,7 +291,7 @@ def contextMenuEvent(self, _event) -> None: ascending=True, ) ) - dec = inner_menu.addAction('Descending') + dec = inner_menu.addAction("Descending") dec.triggered.connect( functools.partial( self.menu_sort, @@ -302,7 +299,7 @@ def contextMenuEvent(self, _event) -> None: ascending=False, ) ) - filter_menu = menu.addMenu('Filters') + filter_menu = menu.addMenu("Filters") for filter_name, filter_info in self._filters.items(): inner_action = filter_menu.addAction(filter_name) inner_action.setCheckable(True) @@ -332,20 +329,15 @@ def get_row_values(self, row: int) -> dict[str, Any]: is the 'connected' str, which is True if all channels are connected. """ - values = {'connected': True} + values = {"connected": True} for col in range(1, self.columnCount()): item = self.item(row, col) values[item.header] = item.get_value() if not item.connected: - values['connected'] = False + values["connected"] = False return values - def add_filter( - self, - filter_name: str, - filter_func: Callable[[dict[str, Any]], bool], - active: bool = True - ) -> None: + def add_filter(self, filter_name: str, filter_func: Callable[[dict[str, Any]], bool], active: bool = True) -> None: """ Add a new visibility filter to the table. @@ -422,7 +414,7 @@ def update_filter(self, row: int) -> None: should_show = filt_info.filter_func(values) except Exception: logger.debug( - 'Error in filter function %s', + "Error in filter function %s", filt_info.name, exc_info=True, ) @@ -503,7 +495,7 @@ def initial_sort(self) -> None: """ self.sort_table(self.initial_sort_header, self.initial_sort_ascending) - @QtCore.Property('QStringList') + @QtCore.Property("QStringList") def hide_headers_in_menu(self) -> list[str]: """ A list of headers that we don't want to see in the sort menu. @@ -599,6 +591,7 @@ class ChannelTableWidgetItem(QtWidgets.QTableWidgetItem): Only update the table if the change is more than the deadband. This can help make large tables less resource-hungry. """ + header: str channel: str | None deadband: float @@ -610,7 +603,7 @@ def __init__( default: Any | None = None, channel: str | None = None, deadband: float = 0.0, - parent: QtWidgets.QWidget | None = None + parent: QtWidgets.QWidget | None = None, ): super().__init__(parent) self.header = header @@ -667,9 +660,9 @@ def __lt__(self, other: ChannelTableWidgetItem) -> bool: elif other.get_value() is None: return True # Make sure empty string sorts as next greatest - elif self.get_value() == '': + elif self.get_value() == "": return False - elif other.get_value() == '': + elif other.get_value() == "": return True return self.get_value() < other.get_value() diff --git a/pcdswidgets/tests/test_icons.py b/pcdswidgets/tests/test_icons.py index 780028f..8437be9 100644 --- a/pcdswidgets/tests/test_icons.py +++ b/pcdswidgets/tests/test_icons.py @@ -7,11 +7,10 @@ import pcdswidgets.icons from pcdswidgets.icons.base import BaseSymbolIcon -icons = [getattr(pcdswidgets.icons, icon) - for icon in pcdswidgets.icons.__all__] +icons = [getattr(pcdswidgets.icons, icon) for icon in pcdswidgets.icons.__all__] -@pytest.mark.parametrize('icon_class', icons, ids=pcdswidgets.icons.__all__) +@pytest.mark.parametrize("icon_class", icons, ids=pcdswidgets.icons.__all__) def test_icon_smoke(qtbot, icon_class): icon = icon_class() qtbot.addWidget(icon) @@ -20,19 +19,23 @@ def test_icon_smoke(qtbot, icon_class): icon.repaint() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def icon(qtbot): icon = BaseSymbolIcon() qtbot.addWidget(icon) return icon -@pytest.mark.parametrize('prop,value', - [('brush', QBrush(QColor(0, 0, 0), Qt.SolidPattern)), - ('penStyle', Qt.DotLine), - ('penColor', QColor(0, 0, 0)), - ('penWidth', 2.0)], - ids=('brush', 'penStyle', 'penColor', 'penWidth')) +@pytest.mark.parametrize( + "prop,value", + [ + ("brush", QBrush(QColor(0, 0, 0), Qt.SolidPattern)), + ("penStyle", Qt.DotLine), + ("penColor", QColor(0, 0, 0)), + ("penWidth", 2.0), + ], + ids=("brush", "penStyle", "penColor", "penWidth"), +) def test_icon_properties(icon, prop, value): setattr(icon, prop, value) assert getattr(icon, prop) == value diff --git a/pcdswidgets/tests/vacuum/test_base.py b/pcdswidgets/tests/vacuum/test_base.py index 3fc8665..dbc40bb 100644 --- a/pcdswidgets/tests/vacuum/test_base.py +++ b/pcdswidgets/tests/vacuum/test_base.py @@ -7,12 +7,13 @@ class BaseSymbol(PCDSSymbolBase): """Test Symbol for base class tests""" + def __init__(self, parent=None): super().__init__(parent=parent) self.icon = RGASymbolIcon(parent=self) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def symbol(qtbot): symbol = BaseSymbol() qtbot.addWidget(symbol) @@ -35,12 +36,16 @@ def test_no_controls_content(symbol): assert widget == symbol.icon -@pytest.mark.parametrize('location,layout,position', - [(ContentLocation.Top, QVBoxLayout, 0), - (ContentLocation.Bottom, QVBoxLayout, 1), - (ContentLocation.Left, QHBoxLayout, 0), - (ContentLocation.Right, QHBoxLayout, 1)], - ids=['Top', 'Bottom', 'Left', 'Right']) +@pytest.mark.parametrize( + "location,layout,position", + [ + (ContentLocation.Top, QVBoxLayout, 0), + (ContentLocation.Bottom, QVBoxLayout, 1), + (ContentLocation.Left, QHBoxLayout, 0), + (ContentLocation.Right, QHBoxLayout, 1), + ], + ids=["Top", "Bottom", "Left", "Right"], +) def test_controls_content_location(symbol, location, layout, position): symbol.controlsLocation = location assert isinstance(symbol.interlock.layout(), layout) @@ -56,18 +61,22 @@ def test_icon_fixed_size(symbol): assert symbol.icon.height() == size -@pytest.mark.parametrize('rotate', (False, True), ids=('Standard', 'Rotated')) +@pytest.mark.parametrize("rotate", (False, True), ids=("Standard", "Rotated")) def test_icon_rotation(symbol, rotate): symbol.rotateIcon = rotate assert symbol.icon.rotation == 90 * int(rotate) -@pytest.mark.parametrize('location,layout,position', - [(ContentLocation.Top, QVBoxLayout, 0), - (ContentLocation.Bottom, QVBoxLayout, 1), - (ContentLocation.Left, QVBoxLayout, 0), - (ContentLocation.Right, QVBoxLayout, 1)], - ids=['Top', 'Bottom', 'Left', 'Right']) +@pytest.mark.parametrize( + "location,layout,position", + [ + (ContentLocation.Top, QVBoxLayout, 0), + (ContentLocation.Bottom, QVBoxLayout, 1), + (ContentLocation.Left, QVBoxLayout, 0), + (ContentLocation.Right, QVBoxLayout, 1), + ], + ids=["Top", "Bottom", "Left", "Right"], +) def test_text_location(symbol, location, layout, position): symbol.controlsLocation = ContentLocation.Bottom symbol.channelsPrefix = "ca://area:function:device:01" @@ -79,10 +88,11 @@ def test_text_location(symbol, location, layout, position): assert widget == symbol.name -@pytest.mark.parametrize('location,layout,position', - [(ContentLocation.Left, QHBoxLayout, 0), - (ContentLocation.Right, QHBoxLayout, 1)], - ids=['Left', 'Right']) +@pytest.mark.parametrize( + "location,layout,position", + [(ContentLocation.Left, QHBoxLayout, 0), (ContentLocation.Right, QHBoxLayout, 1)], + ids=["Left", "Right"], +) def test_text_and_controls_location(symbol, location, layout, position): symbol.controlsLocation = location symbol.channelsPrefix = "ca://area:function:device:01" diff --git a/pcdswidgets/tests/vacuum/test_mixins.py b/pcdswidgets/tests/vacuum/test_mixins.py index 102e053..d27b60c 100644 --- a/pcdswidgets/tests/vacuum/test_mixins.py +++ b/pcdswidgets/tests/vacuum/test_mixins.py @@ -2,12 +2,12 @@ from qtpy.QtWidgets import QWidget from pcdswidgets.vacuum.base import PCDSSymbolBase -from pcdswidgets.vacuum.mixins import (ErrorMixin, InterlockMixin, - OpenCloseStateMixin, StateMixin) +from pcdswidgets.vacuum.mixins import ErrorMixin, InterlockMixin, OpenCloseStateMixin, StateMixin class PCDSSymbolWithIcon(PCDSSymbolBase): """Base mixable class""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.icon = QWidget(parent=self) @@ -15,61 +15,63 @@ def __init__(self, *args, **kwargs): class Interlock(InterlockMixin, PCDSSymbolWithIcon): """Simplest Interlock Widget""" + pass class Error(ErrorMixin, PCDSSymbolWithIcon): """Simplest Error Widget""" + pass class State(StateMixin, PCDSSymbolWithIcon): """Simplest State Widget""" + pass class OpenClose(OpenCloseStateMixin, PCDSSymbolWithIcon): """Simplest OpenCloseState Widget""" + pass -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def interlock(qtbot): - inter = Interlock(':ILK') + inter = Interlock(":ILK") qtbot.addWidget(inter) inter.create_channels() return inter -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def error(qtbot): - error = Error(':ILK') + error = Error(":ILK") qtbot.addWidget(error) error.create_channels() - error.error_enum_changed(('Bad', 'Good')) + error.error_enum_changed(("Bad", "Good")) return error -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def state(qtbot): - state = State(':Status') + state = State(":Status") qtbot.addWidget(state) state.create_channels() - state.state_enum_changed(('Bad', 'Good')) + state.state_enum_changed(("Bad", "Good")) return state -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def openclose(qtbot): - openclose = OpenClose(':Open', 'Close') + openclose = OpenClose(":Open", "Close") qtbot.addWidget(openclose) openclose.create_channels() return openclose -@pytest.mark.parametrize('interlock_bit', - (0, 1), - ids=('low', 'high')) +@pytest.mark.parametrize("interlock_bit", (0, 1), ids=("low", "high")) def test_interlock_value_changed(interlock, interlock_bit): interlock.interlock_value_changed(not interlock_bit) orig_tooltip = interlock.status_tooltip() @@ -83,22 +85,23 @@ def test_interlock_value_changed(interlock, interlock_bit): def test_error_value_changed(error): orig_tooltip = error.status_tooltip() error.error_value_changed(1) - error.error == 'Good' + error.error == "Good" assert orig_tooltip != error.status_tooltip() def test_state_value_changed(state): orig_tooltip = state.status_tooltip() state.state_value_changed(1) - state.state == 'Good' + state.state == "Good" assert orig_tooltip != state.status_tooltip() -@pytest.mark.parametrize('open_switch,closed_switch,state', - [(1, 1, 'INVALID'), (1, 0, 'Open'), - (0, 1, 'Close'), (0, 0, 'INVALID')], - ids=['Fault', 'Open', 'Closed', 'Invalid']) +@pytest.mark.parametrize( + "open_switch,closed_switch,state", + [(1, 1, "INVALID"), (1, 0, "Open"), (0, 1, "Close"), (0, 0, "INVALID")], + ids=["Fault", "Open", "Closed", "Invalid"], +) def test_openclose_value_changed(openclose, open_switch, closed_switch, state): - openclose.state_value_changed('OPEN', open_switch) - openclose.state_value_changed('CLOSE', closed_switch) + openclose.state_value_changed("OPEN", open_switch) + openclose.state_value_changed("CLOSE", closed_switch) assert openclose.state == state diff --git a/pcdswidgets/tests/vacuum/test_symbols.py b/pcdswidgets/tests/vacuum/test_symbols.py index 15cb218..dbe10e2 100644 --- a/pcdswidgets/tests/vacuum/test_symbols.py +++ b/pcdswidgets/tests/vacuum/test_symbols.py @@ -2,11 +2,10 @@ import pcdswidgets.vacuum -symbols = [getattr(pcdswidgets.vacuum, symbol) - for symbol in pcdswidgets.vacuum.__all__] +symbols = [getattr(pcdswidgets.vacuum, symbol) for symbol in pcdswidgets.vacuum.__all__] -@pytest.mark.parametrize('symbol', symbols, ids=pcdswidgets.vacuum.__all__) +@pytest.mark.parametrize("symbol", symbols, ids=pcdswidgets.vacuum.__all__) def test_vacuum_widgets(qtbot, symbol): widget = symbol() qtbot.addWidget(widget) diff --git a/pcdswidgets/vacuum/__init__.py b/pcdswidgets/vacuum/__init__.py index 41b2f75..0a1abae 100644 --- a/pcdswidgets/vacuum/__init__.py +++ b/pcdswidgets/vacuum/__init__.py @@ -1,16 +1,48 @@ -__all__ = ['HotCathodeGauge', 'RoughGauge', 'ColdCathodeGauge', 'IonPump', - 'TurboPump', 'ScrollPump', 'GetterPump', 'RGA', 'PneumaticValve', - 'ApertureValve', 'FastShutter', 'NeedleValve', 'ProportionalValve', - 'RightAngleManualValve', 'ControlValve', 'ControlOnlyValveNC', - 'ControlOnlyValveNO', 'PneumaticValveNO', 'PneumaticValveDA', - 'CapacitanceManometerGauge', 'HotCathodeComboGauge', 'ColdCathodeComboGauge'] +__all__ = [ + "HotCathodeGauge", + "RoughGauge", + "ColdCathodeGauge", + "IonPump", + "TurboPump", + "ScrollPump", + "GetterPump", + "RGA", + "PneumaticValve", + "ApertureValve", + "FastShutter", + "NeedleValve", + "ProportionalValve", + "RightAngleManualValve", + "ControlValve", + "ControlOnlyValveNC", + "ControlOnlyValveNO", + "PneumaticValveNO", + "PneumaticValveDA", + "CapacitanceManometerGauge", + "HotCathodeComboGauge", + "ColdCathodeComboGauge", +] -from .gauges import (CapacitanceManometerGauge, ColdCathodeComboGauge, - ColdCathodeGauge, HotCathodeComboGauge, HotCathodeGauge, - RoughGauge) +from .gauges import ( + CapacitanceManometerGauge, + ColdCathodeComboGauge, + ColdCathodeGauge, + HotCathodeComboGauge, + HotCathodeGauge, + RoughGauge, +) from .others import RGA from .pumps import GetterPump, IonPump, ScrollPump, TurboPump -from .valves import (ApertureValve, ControlOnlyValveNC, ControlOnlyValveNO, - ControlValve, FastShutter, NeedleValve, PneumaticValve, - PneumaticValveDA, PneumaticValveNO, ProportionalValve, - RightAngleManualValve) +from .valves import ( + ApertureValve, + ControlOnlyValveNC, + ControlOnlyValveNO, + ControlValve, + FastShutter, + NeedleValve, + PneumaticValve, + PneumaticValveDA, + PneumaticValveNO, + ProportionalValve, + RightAngleManualValve, +) diff --git a/pcdswidgets/vacuum/base.py b/pcdswidgets/vacuum/base.py index 33f5497..b68d379 100644 --- a/pcdswidgets/vacuum/base.py +++ b/pcdswidgets/vacuum/base.py @@ -8,8 +8,17 @@ from pydm.widgets.embedded_display import PyDMEmbeddedDisplay from qtpy.QtCore import Q_ENUMS, Property, QSize, Qt from qtpy.QtGui import QCursor, QPainter -from qtpy.QtWidgets import (QFrame, QHBoxLayout, QLabel, QSizePolicy, QStyle, - QStyleOption, QTabWidget, QVBoxLayout, QWidget) +from qtpy.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QSizePolicy, + QStyle, + QStyleOption, + QTabWidget, + QVBoxLayout, + QWidget, +) from ..utils import refresh_style @@ -21,6 +30,7 @@ class ContentLocation: Enum Class to be used by the widgets to configure the Controls Content Location. """ + Hidden = 0 Top = 1 Bottom = 2 @@ -67,35 +77,30 @@ def __init__(self, parent=None, **kwargs): self.name = QLabel(self) self.name.setWordWrap(True) - self.name.setSizePolicy(QSizePolicy.Maximum, - QSizePolicy.Maximum) + self.name.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.name.setAlignment(Qt.AlignCenter) self.name.setStyleSheet(f"font-size: {self._font_size}px; background: transparent") self.name.setVisible(self._show_name) - self._icon_cursor = self.setCursor( - QCursor(IconFont().icon("file").pixmap(16, 16)) - ) + self._icon_cursor = self.setCursor(QCursor(IconFont().icon("file").pixmap(16, 16))) self._expert_ophyd_class = self.EXPERT_OPHYD_CLASS or "" self.interlock = QFrame(self) self.interlock.setObjectName("interlock") - self.interlock.setSizePolicy(QSizePolicy.Expanding, - QSizePolicy.Expanding) + self.interlock.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.controls_frame = QFrame(self) self.controls_frame.setObjectName("controls") - self.controls_frame.setSizePolicy(QSizePolicy.Maximum, - QSizePolicy.Maximum) + self.controls_frame.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.setLayout(QVBoxLayout()) self.layout().setSpacing(0) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.interlock) - if not hasattr(self, '_controls_location'): + if not hasattr(self, "_controls_location"): self._controls_location = ContentLocation.Bottom - if not hasattr(self, '_text_lcoation'): + if not hasattr(self, "_text_lcoation"): self._text_location = ContentLocation.Top self.setup_icon() @@ -398,18 +403,16 @@ def iconSize(self, size): return if size <= 0: - size = - 1 + size = -1 min_size = 1 max_size = 999999 - self.icon.setSizePolicy(QSizePolicy.Expanding, - QSizePolicy.Expanding) + self.icon.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.icon.setMinimumSize(min_size, min_size) self.icon.setMaximumSize(max_size, max_size) else: self.icon.setFixedSize(size, size) - self.icon.setSizePolicy(QSizePolicy.Fixed, - QSizePolicy.Fixed) + self.icon.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self._icon_size = size self.icon.update() @@ -518,7 +521,10 @@ def assemble_layout(self): grouped_widgets = QVBoxLayout() # Default # Determine what widgets to group - if self._text_location in [ContentLocation.Left, ContentLocation.Right] and self._text_location == self._controls_location: + if ( + self._text_location in [ContentLocation.Left, ContentLocation.Right] + and self._text_location == self._controls_location + ): grouped_widgets = QVBoxLayout() if self.name is not None: grouped_widgets.addWidget(self.name, alignment=Qt.AlignCenter) @@ -534,10 +540,14 @@ def assemble_layout(self): # Group icon and name if self._text_location in [ContentLocation.Left, ContentLocation.Right]: grouped_widgets = QHBoxLayout() - icon_and_text = [self.name, self.icon] if self._text_location == ContentLocation.Left else [self.icon, self.name] + icon_and_text = ( + [self.name, self.icon] if self._text_location == ContentLocation.Left else [self.icon, self.name] + ) else: grouped_widgets = QVBoxLayout() - icon_and_text = [self.name, self.icon] if self._text_location == ContentLocation.Top else [self.icon, self.name] + icon_and_text = ( + [self.name, self.icon] if self._text_location == ContentLocation.Top else [self.icon, self.name] + ) for widget in icon_and_text: if widget is None: @@ -548,10 +558,18 @@ def assemble_layout(self): if self._controls_location in [ContentLocation.Left, ContentLocation.Right]: layout_cls = QHBoxLayout - widgets = [self.controls_frame, grouped_frame] if self._controls_location == ContentLocation.Left else [grouped_frame, self.controls_frame] + widgets = ( + [self.controls_frame, grouped_frame] + if self._controls_location == ContentLocation.Left + else [grouped_frame, self.controls_frame] + ) else: layout_cls = QVBoxLayout - widgets = [self.controls_frame, grouped_frame] if self._controls_location == ContentLocation.Top else [grouped_frame, self.controls_frame] + widgets = ( + [self.controls_frame, grouped_frame] + if self._controls_location == ContentLocation.Top + else [grouped_frame, self.controls_frame] + ) grouped_widgets.setContentsMargins(0, 0, 0, 0) grouped_widgets.setSpacing(0) @@ -579,29 +597,29 @@ def setup_icon(self): if not self.icon: return self.icon.setMinimumSize(16, 16) - self.icon.setSizePolicy(QSizePolicy.Expanding, - QSizePolicy.Expanding) + self.icon.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.icon.setVisible(self._show_icon) self.iconSize = 32 - if hasattr(self.icon, 'clicked'): + if hasattr(self.icon, "clicked"): self.icon.clicked.connect(self._handle_icon_click) if self._expert_display is not None: self.icon.setCursor(self._icon_cursor) def _handle_icon_click(self): if not self.channelsPrefix: - logger.error('No channel prefix specified.' - 'Cannot proceed with opening expert screen for %s.', - self.__class__.__name__) + logger.error( + "No channel prefix specified." "Cannot proceed with opening expert screen for %s.", + self.__class__.__name__, + ) return if self.tab_widget is not None: - logger.debug('Bringing existing custom display to front.') + logger.debug("Bringing existing custom display to front.") self.tab_widget.show() self.tab_widget.raise_() return elif self._expert_display is not None: - logger.debug('Bringing existing display to front.') + logger.debug("Bringing existing display to front.") self._expert_display.show() self._expert_display.raise_() return @@ -609,15 +627,14 @@ def _handle_icon_click(self): prefix = remove_protocol(self.channelsPrefix) klass = self.expertOphydClass if not klass: - logger.error('No expertOphydClass specified for pcdswidgets %s', - self.__class__.__name__) + logger.error("No expertOphydClass specified for pcdswidgets %s", self.__class__.__name__) return - name = prefix.replace(':', '_') + name = prefix.replace(":", "_") try: import typhos except ImportError: - logger.error('Typhos not installed. Cannot create display.') + logger.error("Typhos not installed. Cannot create display.") return kwargs = {"name": name, "prefix": prefix} @@ -630,14 +647,10 @@ def _handle_icon_click(self): self.tab_widget.setTabPosition(QTabWidget.TabPosition.West) self.tab_widget.addTab(display, "Typhos") - for file_path, title, macros in zip_longest( - self.ui_file_paths, - self.ui_file_titles, - self.ui_file_macros - ): + for file_path, title, macros in zip_longest(self.ui_file_paths, self.ui_file_titles, self.ui_file_macros): embedded = PyDMEmbeddedDisplay() title = title or file_path - macros = macros or '' + macros = macros or "" embedded.set_macros_and_filename(file_path, macros) self.tab_widget.addTab(embedded, title) @@ -648,7 +661,7 @@ def _handle_icon_click(self): elif display: display.show() - @Property('QStringList') + @Property("QStringList") def ui_paths(self): return self.ui_file_paths @@ -657,7 +670,7 @@ def ui_paths(self, path): if path != self.ui_file_paths: self.ui_file_paths = path - @Property('QStringList') + @Property("QStringList") def ui_macros(self): return self.ui_file_macros @@ -666,7 +679,7 @@ def ui_macros(self, macros): if macros != self.ui_macros: self.ui_file_macros = macros - @Property('QStringList') + @Property("QStringList") def ui_titles(self): return self.ui_file_titles @@ -687,7 +700,7 @@ def status_tooltip(self): str """ status = "" - if hasattr(self, 'NAME'): + if hasattr(self, "NAME"): status = self.NAME if status: status += os.linesep diff --git a/pcdswidgets/vacuum/demo/__main__.py b/pcdswidgets/vacuum/demo/__main__.py index 1804615..0d451a7 100644 --- a/pcdswidgets/vacuum/demo/__main__.py +++ b/pcdswidgets/vacuum/demo/__main__.py @@ -4,6 +4,7 @@ Invoke as e.g. "python -m pcdswidgets.vacuum.demo PneumaticValveDA CRIX:VGC:11" """ + import sys import pydm @@ -26,6 +27,6 @@ ) widget = globals()[cls]() -widget.channelsPrefix = 'ca://' + sys.argv[2] +widget.channelsPrefix = "ca://" + sys.argv[2] app.main_window.set_display_widget(widget) app.exec() diff --git a/pcdswidgets/vacuum/gauges.py b/pcdswidgets/vacuum/gauges.py index 7c74c1f..7601541 100644 --- a/pcdswidgets/vacuum/gauges.py +++ b/pcdswidgets/vacuum/gauges.py @@ -1,14 +1,16 @@ from pydm.widgets.display_format import DisplayFormat from qtpy.QtCore import QSize -from ..icons.gauges import (CapManometerGaugeSymbolIcon, - ColdCathodeComboGaugeSymbolIcon, - ColdCathodeGaugeSymbolIcon, - HotCathodeComboGaugeSymbolIcon, - HotCathodeGaugeSymbolIcon, RoughGaugeSymbolIcon) +from ..icons.gauges import ( + CapManometerGaugeSymbolIcon, + ColdCathodeComboGaugeSymbolIcon, + ColdCathodeGaugeSymbolIcon, + HotCathodeComboGaugeSymbolIcon, + HotCathodeGaugeSymbolIcon, + RoughGaugeSymbolIcon, +) from .base import PCDSSymbolBase -from .mixins import (ButtonLabelControl, InterlockMixin, LabelControl, - StateMixin) +from .mixins import ButtonLabelControl, InterlockMixin, LabelControl, StateMixin class RoughGauge(StateMixin, LabelControl, PCDSSymbolBase): @@ -80,8 +82,9 @@ def __init__(self, parent=None, **kwargs): parent=parent, state_suffix=self._state_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = RoughGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -167,8 +170,9 @@ def __init__(self, parent=None, **kwargs): state_suffix=self._state_suffix, command_suffix=self._command_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = HotCathodeGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -255,8 +259,9 @@ def __init__(self, parent=None, **kwargs): state_suffix=self._state_suffix, command_suffix=self._command_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = ColdCathodeGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -333,8 +338,9 @@ def __init__(self, parent=None, **kwargs): parent=parent, state_suffix=self._state_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = ColdCathodeComboGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -411,8 +417,9 @@ def __init__(self, parent=None, **kwargs): parent=parent, state_suffix=self._state_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = HotCathodeComboGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -489,8 +496,9 @@ def __init__(self, parent=None, **kwargs): parent=parent, state_suffix=self._state_suffix, readback_suffix=self._readback_suffix, - readback_name='pressure', - **kwargs) + readback_name="pressure", + **kwargs, + ) self.icon = CapManometerGaugeSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential diff --git a/pcdswidgets/vacuum/mixins.py b/pcdswidgets/vacuum/mixins.py index e3706fc..01ed810 100644 --- a/pcdswidgets/vacuum/mixins.py +++ b/pcdswidgets/vacuum/mixins.py @@ -32,6 +32,7 @@ class InterlockMixin: The suffix to be used along with the channelPrefix from PCDSSymbolBase to compose the interlock channel address. """ + def __init__(self, interlock_suffix, **kwargs): self._interlock_suffix = interlock_suffix self._interlocked = False @@ -64,10 +65,9 @@ def create_channels(self): self._interlock_connected = False self.interlock_channel = PyDMChannel( - address="{}{}".format(self._channels_prefix, - self._interlock_suffix), + address="{}{}".format(self._channels_prefix, self._interlock_suffix), connection_slot=self.interlock_connection_changed, - value_slot=self.interlock_value_changed + value_slot=self.interlock_value_changed, ) self.interlock_channel.connect() @@ -133,6 +133,7 @@ class ErrorMixin: The suffix to be used along with the channelPrefix from PCDSSymbolBase to compose the error channel address. """ + def __init__(self, error_suffix, **kwargs): self._error_suffix = error_suffix self._error = "" @@ -170,7 +171,7 @@ def create_channels(self): address=f"{self._channels_prefix}{self._error_suffix}", connection_slot=self.error_connection_changed, value_slot=self.error_value_changed, - enum_strings_slot=self.error_enum_changed + enum_strings_slot=self.error_enum_changed, ) self.error_channel.connect() @@ -271,6 +272,7 @@ class StateMixin: The suffix to be used along with the channelPrefix from PCDSSymbolBase to compose the state channel address. """ + def __init__(self, state_suffix, **kwargs): self._state_suffix = state_suffix self._state = "" @@ -308,7 +310,7 @@ def create_channels(self): address=f"{self._channels_prefix}{self._state_suffix}", connection_slot=self.state_connection_changed, value_slot=self.state_value_changed, - enum_strings_slot=self.state_enum_changed + enum_strings_slot=self.state_enum_changed, ) self.state_channel.connect() @@ -414,6 +416,7 @@ class OpenCloseStateMixin: The suffix to be used along with the channelPrefix from PCDSSymbolBase to compose the close state channel address. """ + def __init__(self, open_suffix, close_suffix, **kwargs): self._open_suffix = open_suffix self._close_suffix = close_suffix @@ -465,14 +468,14 @@ def create_channels(self): self.state_open_channel = PyDMChannel( address=f"{self._channels_prefix}{self._open_suffix}", connection_slot=partial(self.state_connection_changed, "OPEN"), - value_slot=partial(self.state_value_changed, "OPEN") + value_slot=partial(self.state_value_changed, "OPEN"), ) self.state_open_channel.connect() self.state_close_channel = PyDMChannel( address=f"{self._channels_prefix}{self._close_suffix}", connection_slot=partial(self.state_connection_changed, "CLOSE"), - value_slot=partial(self.state_value_changed, "CLOSE") + value_slot=partial(self.state_value_changed, "CLOSE"), ) self.state_close_channel.connect() @@ -542,6 +545,7 @@ class ButtonControl: The suffix to be used along with the channelPrefix from PCDSSymbolBase to compose the command button channel address. """ + def __init__(self, command_suffix, **kwargs): self._command_suffix = command_suffix self._orientation = Qt.Horizontal @@ -578,8 +582,7 @@ def create_channels(self): """ super().create_channels() if self._channels_prefix: - self.control_btn.channel = "{}{}".format(self._channels_prefix, - self._command_suffix) + self.control_btn.channel = "{}{}".format(self._channels_prefix, self._command_suffix) def destroy_channels(self): """ @@ -604,8 +607,8 @@ class LabelControl: The name to be set to the PyDMLabel so one can refer to it by name with stylesheet """ - def __init__(self, readback_suffix, readback_name, - **kwargs): + + def __init__(self, readback_suffix, readback_name, **kwargs): self._readback_suffix = readback_suffix self.readback_label = PyDMLabel() if readback_name: @@ -626,8 +629,7 @@ def create_channels(self): """ super().create_channels() if self._channels_prefix: - self.readback_label.channel = "{}{}".format(self._channels_prefix, - self._readback_suffix) + self.readback_label.channel = "{}{}".format(self._channels_prefix, self._readback_suffix) def destroy_channels(self): """ @@ -656,8 +658,8 @@ class ButtonLabelControl(ButtonControl): The name to be set to the PyDMLabel so one can refer to it by name with stylesheet """ - def __init__(self, command_suffix, readback_suffix, readback_name, - **kwargs): + + def __init__(self, command_suffix, readback_suffix, readback_name, **kwargs): self._readback_suffix = readback_suffix self.readback_label = PyDMLabel() @@ -674,8 +676,7 @@ def create_channels(self): """ super().create_channels() if self._channels_prefix: - self.readback_label.channel = "{}{}".format(self._channels_prefix, - self._readback_suffix) + self.readback_label.channel = "{}{}".format(self._channels_prefix, self._readback_suffix) def destroy_channels(self): """ @@ -706,6 +707,7 @@ class MultipleButtonControl: - value the value to be written when the button is pressed """ + def __init__(self, *, commands, **kwargs): self._command_buttons_config = commands self._orientation = Qt.Horizontal @@ -755,12 +757,12 @@ def clear_control_layout(self): def create_buttons(self): for btn in self._command_buttons_config: try: - text = btn['text'] - value = btn['value'] + text = btn["text"] + value = btn["value"] btn = PyDMPushButton(label=text, pressValue=value) self.buttons.append(btn) except KeyError: - logger.exception('Invalid config for MultipleButtonControl.') + logger.exception("Invalid config for MultipleButtonControl.") def create_channels(self): """ @@ -771,7 +773,7 @@ def create_channels(self): super().create_channels() if self._channels_prefix: for idx, btn in enumerate(self.buttons): - suffix = self._command_buttons_config[idx]['suffix'] + suffix = self._command_buttons_config[idx]["suffix"] btn.channel = f"{self._channels_prefix}{suffix}" def destroy_channels(self): diff --git a/pcdswidgets/vacuum/pumps.py b/pcdswidgets/vacuum/pumps.py index 40b3008..8207708 100644 --- a/pcdswidgets/vacuum/pumps.py +++ b/pcdswidgets/vacuum/pumps.py @@ -1,16 +1,12 @@ from pydm.widgets.display_format import DisplayFormat from qtpy.QtCore import Property, QSize -from ..icons.pumps import (GetterPumpSymbolIcon, IonPumpSymbolIcon, - ScrollPumpSymbolIcon, TurboPumpSymbolIcon) +from ..icons.pumps import GetterPumpSymbolIcon, IonPumpSymbolIcon, ScrollPumpSymbolIcon, TurboPumpSymbolIcon from .base import ContentLocation, PCDSSymbolBase -from .mixins import (ButtonControl, ButtonLabelControl, ErrorMixin, - InterlockMixin, StateMixin) +from .mixins import ButtonControl, ButtonLabelControl, ErrorMixin, InterlockMixin, StateMixin -class IonPump( - InterlockMixin, ErrorMixin, StateMixin, ButtonLabelControl, PCDSSymbolBase -): +class IonPump(InterlockMixin, ErrorMixin, StateMixin, ButtonLabelControl, PCDSSymbolBase): """ A Symbol Widget representing an Ion Pump with the proper icon and controls. @@ -98,7 +94,7 @@ def __init__(self, parent=None, **kwargs): command_suffix=self._command_suffix, readback_suffix=self._readback_suffix, readback_name="pressure", - **kwargs + **kwargs, ) self.icon = IonPumpSymbolIcon(parent=self) self.readback_label.displayFormat = DisplayFormat.Exponential @@ -190,7 +186,8 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = TurboPumpSymbolIcon(parent=self) def sizeHint(self): @@ -280,7 +277,8 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ScrollPumpSymbolIcon(parent=self) def sizeHint(self): diff --git a/pcdswidgets/vacuum/valves.py b/pcdswidgets/vacuum/valves.py index 68607ef..96d5a76 100644 --- a/pcdswidgets/vacuum/valves.py +++ b/pcdswidgets/vacuum/valves.py @@ -3,22 +3,23 @@ from qtpy.QtCore import Property, QSize, Qt from qtpy.QtWidgets import QGridLayout -from ..icons.valves import (ApertureValveSymbolIcon, - ControlOnlyValveSymbolIcon, ControlValveSymbolIcon, - FastShutterSymbolIcon, NeedleValveSymbolIcon, - PneumaticValveDASymbolIcon, - PneumaticValveNOSymbolIcon, - PneumaticValveSymbolIcon, - ProportionalValveSymbolIcon, - RightAngleManualValveSymbolIcon) +from ..icons.valves import ( + ApertureValveSymbolIcon, + ControlOnlyValveSymbolIcon, + ControlValveSymbolIcon, + FastShutterSymbolIcon, + NeedleValveSymbolIcon, + PneumaticValveDASymbolIcon, + PneumaticValveNOSymbolIcon, + PneumaticValveSymbolIcon, + ProportionalValveSymbolIcon, + RightAngleManualValveSymbolIcon, +) from .base import ContentLocation, PCDSSymbolBase -from .mixins import (ButtonControl, ErrorMixin, InterlockMixin, - MultipleButtonControl, StateMixin) +from .mixins import ButtonControl, ErrorMixin, InterlockMixin, MultipleButtonControl, StateMixin -class PneumaticValve( - InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase -): +class PneumaticValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase): """ A Symbol Widget representing a Pneumatic Valve with the proper icon and controls. @@ -110,16 +111,15 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = PneumaticValveSymbolIcon(parent=self) def sizeHint(self): return QSize(180, 70) -class ApertureValve( - InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase -): +class ApertureValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase): """ A Symbol Widget representing an Aperture Valve with the proper icon and controls. @@ -210,16 +210,15 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ApertureValveSymbolIcon(parent=self) def sizeHint(self): return QSize(180, 70) -class FastShutter( - InterlockMixin, ErrorMixin, StateMixin, MultipleButtonControl, PCDSSymbolBase -): +class FastShutter(InterlockMixin, ErrorMixin, StateMixin, MultipleButtonControl, PCDSSymbolBase): """ A Symbol Widget representing a Fast Shutter with the proper icon and controls. @@ -307,7 +306,8 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, commands=self._command_buttons, - **kwargs) + **kwargs, + ) self.icon = FastShutterSymbolIcon(parent=self) def sizeHint(self): @@ -389,7 +389,8 @@ def __init__(self, parent=None, **kwargs): interlock_suffix=self._interlock_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = NeedleValveSymbolIcon(parent=self) def sizeHint(self): @@ -471,7 +472,8 @@ def __init__(self, parent=None, **kwargs): interlock_suffix=self._interlock_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ProportionalValveSymbolIcon(parent=self) def sizeHint(self): @@ -544,9 +546,7 @@ def controlsLocation(self): return super().controlsLocation -class ControlValve( - InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase -): +class ControlValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase): """ A Symbol Widget representing a Control Valve with the proper icon and controls. @@ -622,7 +622,7 @@ class ControlValve( "group": "PCDS Valves", "is_container": False, } - NAME = 'Control Valve with Readback' + NAME = "Control Valve with Readback" EXPERT_OPHYD_CLASS = "pcdsdevices.valve.VVC" _interlock_suffix = ":OPN_OK_RBV" @@ -637,7 +637,8 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ControlValveSymbolIcon(parent=self) def sizeHint(self): @@ -717,11 +718,11 @@ class ControlOnlyValveNC(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBa "group": "PCDS Valves", "is_container": False, } - NAME = 'Normally Closed Control Valve with No Readback' + NAME = "Normally Closed Control Valve with No Readback" EXPERT_OPHYD_CLASS = "pcdsdevices.valve.VVC" _interlock_suffix = ":OPN_OK_RBV" - _state_suffix = ':OPN_DO_RBV' + _state_suffix = ":OPN_DO_RBV" _command_suffix = ":OPN_SW" def __init__(self, parent=None, **kwargs): @@ -730,7 +731,8 @@ def __init__(self, parent=None, **kwargs): interlock_suffix=self._interlock_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ControlOnlyValveSymbolIcon(parent=self) def sizeHint(self): @@ -810,11 +812,11 @@ class ControlOnlyValveNO(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBa "group": "PCDS Valves", "is_container": False, } - NAME = 'Normally Open Control Valve with No Readback' + NAME = "Normally Open Control Valve with No Readback" EXPERT_OPHYD_CLASS = "pcdsdevices.valve.VVCNO" _interlock_suffix = ":CLS_OK_RBV" - _state_suffix = ':CLS_DO_RBV' + _state_suffix = ":CLS_DO_RBV" _command_suffix = ":CLS_SW" def __init__(self, parent=None, **kwargs): @@ -823,13 +825,12 @@ def __init__(self, parent=None, **kwargs): interlock_suffix=self._interlock_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = ControlOnlyValveSymbolIcon(parent=self) -class PneumaticValveNO( - InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase -): +class PneumaticValveNO(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbolBase): """ A Symbol Widget representing a Normally Open Pneumatic Valve with the proper icon and controls. @@ -920,7 +921,8 @@ def __init__(self, parent=None, **kwargs): error_suffix=self._error_suffix, state_suffix=self._state_suffix, command_suffix=self._command_suffix, - **kwargs) + **kwargs, + ) self.icon = PneumaticValveNOSymbolIcon(parent=self) def sizeHint(self): @@ -1030,14 +1032,15 @@ def __init__(self, parent=None, **kwargs): interlock_suffix=self._interlock_suffix, error_suffix=self._error_suffix, state_suffix=self._state_suffix, - **kwargs) + **kwargs, + ) self.icon = PneumaticValveDASymbolIcon(parent=self) self.open_btn = PyDMPushButton( - label='OPEN', + label="OPEN", pressValue=1, ) self.cls_btn = PyDMPushButton( - label='CLOSE', + label="CLOSE", pressValue=1, ) self.open_btn.setFixedSize(55, 25) @@ -1095,10 +1098,9 @@ def create_channels(self): self._cls_interlock_connected = False self.cls_interlock_channel = PyDMChannel( - address="{}{}".format(self._channels_prefix, - self._cls_interlock_suffix), + address="{}{}".format(self._channels_prefix, self._cls_interlock_suffix), connection_slot=self.cls_interlock_connection_changed, - value_slot=self.cls_interlock_value_changed + value_slot=self.cls_interlock_value_changed, ) self.cls_interlock_channel.connect() diff --git a/pcdswidgets/version.py b/pcdswidgets/version.py index da8a5d5..fa1e59b 100644 --- a/pcdswidgets/version.py +++ b/pcdswidgets/version.py @@ -22,6 +22,7 @@ class VersionProxy(UserString): 4. A fallback in case none of the above match - resulting in a version of 0.0.unknown """ + def __init__(self): self._version = None @@ -32,6 +33,7 @@ def _get_version(self) -> Optional[str]: try: # Git checkout from setuptools_scm import get_version + return get_version(root="..", relative_to=__file__) except (ImportError, LookupError): ... @@ -40,6 +42,7 @@ def _get_version(self) -> Optional[str]: # done a build at least once. try: from ._version import version # noqa: F401 + return version except ImportError: ... @@ -51,7 +54,7 @@ def data(self) -> str: # This is accessed by UserString to allow us to lazily fill in the # information if self._version is None: - self._version = self._get_version() or '0.0.unknown' + self._version = self._get_version() or "0.0.unknown" return self._version From 96bd72506f39e6b8d1d094da69b92a218a64f844 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:45:31 -0700 Subject: [PATCH 028/104] STY: manual fixes from pre-commit output --- pcdswidgets/table.py | 4 ++-- pcdswidgets/tests/vacuum/test_mixins.py | 4 ++-- pcdswidgets/vacuum/base.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pcdswidgets/table.py b/pcdswidgets/table.py index cade900..8d6d7cd 100644 --- a/pcdswidgets/table.py +++ b/pcdswidgets/table.py @@ -260,8 +260,8 @@ def add_context_menu_to_children(self, widget: QtWidgets.QWidget) -> None: This makes it so you can right click to configure the table from within any of the contained widgets. """ - for widget in widget.children(): - widget.contextMenuEvent = self.contextMenuEvent + for child_widget in widget.children(): + child_widget.contextMenuEvent = self.contextMenuEvent def contextMenuEvent(self, _event) -> None: """ diff --git a/pcdswidgets/tests/vacuum/test_mixins.py b/pcdswidgets/tests/vacuum/test_mixins.py index d27b60c..a33449d 100644 --- a/pcdswidgets/tests/vacuum/test_mixins.py +++ b/pcdswidgets/tests/vacuum/test_mixins.py @@ -85,14 +85,14 @@ def test_interlock_value_changed(interlock, interlock_bit): def test_error_value_changed(error): orig_tooltip = error.status_tooltip() error.error_value_changed(1) - error.error == "Good" + assert error.error == "Good" assert orig_tooltip != error.status_tooltip() def test_state_value_changed(state): orig_tooltip = state.status_tooltip() state.state_value_changed(1) - state.state == "Good" + assert state.state == "Good" assert orig_tooltip != state.status_tooltip() diff --git a/pcdswidgets/vacuum/base.py b/pcdswidgets/vacuum/base.py index b68d379..b88f4b1 100644 --- a/pcdswidgets/vacuum/base.py +++ b/pcdswidgets/vacuum/base.py @@ -112,7 +112,7 @@ def __init__(self, parent=None, **kwargs): self.ui_file_titles = [] self.tab_widget = None - self.embedded_displays = list() + self.embedded_displays = [] def sizeHint(self): """ @@ -507,7 +507,7 @@ def clear(self): # empty widget. QWidget().setLayout(self.interlock.layout()) - def assemble_layout(self): + def assemble_layout(self): # noqa: C901 """ Assembles the widget's inner layout depending on the ContentLocation and other configurations set. From b48720789270bdcb5da8ecde83cb48cb3a9b3cc4 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:50:26 -0700 Subject: [PATCH 029/104] BLD: jinja2 is needed to run the tests now --- pyproject.toml | 2 ++ uv.lock | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 942f2da..f6b32cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,12 +66,14 @@ doc = [ "sphinxcontrib-jquery>=4.1", ] test = [ + "jinja2>=3", "pytest>=9.0.2", "pytest-qt>=4.5.0", "pytest-timeout>=2.4.0", "tomlkit>=0.14.0", ] dev = [ + "jinja2>=3", "tomlkit>=0.14.0", ] diff --git a/uv.lock b/uv.lock index 82daa84..08d363c 100644 --- a/uv.lock +++ b/uv.lock @@ -325,6 +325,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "jinja2" }, { name = "tomlkit" }, ] doc = [ @@ -334,6 +335,7 @@ doc = [ { name = "sphinxcontrib-jquery" }, ] test = [ + { name = "jinja2" }, { name = "pytest" }, { name = "pytest-qt" }, { name = "pytest-timeout" }, @@ -343,6 +345,8 @@ test = [ [package.metadata] requires-dist = [ { name = "docs-versions-menu", marker = "extra == 'doc'", specifier = ">=0.5.2" }, + { name = "jinja2", marker = "extra == 'dev'", specifier = ">=3" }, + { name = "jinja2", marker = "extra == 'test'", specifier = ">=3" }, { name = "pydm", specifier = ">=1.9.0" }, { name = "pyqt5", specifier = ">=5.15.11" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2" }, From 1cec0973ccde2c250ec43a8df7ef41a3ce673792 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 11:55:15 -0700 Subject: [PATCH 030/104] TST: add missing conda test requirements --- conda-recipe/meta.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 39d5cbf..625215d 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -31,9 +31,11 @@ test: imports: - pcdswidgets requires: + - jinja2 >=3 - pytest - pytest-qt - pytest-timeout + - tomlkit >=0.14.0 about: home: https://github.com/pcdshub/pcdswidgets From daf7b016ad6d214419639848cd951b44e2816d28 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 12 Mar 2026 15:07:38 -0700 Subject: [PATCH 031/104] TST: remove the toml test, it's extra and can't work with the installed package --- pcdswidgets/tests/test_entrypoint_widgets.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index 948c876..4581dae 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -2,20 +2,7 @@ from pydm.config import ENTRYPOINT_WIDGET -from pcdswidgets.entrypoint_widgets import get_current_widget_table, get_widget_entrypoint_data - - -def test_toml_has_all_widgets(): - """ - Ensure that all widgets are included in pyproject.toml, and in the generated order. - - If this fails, it's likely that we forgot to run the entrypoint_widgets generator program. - """ - name_and_entrypoint = get_widget_entrypoint_data() - current_table, _ = get_current_widget_table() - for (name, entrypoint), (key, value) in zip(name_and_entrypoint, current_table.items(), strict=True): - assert name == key - assert entrypoint == value +from pcdswidgets.entrypoint_widgets import get_widget_entrypoint_data def test_entrypoint_has_all_widgets(): From 77ffcc4a039de7100867cd995232771827bc6d54 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 17 Mar 2026 18:02:11 -0700 Subject: [PATCH 032/104] DOC/TST: add sizing/naming rules and make failing test with existing names --- README.md | 43 +++++++++++++ pcdswidgets/entrypoint_widgets.py | 25 +++++-- pcdswidgets/tests/test_entrypoint_widgets.py | 68 +++++++++++++++++++- 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dd0a99d..1af7bfd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,49 @@ pip install -e . ## Adding Widgets +### Widget Sizing +Device control widgets should fall into exactly one of three size classes. +Widgets can be smaller than the maximum of their size class by up to 10% before being flagged by CI. + +To ensure sizing consistency, set the minimum and maximum sizes to values that look good throughout the range +and are permissible sizes as recorded below. +It's recommended to used fixed sizing when possible because dynamic sizing is hard to do right. + +Widgets should always be maintained to work at the original designed size, because changing this can break existing screens. + +#### Full Size +- Width: 250px +- Height: 120px + +#### Compact Size +- Width: 75px +- Height: 75px + +#### Row Size +- Width: 680px +- Height: 40px + +#### Widgets that aren't control widgets (containers, etc.) +These should not have a maximum or a minimum size- they should be usable at any size. + +#### Widgets created before 2026 +These may have a variety of sizes because we had no standards, and will not be checked in CI. + + +### Widget Naming +Device control widgets should be named based on the type of device that they control. +The name should be specific enough to distinguish it from other widgets, but general enough to cover all devices that can be used. +Widgets are named using CamelCase and must end with the size, e.g. `MotorRecordFull` + +There is no need to end a widget name with "Widget". + +Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_record_full.ui` for `MotorRecordFull`, as well as the module that contains the widget which should be called `motor_record_full.py`. + +Widgets should never be renamed between tags, this will break existing screens. + +Widgets named before 2026 may break some of these rules because we don't want to rename them. + + ### Adding a Symbol-based Widget This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index 3a167a4..07162b1 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -14,6 +14,7 @@ import tomlkit as tk import tomlkit.items as tki +from qtpy.QtWidgets import QWidget SKIP_WIDGETS = [ "PCDSSymbolBase", @@ -34,14 +35,30 @@ def main(): def get_widget_entrypoint_data() -> list[tuple[str, str]]: key_val_set: set[tuple[str, str]] = set() + for name, WidgetCls in iter_all_widgets(): + key_val_set.add((name, f"{WidgetCls.__module__}:{name}")) + key_val = sorted(key_val_set) + return key_val + + +def iter_all_widgets() -> Iterator[tuple[str, type[QWidget]]]: + """ + Recursively yield all widgets to export from pcdswidgets. + + Yields + ------ + name, widget: str, QWidget + """ + seen: set[str] = set() for module in iter_submodules(): for name, obj in inspect.getmembers(module, inspect.isclass): if name in SKIP_WIDGETS: continue - if hasattr(obj, "_qt_designer_"): - key_val_set.add((name, f"{obj.__module__}:{name}")) - key_val = sorted(key_val_set) - return key_val + if name in seen: + continue + if issubclass(obj, QWidget) and hasattr(obj, "_qt_designer_"): + seen.add(name) + yield (name, obj) def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index 4581dae..2faac0e 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -1,8 +1,10 @@ from importlib.metadata import entry_points +import pytest from pydm.config import ENTRYPOINT_WIDGET +from qtpy.QtWidgets import QWidget -from pcdswidgets.entrypoint_widgets import get_widget_entrypoint_data +from pcdswidgets.entrypoint_widgets import get_widget_entrypoint_data, iter_all_widgets def test_entrypoint_has_all_widgets(): @@ -16,3 +18,67 @@ def test_entrypoint_has_all_widgets(): name_and_entrypoint = get_widget_entrypoint_data() for name, entrypoint in name_and_entrypoint: assert pydm_widgets.select(name=name)[name].value == entrypoint + + +# Don't check widgets from before we made sizing/naming standards +exempt_widgets = [ + "ApertureValve", + "CapacitanceManometerGauge", + "ColdCathodeComboGauge", + "ColdCathodeGauge", + "ControlOnlyValveNC", + "ControlOnlyValveNO", + "ControlValve", + "EPSByteIndicator", + "FastShutter", + "FilterSortWidgetTable", + "GetterPump", + "HotCathodeComboGauge", + "HotCathodeGauge", + "IonPump", + "NeedleValve", + "PneumaticValve", + "PneumaticValveDA", + "PneumaticValveNO", + "ProportionalValve", + "RGA", + "RightAngleManualValve", + "RoughGauge", + "ScrollPump", + "TurboPump", +] + + +@pytest.mark.parametrize( + "widget_name,WidgetCls", [elem for elem in iter_all_widgets() if elem[0] not in exempt_widgets] +) +def test_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): + """ + Ensure that all widgets are named and sized appropriately as per our standards. + """ + widget = WidgetCls() + qtbot.addWidget(widget) + + if widget_name.endswith("Full"): + max_w = 250 + max_h = 120 + min_w = 0.9 * max_w + min_h = 0.9 * max_h + elif widget_name.endswith("Compact"): + max_w = 75 + max_h = 75 + min_w = 0.9 * max_w + min_h = 0.9 * max_h + elif widget_name.endswith("Row"): + max_w = 680 + max_h = 40 + min_w = 0.9 * max_w + min_h = 0.9 * max_h + else: + raise ValueError( + f"Widget named {widget_name} does not follow naming convention: " + "must end with Full, Compact, or Row to signal size class." + ) + + assert min_w <= widget.minimumWidth() <= widget.maximumWidth() <= max_w + assert min_h <= widget.minimumHeight() <= widget.maximumHeight() <= max_h From 90101aca3c7ea0f25c0e8e2d5b65c45d0a58ee5b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 17 Mar 2026 18:02:38 -0700 Subject: [PATCH 033/104] MNT: add vscode settings folder to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 25ef066..c6ba6a7 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,6 @@ venv.bak/ # pyuic5 **/ui/*.py !**/ui/__init__.py + +# vscode +.vscode/* From ef3a6b390acdc032b6f42ff455e932fe034af7eb Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 14:30:57 -0700 Subject: [PATCH 034/104] MNT/DOC: align first set of composite widget names with convention --- README.md | 6 +++--- pcdswidgets/builder/build.py | 2 +- .../builder/ui/{positioner.ui => motor_record_full.ui} | 0 .../builder/ui/{positioner_row.ui => motor_record_row.ui} | 0 ...w_tc_interlock.ui => motor_record_tc_interlock_row.ui} | 6 +++--- .../{smaract_open_loop.ui => smaract_open_loop_full.ui} | 0 .../motion/{positioner_widget.py => motor_record_full.py} | 4 ++-- .../{positioner_row_widget.py => motor_record_row.py} | 4 ++-- ...terlock_widget.py => motor_record_tc_interlock_row.py} | 4 ++-- ...ract_open_loop_widget.py => smaract_open_loop_full.py} | 4 ++-- pyproject.toml | 8 ++++---- 11 files changed, 19 insertions(+), 19 deletions(-) rename pcdswidgets/builder/ui/{positioner.ui => motor_record_full.ui} (100%) rename pcdswidgets/builder/ui/{positioner_row.ui => motor_record_row.ui} (100%) rename pcdswidgets/builder/ui/{positioner_row_tc_interlock.ui => motor_record_tc_interlock_row.ui} (97%) rename pcdswidgets/builder/ui/{smaract_open_loop.ui => smaract_open_loop_full.ui} (100%) rename pcdswidgets/motion/{positioner_widget.py => motor_record_full.py} (60%) rename pcdswidgets/motion/{positioner_row_widget.py => motor_record_row.py} (57%) rename pcdswidgets/motion/{positioner_row_tc_interlock_widget.py => motor_record_tc_interlock_row.py} (50%) rename pcdswidgets/motion/{smaract_open_loop_widget.py => smaract_open_loop_full.py} (56%) diff --git a/README.md b/README.md index 1af7bfd..1817f26 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,12 @@ If the widget has been added and is included in the pyproject.toml file, it will #### Widget Classes The widget class looks something like: ``` -from pcdswidgets.builder.ui.my_widget_base import MyWidgetBase +from pcdswidgets.builder.ui.some_name_base import SomeNameBase -class MyWidget(MyWidgetBase): +class SomeName(SomeNameBase): _qt_designer_ = { - "group": "My Category", + "group": "Some Category", "is_container": False, } ``` diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index fb9bb8c..9c2425e 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -39,7 +39,7 @@ def build_base_widget(designer_ui: str): # Bring the info into a good form for the jinja template ui_name = os.path.basename(designer_ui) - base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "WidgetBase" + base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "Base" info_for_jinja = process_widget_macros(ui_info) macro_names = sorted(info_for_jinja.macro_set) diff --git a/pcdswidgets/builder/ui/positioner.ui b/pcdswidgets/builder/ui/motor_record_full.ui similarity index 100% rename from pcdswidgets/builder/ui/positioner.ui rename to pcdswidgets/builder/ui/motor_record_full.ui diff --git a/pcdswidgets/builder/ui/positioner_row.ui b/pcdswidgets/builder/ui/motor_record_row.ui similarity index 100% rename from pcdswidgets/builder/ui/positioner_row.ui rename to pcdswidgets/builder/ui/motor_record_row.ui diff --git a/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui b/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui similarity index 97% rename from pcdswidgets/builder/ui/positioner_row_tc_interlock.ui rename to pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui index 296e499..79374d0 100644 --- a/pcdswidgets/builder/ui/positioner_row_tc_interlock.ui +++ b/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui @@ -66,7 +66,7 @@ 5
- + @@ -251,9 +251,9 @@
pydm.widgets.byte
- PositionerRowWidget + MotorRecordRow QWidget -
pcdswidgets.motion.positioner_row_widget
+
pcdswidgets.motion.motor_record_row
diff --git a/pcdswidgets/builder/ui/smaract_open_loop.ui b/pcdswidgets/builder/ui/smaract_open_loop_full.ui similarity index 100% rename from pcdswidgets/builder/ui/smaract_open_loop.ui rename to pcdswidgets/builder/ui/smaract_open_loop_full.ui diff --git a/pcdswidgets/motion/positioner_widget.py b/pcdswidgets/motion/motor_record_full.py similarity index 60% rename from pcdswidgets/motion/positioner_widget.py rename to pcdswidgets/motion/motor_record_full.py index 5b2578b..3cdf2dd 100644 --- a/pcdswidgets/motion/positioner_widget.py +++ b/pcdswidgets/motion/motor_record_full.py @@ -1,9 +1,9 @@ from pydm.widgets.qtplugins import ifont -from pcdswidgets.builder.ui.positioner_base import PositionerWidgetBase +from pcdswidgets.builder.ui.motor_record_full_base import MotorRecordFullBase -class PositionerWidget(PositionerWidgetBase): +class MotorRecordFull(MotorRecordFullBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pcdswidgets/motion/positioner_row_widget.py b/pcdswidgets/motion/motor_record_row.py similarity index 57% rename from pcdswidgets/motion/positioner_row_widget.py rename to pcdswidgets/motion/motor_record_row.py index 3fa4f68..1ee60bc 100644 --- a/pcdswidgets/motion/positioner_row_widget.py +++ b/pcdswidgets/motion/motor_record_row.py @@ -1,9 +1,9 @@ from pydm.widgets.qtplugins import ifont -from pcdswidgets.builder.ui.positioner_row_base import PositionerRowWidgetBase +from pcdswidgets.builder.ui.motor_record_row_base import MotorRecordRowBase -class PositionerRowWidget(PositionerRowWidgetBase): +class MotorRecordRow(MotorRecordRowBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py b/pcdswidgets/motion/motor_record_tc_interlock_row.py similarity index 50% rename from pcdswidgets/motion/positioner_row_tc_interlock_widget.py rename to pcdswidgets/motion/motor_record_tc_interlock_row.py index a0406fb..6b8330b 100644 --- a/pcdswidgets/motion/positioner_row_tc_interlock_widget.py +++ b/pcdswidgets/motion/motor_record_tc_interlock_row.py @@ -1,9 +1,9 @@ from pydm.widgets.qtplugins import ifont -from pcdswidgets.builder.ui.positioner_row_tc_interlock_base import PositionerRowTcInterlockWidgetBase +from pcdswidgets.builder.ui.motor_record_tc_interlock_row_base import MotorRecordTcInterlockRowBase -class PositionerRowTcInterlockWidget(PositionerRowTcInterlockWidgetBase): +class MotorRecordTcInterlockRow(MotorRecordTcInterlockRowBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pcdswidgets/motion/smaract_open_loop_widget.py b/pcdswidgets/motion/smaract_open_loop_full.py similarity index 56% rename from pcdswidgets/motion/smaract_open_loop_widget.py rename to pcdswidgets/motion/smaract_open_loop_full.py index d109666..8c789b9 100644 --- a/pcdswidgets/motion/smaract_open_loop_widget.py +++ b/pcdswidgets/motion/smaract_open_loop_full.py @@ -1,9 +1,9 @@ from pydm.widgets.qtplugins import ifont -from pcdswidgets.builder.ui.smaract_open_loop_base import SmaractOpenLoopWidgetBase +from pcdswidgets.builder.ui.smaract_open_loop_full_base import SmaractOpenLoopFullBase -class SmaractOpenLoopWidget(SmaractOpenLoopWidgetBase): +class SmaractOpenLoopFull(SmaractOpenLoopFullBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, diff --git a/pyproject.toml b/pyproject.toml index f6b32cd..fc5d44f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,19 +43,19 @@ GetterPump = "pcdswidgets.vacuum.pumps:GetterPump" HotCathodeComboGauge = "pcdswidgets.vacuum.gauges:HotCathodeComboGauge" HotCathodeGauge = "pcdswidgets.vacuum.gauges:HotCathodeGauge" IonPump = "pcdswidgets.vacuum.pumps:IonPump" +MotorRecordFull = "pcdswidgets.motion.motor_record_full:MotorRecordFull" +MotorRecordRow = "pcdswidgets.motion.motor_record_row:MotorRecordRow" +MotorRecordTcInterlockRow = "pcdswidgets.motion.motor_record_tc_interlock_row:MotorRecordTcInterlockRow" NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" PneumaticValveNO = "pcdswidgets.vacuum.valves:PneumaticValveNO" -PositionerRowTcInterlockWidget = "pcdswidgets.motion.positioner_row_tc_interlock_widget:PositionerRowTcInterlockWidget" -PositionerRowWidget = "pcdswidgets.motion.positioner_row_widget:PositionerRowWidget" -PositionerWidget = "pcdswidgets.motion.positioner_widget:PositionerWidget" ProportionalValve = "pcdswidgets.vacuum.valves:ProportionalValve" RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" -SmaractOpenLoopWidget = "pcdswidgets.motion.smaract_open_loop_widget:SmaractOpenLoopWidget" +SmaractOpenLoopFull = "pcdswidgets.motion.smaract_open_loop_full:SmaractOpenLoopFull" TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] From 1e6bba1458c716c08dec4578ff3e6b90bb8162ef Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 14:54:53 -0700 Subject: [PATCH 035/104] FIX: avoid circular imports by not importing from pydm.widgets.qtplugins --- README.md | 15 +++++++++++++++ pcdswidgets/builder/designer_widget.py | 9 +++++++++ pcdswidgets/motion/motor_record_full.py | 4 +--- pcdswidgets/motion/motor_record_row.py | 4 +--- .../motion/motor_record_tc_interlock_row.py | 4 +--- pcdswidgets/motion/smaract_open_loop_full.py | 4 +--- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1817f26..9239842 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,21 @@ class SomeName(SomeNameBase): If you like, you can extend these classes to add additional python code to use at runtime. +#### Icons +If you want to set a non-default icon for the designer widget list, you can include a QIcon or a string +in the "icon" key of the `_qt_designer_` variable: +``` + _qt_designer_ = { + "group": "Some Category", + "is_container": False, + "icon": "expand-arrows-alt", + } +``` + +If this is a string, we'll convert it to a QIcon using Pydm's IconFont. +This uses a portable version of fontawesome, try running `qta-browser` +and look through everything with the `fa` prefix to browse options. + #### Limitations - Widgets that contain PyDMEmbeddedWidget are not supported: bootstrap these by turning the contents into widgets themselves. diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index 0385519..4e06edc 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -5,10 +5,13 @@ from string import Template from typing import Any, ClassVar, Protocol +from pydm.utilities.iconfont import IconFont from pydm.widgets.base import PyDMPrimitiveWidget from pydm.widgets.qtplugin_extensions import RulesExtension from qtpy.QtWidgets import QAction, QDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget +ifont = IconFont() + class _UiForm(Protocol): def setupUi(self, Form): ... @@ -43,6 +46,12 @@ def __init_subclass__(cls): cls._qt_designer_["extensions"] = new_ext except AttributeError: ... + # Interpret strings as icons so we don't have to import IconFont everywhere + try: + if isinstance(cls._qt_designer_["icon"], str): + cls._qt_designer_["icon"] = ifont.icon(cls._qt_designer_["icon"]) + except (AttributeError, KeyError): + ... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pcdswidgets/motion/motor_record_full.py b/pcdswidgets/motion/motor_record_full.py index 3cdf2dd..646e8e3 100644 --- a/pcdswidgets/motion/motor_record_full.py +++ b/pcdswidgets/motion/motor_record_full.py @@ -1,5 +1,3 @@ -from pydm.widgets.qtplugins import ifont - from pcdswidgets.builder.ui.motor_record_full_base import MotorRecordFullBase @@ -7,5 +5,5 @@ class MotorRecordFull(MotorRecordFullBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, - "icon": ifont.icon("expand-arrows-alt"), + "icon": "expand-arrows-alt", } diff --git a/pcdswidgets/motion/motor_record_row.py b/pcdswidgets/motion/motor_record_row.py index 1ee60bc..24719f8 100644 --- a/pcdswidgets/motion/motor_record_row.py +++ b/pcdswidgets/motion/motor_record_row.py @@ -1,5 +1,3 @@ -from pydm.widgets.qtplugins import ifont - from pcdswidgets.builder.ui.motor_record_row_base import MotorRecordRowBase @@ -7,5 +5,5 @@ class MotorRecordRow(MotorRecordRowBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, - "icon": ifont.icon("arrows-alt-h"), + "icon": "arrows-alt-h", } diff --git a/pcdswidgets/motion/motor_record_tc_interlock_row.py b/pcdswidgets/motion/motor_record_tc_interlock_row.py index 6b8330b..5cc067a 100644 --- a/pcdswidgets/motion/motor_record_tc_interlock_row.py +++ b/pcdswidgets/motion/motor_record_tc_interlock_row.py @@ -1,5 +1,3 @@ -from pydm.widgets.qtplugins import ifont - from pcdswidgets.builder.ui.motor_record_tc_interlock_row_base import MotorRecordTcInterlockRowBase @@ -7,5 +5,5 @@ class MotorRecordTcInterlockRow(MotorRecordTcInterlockRowBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, - "icon": ifont.icon("arrows-alt-h"), + "icon": "arrows-alt-h", } diff --git a/pcdswidgets/motion/smaract_open_loop_full.py b/pcdswidgets/motion/smaract_open_loop_full.py index 8c789b9..b1b6fc5 100644 --- a/pcdswidgets/motion/smaract_open_loop_full.py +++ b/pcdswidgets/motion/smaract_open_loop_full.py @@ -1,5 +1,3 @@ -from pydm.widgets.qtplugins import ifont - from pcdswidgets.builder.ui.smaract_open_loop_full_base import SmaractOpenLoopFullBase @@ -7,5 +5,5 @@ class SmaractOpenLoopFull(SmaractOpenLoopFullBase): _qt_designer_ = { "group": "PCDS Motion", "is_container": False, - "icon": ifont.icon("arrows-alt-h"), + "icon": "arrows-alt-h", } From d765416d64ff6589375b749fe755a6e72d45e19d Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 14:59:53 -0700 Subject: [PATCH 036/104] TST: make it more clear why this test has failed --- pcdswidgets/tests/test_entrypoint_widgets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index 2faac0e..7dc4d0d 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -80,5 +80,7 @@ def test_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): "must end with Full, Compact, or Row to signal size class." ) - assert min_w <= widget.minimumWidth() <= widget.maximumWidth() <= max_w - assert min_h <= widget.minimumHeight() <= widget.maximumHeight() <= max_h + assert widget.minimumWidth() >= min_w, f"{widget_name}'s minimum width is too small." + assert widget.maximumWidth() <= max_w, f"{widget_name}'s maximum width is too large." + assert widget.minimumHeight() >= min_h, f"{widget_name}'s minimum height is too small." + assert widget.maximumHeight() <= max_h, f"{widget_name}'s maximum height is too large." From fd6e880b15be14eb2cc3b7cadbb44b84300230d6 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 15:26:08 -0700 Subject: [PATCH 037/104] MNT: settle on size constraints --- README.md | 14 +- pcdswidgets/builder/ui/motor_record_full.ui | 76 ++++---- pcdswidgets/builder/ui/motor_record_row.ui | 184 +++++++++--------- .../ui/motor_record_tc_interlock_row.ui | 4 +- ..._loop_full.ui => smaract_open_loop_row.ui} | 122 ++++++------ pcdswidgets/motion/smaract_open_loop_full.py | 9 - pcdswidgets/motion/smaract_open_loop_row.py | 9 + pcdswidgets/tests/test_entrypoint_widgets.py | 24 ++- pyproject.toml | 2 +- 9 files changed, 225 insertions(+), 219 deletions(-) rename pcdswidgets/builder/ui/{smaract_open_loop_full.ui => smaract_open_loop_row.ui} (75%) delete mode 100644 pcdswidgets/motion/smaract_open_loop_full.py create mode 100644 pcdswidgets/motion/smaract_open_loop_row.py diff --git a/README.md b/README.md index 9239842..cfda858 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ pip install -e . ## Adding Widgets ### Widget Sizing Device control widgets should fall into exactly one of three size classes. -Widgets can be smaller than the maximum of their size class by up to 10% before being flagged by CI. +Widgets can be smaller than the maximum of their size class by up to 20% before being flagged by CI. To ensure sizing consistency, set the minimum and maximum sizes to values that look good throughout the range and are permissible sizes as recorded below. @@ -24,16 +24,18 @@ It's recommended to used fixed sizing when possible because dynamic sizing is ha Widgets should always be maintained to work at the original designed size, because changing this can break existing screens. #### Full Size -- Width: 250px -- Height: 120px +- Width: 400px +- Height: 125px #### Compact Size -- Width: 75px +- Width: 100px - Height: 75px #### Row Size -- Width: 680px -- Height: 40px +- Width: 800px +- Height: 50px + +Rows are also allowed to be double-height, e.g. 100px height. #### Widgets that aren't control widgets (containers, etc.) These should not have a maximum or a minimum size- they should be usable at any size. diff --git a/pcdswidgets/builder/ui/motor_record_full.ui b/pcdswidgets/builder/ui/motor_record_full.ui index aec9dee..74b3e85 100644 --- a/pcdswidgets/builder/ui/motor_record_full.ui +++ b/pcdswidgets/builder/ui/motor_record_full.ui @@ -24,8 +24,8 @@
- 650 - 200 + 400 + 125 @@ -112,13 +112,13 @@ - + false - + ca://${MOTOR}.DESC - + PyDMLabel::String @@ -159,16 +159,16 @@ - + false - + ca://${MOTOR}.MOVN - + false - + true @@ -200,7 +200,7 @@ - + false @@ -238,10 +238,10 @@ Qt::AlignCenter - + ca://${MOTOR}.RBV - + PyDMLabel::Decimal @@ -257,7 +257,7 @@ - + false @@ -295,10 +295,10 @@ Stop - + ca://${MOTOR}.STOP - + 1 @@ -350,19 +350,19 @@ Qt::AlignCenter - + 4 - + false - + false - + ca://${MOTOR}.VAL - + PyDMLineEdit::Decimal @@ -408,25 +408,25 @@ - + 0 - + false - + true - + false - + false - + - + ca://${MOTOR}.EGU @@ -473,13 +473,13 @@ << - + ca://${MOTOR}.TWR - + 1 - + true @@ -515,16 +515,16 @@ Qt::AlignCenter - + 4 - + false - + false - + ca://${MOTOR}.TWV @@ -555,13 +555,13 @@ >> - + ca://${MOTOR}.TWF - + 1 - + true @@ -593,7 +593,7 @@ - + motor-expert-screen ${MOTOR} diff --git a/pcdswidgets/builder/ui/motor_record_row.ui b/pcdswidgets/builder/ui/motor_record_row.ui index 070217e..c9594de 100644 --- a/pcdswidgets/builder/ui/motor_record_row.ui +++ b/pcdswidgets/builder/ui/motor_record_row.ui @@ -7,7 +7,7 @@ 0 0 753 - 55 + 45 @@ -24,8 +24,8 @@ - 1000 - 55 + 800 + 50 @@ -90,43 +90,43 @@ >> - + false - + false - + - + ca://${MOTOR}.TWF - + false - + - + - + false - + Are you sure you want to proceed? - + 1 - + None - + false - + false @@ -148,38 +148,38 @@ - + false - + true - + - + ca://${MOTOR}.HLS - + 255 165 0 - + false - + false - + false - + 1 - + Bit 0 @@ -216,13 +216,13 @@ - + false - + ca://${MOTOR}.DESC - + PyDMLabel::String @@ -251,22 +251,22 @@ - + 0 - + false - + true - + false - + false - + ca://${MOTOR}.EGU @@ -276,10 +276,10 @@ - + false - + motor-expert-screen ${MOTOR} @@ -308,22 +308,22 @@ - + 3 - + false - + false - + false - + false - + ca://${MOTOR}.TWV @@ -345,41 +345,41 @@ - + false - + true - + - + ca://${MOTOR}.LLS - + 255 165 0 - + false - + false - + false - + 1 - + 0 - + Bit 0 @@ -413,25 +413,25 @@ Qt::AlignCenter - + 3 - + false - + false - + false - + true - + ca://${MOTOR}.RBV - + PyDMLabel::Decimal @@ -462,31 +462,31 @@ - + false - + true - + ca://${MOTOR}.MOVN - + false - + false - + true - + 1 - + 0 - + Bit 0 @@ -507,43 +507,43 @@ << - + false - + false - + - + ca://${MOTOR}.TWR - + false - + - + - + false - + Are you sure you want to proceed? - + 1 - + None - + false - + false @@ -571,10 +571,10 @@ Stop - + ca://${MOTOR}.STOP - + 1 @@ -604,25 +604,25 @@ - + 3 - + false - + false - + false - + false - + ca://${MOTOR}.VAL - + PyDMLineEdit::Default diff --git a/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui b/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui index 79374d0..3eb2194 100644 --- a/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui +++ b/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui @@ -18,7 +18,7 @@ - 825 + 800 100 @@ -66,7 +66,7 @@ 5 - + diff --git a/pcdswidgets/builder/ui/smaract_open_loop_full.ui b/pcdswidgets/builder/ui/smaract_open_loop_row.ui similarity index 75% rename from pcdswidgets/builder/ui/smaract_open_loop_full.ui rename to pcdswidgets/builder/ui/smaract_open_loop_row.ui index eae3206..5bcbe18 100644 --- a/pcdswidgets/builder/ui/smaract_open_loop_full.ui +++ b/pcdswidgets/builder/ui/smaract_open_loop_row.ui @@ -66,19 +66,19 @@ - + true - + false - + false - + - + edm -eolc -x -m MOTOR=${MOTOR} /reg/g/pcds/epics/ioc/common/smaract/R1.0.8/motorScreens/mcs2_openloop.edl @@ -109,25 +109,25 @@ - + 0 - + false - + true - + false - + true - + ca://${MOTOR}.DESC - + PyDMLabel::String @@ -166,25 +166,25 @@ 12 - + 0 - + false - + true - + false - + true - + ca://${MOTOR}:TOTAL_STEP_COUNT - + PyDMLineEdit::Decimal @@ -203,40 +203,40 @@ >> - + false - + false - + ca://${MOTOR}:STEP_FORWARD.PROC - + false - + - + - + false - + Are you sure you want to proceed? - + 1 - + None - + true - + false @@ -263,22 +263,22 @@ - + 0 - + false - + true - + false - + false - + ca://${MOTOR}:STEP_COUNT @@ -306,40 +306,40 @@ Stop - + false - + false - + ca://${MOTOR}.STOP - + false - + - + - + false - + Are you sure you want to proceed? - + 1 - + None - + false - + false @@ -358,40 +358,40 @@ << - + false - + false - + ca://${MOTOR}:STEP_REVERSE.PROC - + false - + - + - + false - + Are you sure you want to proceed? - + 1 - + None - + true - + false diff --git a/pcdswidgets/motion/smaract_open_loop_full.py b/pcdswidgets/motion/smaract_open_loop_full.py deleted file mode 100644 index b1b6fc5..0000000 --- a/pcdswidgets/motion/smaract_open_loop_full.py +++ /dev/null @@ -1,9 +0,0 @@ -from pcdswidgets.builder.ui.smaract_open_loop_full_base import SmaractOpenLoopFullBase - - -class SmaractOpenLoopFull(SmaractOpenLoopFullBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - "icon": "arrows-alt-h", - } diff --git a/pcdswidgets/motion/smaract_open_loop_row.py b/pcdswidgets/motion/smaract_open_loop_row.py new file mode 100644 index 0000000..15f4684 --- /dev/null +++ b/pcdswidgets/motion/smaract_open_loop_row.py @@ -0,0 +1,9 @@ +from pcdswidgets.builder.ui.smaract_open_loop_row_base import SmaractOpenLoopRowBase + + +class SmaractOpenLoopRow(SmaractOpenLoopRowBase): + _qt_designer_ = { + "group": "PCDS Motion", + "is_container": False, + "icon": "arrows-alt-h", + } diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index 7dc4d0d..cb1cf5e 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -58,22 +58,26 @@ def test_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): """ widget = WidgetCls() qtbot.addWidget(widget) + # 20% smaller is OK + ratio = 0.8 if widget_name.endswith("Full"): - max_w = 250 + max_w = 400 max_h = 120 - min_w = 0.9 * max_w - min_h = 0.9 * max_h + min_w = ratio * max_w + min_h = ratio * max_h elif widget_name.endswith("Compact"): - max_w = 75 + max_w = 100 max_h = 75 - min_w = 0.9 * max_w - min_h = 0.9 * max_h + min_w = ratio * max_w + min_h = ratio * max_h elif widget_name.endswith("Row"): - max_w = 680 - max_h = 40 - min_w = 0.9 * max_w - min_h = 0.9 * max_h + max_w = 800 + max_h = 50 + min_w = ratio * max_w + min_h = ratio * max_h + # Allow double rows + max_h = max_h * 2 else: raise ValueError( f"Widget named {widget_name} does not follow naming convention: " diff --git a/pyproject.toml b/pyproject.toml index fc5d44f..0050338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" -SmaractOpenLoopFull = "pcdswidgets.motion.smaract_open_loop_full:SmaractOpenLoopFull" +SmaractOpenLoopRow = "pcdswidgets.motion.smaract_open_loop_row:SmaractOpenLoopRow" TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] From 208e56dd7a824ebb71eb37b9ae7abb273b43464b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 15:45:43 -0700 Subject: [PATCH 038/104] FIX: make smaract row conform to conventions --- pcdswidgets/builder/ui/smaract_open_loop_row.ui | 8 ++++---- pcdswidgets/tests/test_entrypoint_widgets.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pcdswidgets/builder/ui/smaract_open_loop_row.ui b/pcdswidgets/builder/ui/smaract_open_loop_row.ui index 5bcbe18..18e697e 100644 --- a/pcdswidgets/builder/ui/smaract_open_loop_row.ui +++ b/pcdswidgets/builder/ui/smaract_open_loop_row.ui @@ -6,8 +6,8 @@ 0 0 - 609 - 36 + 640 + 40 @@ -18,8 +18,8 @@ - 550 - 33 + 640 + 40 diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index cb1cf5e..e1c9ce9 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -63,7 +63,7 @@ def test_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): if widget_name.endswith("Full"): max_w = 400 - max_h = 120 + max_h = 125 min_w = ratio * max_w min_h = ratio * max_h elif widget_name.endswith("Compact"): From a08f8033b07a10dd96a7a1c19fcb0bfed17844c9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 15:54:49 -0700 Subject: [PATCH 039/104] FIX: parent the macro editor so it appears in a good location on screen --- pcdswidgets/builder/designer_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index 4e06edc..0a7c893 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -109,7 +109,7 @@ def actions(self) -> list[QAction]: return [self.edit_macros_action] def open_dialog(self): - dialog = MacroValueEditor(self.widget, parent=None) + dialog = MacroValueEditor(self.widget, parent=self.widget) dialog.exec_() From 53510894cf7adc070b4c01dfc5728b99394b2497 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 16:45:12 -0700 Subject: [PATCH 040/104] MNT: correct type annotation --- pcdswidgets/builder/designer_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index 0a7c893..89f9825 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -25,7 +25,7 @@ class DesignerWidget(QWidget, PyDMPrimitiveWidget): # type: ignore # Loaded from uic ui_form: ClassVar[type[_UiForm]] # Tells PyDM to include in designer - _qt_designer_: dict[str, Any] + _qt_designer_: ClassVar[dict[str, Any]] # Macro name to widget names that include that macro _macro_to_widget: ClassVar[dict[str, list[str]]] # Widget name to required macros: all must be non-empty before updating From 4573b5d02761d195255bd32253ee1dc229822ade Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 16:55:54 -0700 Subject: [PATCH 041/104] FIX: make the dialog's changes actually save properly --- pcdswidgets/builder/designer_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index 89f9825..f6298d8 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -7,6 +7,7 @@ from pydm.utilities.iconfont import IconFont from pydm.widgets.base import PyDMPrimitiveWidget +from pydm.widgets.designer_settings import update_property_for_widget from pydm.widgets.qtplugin_extensions import RulesExtension from qtpy.QtWidgets import QAction, QDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget @@ -158,7 +159,7 @@ def setup_ui(self): def save_changes(self): for macro_name, widget in self.edit_widgets.items(): - self.widget._set_macro(macro_name, widget.text()) + update_property_for_widget(self.widget, macro_name.lower(), widget.text()) if self.sender() == self.save_button: self.accept() From 934f56d414096b187686bb117794a7fa65544734 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 17:58:37 -0700 Subject: [PATCH 042/104] MNT/STY: update pre-commit versions and re-run --- .pre-commit-config.yaml | 4 ++-- pcdswidgets/vacuum/base.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e71a018..7506849 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ exclude: | repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch - id: trailing-whitespace @@ -23,7 +23,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.15.6 hooks: - id: ruff # run the linter args: [ --fix ] # and the safe fixes diff --git a/pcdswidgets/vacuum/base.py b/pcdswidgets/vacuum/base.py index b88f4b1..61c0054 100644 --- a/pcdswidgets/vacuum/base.py +++ b/pcdswidgets/vacuum/base.py @@ -608,7 +608,7 @@ def setup_icon(self): def _handle_icon_click(self): if not self.channelsPrefix: logger.error( - "No channel prefix specified." "Cannot proceed with opening expert screen for %s.", + "No channel prefix specified.Cannot proceed with opening expert screen for %s.", self.__class__.__name__, ) return From d859a70eeefd852641723d6b49c22926835cfdd5 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 17:59:45 -0700 Subject: [PATCH 043/104] BLD: we probably don't need the ui source in the distribution --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index cf1ec58..6d30b08 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,3 @@ -include **/*.ui - include AUTHORS.rst include CONTRIBUTING.rst include LICENSE.md From cdf7c702aaa25cfff4987ce007930ddcc467c0bd Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 18:00:16 -0700 Subject: [PATCH 044/104] DOC: fix out of date example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfda858..689058e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ The steps are: 4. make - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. 5. Create a widget class - - Look around for examples, e.g. pcdswidgets/motion/positioner_widget.py + - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - Keeping these in separate files can avoid circular import errors and lets us include widgets inside widgets - Import from the _base module created from your .ui file and subclass 6. make, again From 09953d1d0ddf3bc062d54c25259c5a09d92c6f3c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 18:01:08 -0700 Subject: [PATCH 045/104] DOC: clarify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 689058e..8341019 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,4 @@ and look through everything with the `fa` prefix to browse options. #### Limitations - Widgets that contain PyDMEmbeddedWidget are not supported: bootstrap these by turning the contents into widgets themselves. -- The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the widget class in python, giving your widgets more unique names will help give more useful type hints, automatically. +- The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. From ca6a6342d5d78ad31eb36f0382c0371e060a97ef Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 18 Mar 2026 18:01:41 -0700 Subject: [PATCH 046/104] DOC: nitpick tense, phrasing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8341019..2864660 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ The steps are: - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. 5. Create a widget class - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - - Keeping these in separate files can avoid circular import errors and lets us include widgets inside widgets + - Keep these in separate files to avoid circular import errors from including widgets inside widgets - Import from the _base module created from your .ui file and subclass 6. make, again - This will include your widget in pyproject.toml From 6574b480978dcaaa745a98674ab5b867220c8cc6 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 20 Mar 2026 10:18:51 -0700 Subject: [PATCH 047/104] MNT: remove flake8 settings clutter, we are using ruff now --- .flake8 | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 957b1a9..0000000 --- a/.flake8 +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -exclude = .git,__pycache__,build,dist,pcdswidgets/_version.py -max-line-length = 88 -select = C,E,F,W,B,B950 -extend-ignore = E203, E501, E226, W503, W504 - -# Explanation section: -# B950 -# This takes into account max-line-length but only triggers when the value -# has been exceeded by more than 10% (96 characters). -# E203: Whitespace before ':' -# This is recommended by black in relation to slice formatting. -# E501: Line too long (82 > 79 characters) -# Our line length limit is 88 (above 79 defined in E501). Ignore it. -# E226: Missing whitespace around arithmetic operator -# This is a stylistic choice which you'll find everywhere in pcdsdevices, for -# example. Formulas can be easier to read when operators and operands -# have no whitespace between them. -# -# W503: Line break occurred before a binary operator -# W504: Line break occurred after a binary operator -# flake8 wants us to choose one of the above two. Our choice -# is to make no decision. From 0b3780dfa2bfc359db43fed0162506ffabab16d1 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 20 Mar 2026 10:19:43 -0700 Subject: [PATCH 048/104] MNT: update copyright year --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index e14da2c..88bab14 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2023, The Board of Trustees of the Leland Stanford Junior +Copyright (c) 2026, The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. Redistribution and use in source and binary forms, with or without From 17b40f118ba73f43417d6595402d6eec7fb98e0c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 20 Mar 2026 10:20:00 -0700 Subject: [PATCH 049/104] FIX: motor record fields should get precision from PV --- pcdswidgets/builder/ui/motor_record_full.ui | 8 ++++---- pcdswidgets/builder/ui/motor_record_row.ui | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pcdswidgets/builder/ui/motor_record_full.ui b/pcdswidgets/builder/ui/motor_record_full.ui index 74b3e85..78bd6c3 100644 --- a/pcdswidgets/builder/ui/motor_record_full.ui +++ b/pcdswidgets/builder/ui/motor_record_full.ui @@ -351,10 +351,10 @@ Qt::AlignCenter - 4 + 0 - false + true false @@ -516,10 +516,10 @@ Qt::AlignCenter - 4 + 0 - false + true false diff --git a/pcdswidgets/builder/ui/motor_record_row.ui b/pcdswidgets/builder/ui/motor_record_row.ui index c9594de..67eb05e 100644 --- a/pcdswidgets/builder/ui/motor_record_row.ui +++ b/pcdswidgets/builder/ui/motor_record_row.ui @@ -309,13 +309,13 @@ - 3 + 0 false - false + true false @@ -414,13 +414,13 @@ Qt::AlignCenter - 3 + 0 false - false + true false @@ -605,13 +605,13 @@ - 3 + 0 false - false + true false From 4d6774615f6715f89ae5467df3afde6535cd269b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 24 Mar 2026 18:22:22 -0700 Subject: [PATCH 050/104] ENH: make it much easier to set up a proper python environment for interactive testing --- .github/workflows/standard.yml | 2 +- Makefile | 5 +++- base_env_vars.sh | 12 ++++++++ build_local_venv.sh | 47 +++++++++++++++++++++++++++++ pyproject.toml | 9 +++--- try_in_designer.sh | 17 +++++++++++ try_in_pydm.sh | 14 +++++++++ uv.lock | 54 +--------------------------------- 8 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 base_env_vars.sh create mode 100755 build_local_venv.sh create mode 100755 try_in_designer.sh create mode 100755 try_in_pydm.sh diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index e17c168..bf16ec3 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -20,6 +20,6 @@ jobs: # Extras to be installed only for conda-based testing: conda-testing-extras: "" # Extras to be installed only for pip-based testing: - pip-testing-extras: "" + pip-testing-extras: "PyQt5==5.15" # Set if using setuptools-scm for the conda-build workflow use-setuptools-scm: true diff --git a/Makefile b/Makefile index 9cd7bdb..bf75b87 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build clean +.PHONY: all build clean venv UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) @@ -24,3 +24,6 @@ $(PY_BASE): $(UI_SOURCE) $(PY_SOURCE) $(JINJA_SOURCE) pyproject.toml: $(PY_SOURCE) python -m pcdswidgets.entrypoint_widgets + +venv: + ./build_local_venv.sh diff --git a/base_env_vars.sh b/base_env_vars.sh new file mode 100644 index 0000000..318cdf5 --- /dev/null +++ b/base_env_vars.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Local environment settings for building/testing at LCLS +# + +# Disable GL because it crashes on our servers +QT_XCB_GL_INTEGRATION=none +# Pick a base env that has designer configured appropriately and built to match python + pyqt versions +BASE_ENV="${PYPS_SITE_TOP}/conda/dev/zlentz/miniforge3/envs/ecs-base-0.0.2" + +export QT_XCB_GL_INTEGRATION +export BASE_ENV diff --git a/build_local_venv.sh b/build_local_venv.sh new file mode 100755 index 0000000..24bab92 --- /dev/null +++ b/build_local_venv.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# shellcheck disable=SC1091 +# +# This is the script we run on "make venv" +# +# Builds a .venv with a local install of pcdswidgets and working designer plugin. +# This can be re-run to update the pcdswidgets install, e.g. if you added a new widget. +# If you change the base environment, you'll have to remove and rebuild the .venv +# +set -e + +THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +cd "${THIS_DIR}" + +source base_env_vars.sh +PYTHON_EXE="${BASE_ENV}/bin/python" + +# Two paths: can specify uv or venv as an input arg +# Or, I will default to uv if it's on your path, venv otherwise +if [[ "$1" == "uv" ]]; then + MODE="uv" +elif [[ "$1" == "venv" ]]; then + MODE="venv" +elif command -v "uv"; then + MODE="uv" +else + MODE="venv" +fi + +if [[ "$MODE" == "uv" ]]; then + if [[ ! -d ".venv" ]]; then + echo "Building new .venv using uv" + uv venv --system-site-packages --python "$PYTHON_EXE" .venv + fi + echo "Updating .venv using uv" + uv sync --extra dev --extra doc --extra test +elif [[ "$MODE" == "venv" ]]; then + if [[ ! -d ".venv" ]]; then + echo "Building new .venv using venv module" + "$PYTHON_EXE" -m venv --system-site-packages .venv + fi + source .venv/bin/activate + echo "Updating .venv using pip" + pip install -e '.[dev,doc,test]' +else + echo "Unhandled mode ${MODE}" +fi diff --git a/pyproject.toml b/pyproject.toml index 0050338..064a285 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ name = "pcdswidgets" requires-python = ">=3.12" dependencies = [ "pydm>=1.9.0", - "pyqt5>=5.15.11", "qtpy>=2.4.3", ] @@ -59,6 +58,10 @@ SmaractOpenLoopRow = "pcdswidgets.motion.smaract_open_loop_row:SmaractOpenLoopRo TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] +dev = [ + "jinja2>=3", + "tomlkit>=0.14.0", +] doc = [ "docs-versions-menu>=0.5.2", "sphinx>=9.1.0", @@ -72,10 +75,6 @@ test = [ "pytest-timeout>=2.4.0", "tomlkit>=0.14.0", ] -dev = [ - "jinja2>=3", - "tomlkit>=0.14.0", -] [tool.setuptools.packages.find] where = [ ".",] diff --git a/try_in_designer.sh b/try_in_designer.sh new file mode 100755 index 0000000..f2117e6 --- /dev/null +++ b/try_in_designer.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# shellcheck disable=SC1091 +# +# Runs designer using widgets from the local .venv +# +set -e + +THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +cd "${THIS_DIR}" + +source base_env_vars.sh +source .venv/bin/activate +PYVER="$(cat .python-version)" +export PYQTDESIGNERPATH=".venv/lib/python${PYVER}/site-packages/pydm" +export PYDM_DESIGNER_ONLINE=1 + +"${BASE_ENV}/bin/designer" "$@" diff --git a/try_in_pydm.sh b/try_in_pydm.sh new file mode 100755 index 0000000..8efa659 --- /dev/null +++ b/try_in_pydm.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# shellcheck disable=SC1091 +# +# Runs pydm using widgets from the local .venv +# +set -e + +THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +cd "${THIS_DIR}" + +source base_env_vars.sh +source .venv/bin/activate + +pydm "$@" diff --git a/uv.lock b/uv.lock index 08d363c..8cbc7d1 100644 --- a/uv.lock +++ b/uv.lock @@ -319,7 +319,6 @@ name = "pcdswidgets" source = { editable = "." } dependencies = [ { name = "pydm" }, - { name = "pyqt5" }, { name = "qtpy" }, ] @@ -348,7 +347,6 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'dev'", specifier = ">=3" }, { name = "jinja2", marker = "extra == 'test'", specifier = ">=3" }, { name = "pydm", specifier = ">=1.9.0" }, - { name = "pyqt5", specifier = ">=5.15.11" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2" }, { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.5.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, @@ -359,7 +357,7 @@ requires-dist = [ { name = "tomlkit", marker = "extra == 'dev'", specifier = ">=0.14.0" }, { name = "tomlkit", marker = "extra == 'test'", specifier = ">=0.14.0" }, ] -provides-extras = ["doc", "test", "dev"] +provides-extras = ["dev", "doc", "test"] [[package]] name = "pluggy" @@ -418,56 +416,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pyqt5" -version = "5.15.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyqt5-qt5" }, - { name = "pyqt5-sip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, - { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, -] - -[[package]] -name = "pyqt5-qt5" -version = "5.15.18" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, -] - -[[package]] -name = "pyqt5-sip" -version = "12.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, - { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, - { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, - { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, - { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, -] - [[package]] name = "pyqtgraph" version = "0.14.0" From ff6282eb76b06fe30aa188e587d13dfcf1303c27 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 25 Mar 2026 10:15:16 -0700 Subject: [PATCH 051/104] CI: try the specific pyqt version we use in prod --- .github/workflows/standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index bf16ec3..ea463a5 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -20,6 +20,6 @@ jobs: # Extras to be installed only for conda-based testing: conda-testing-extras: "" # Extras to be installed only for pip-based testing: - pip-testing-extras: "PyQt5==5.15" + pip-testing-extras: "PyQt5==5.15.9" # Set if using setuptools-scm for the conda-build workflow use-setuptools-scm: true From d11f0c8f97e4bba6216a61c1610bbc63207bb2f9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 25 Mar 2026 10:20:42 -0700 Subject: [PATCH 052/104] CI: try unpinned, which is what this repo was doing previously --- .github/workflows/standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index ea463a5..50af250 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -20,6 +20,6 @@ jobs: # Extras to be installed only for conda-based testing: conda-testing-extras: "" # Extras to be installed only for pip-based testing: - pip-testing-extras: "PyQt5==5.15.9" + pip-testing-extras: "PyQt5" # Set if using setuptools-scm for the conda-build workflow use-setuptools-scm: true From 7a779d71df3dc00b35470001684e9dbd93cf7c20 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 25 Mar 2026 11:20:23 -0700 Subject: [PATCH 053/104] CI: try adding docs build extra --- .github/workflows/standard.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index 50af250..28398d0 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -21,5 +21,7 @@ jobs: conda-testing-extras: "" # Extras to be installed only for pip-based testing: pip-testing-extras: "PyQt5" + # Extras to be installed only for docs builds (pip) + docs-build-extras: "PyQt5" # Set if using setuptools-scm for the conda-build workflow use-setuptools-scm: true From bec69737cd2d31ac0d92d2255c05d57d181dfe63 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 14:47:41 -0700 Subject: [PATCH 054/104] FIX: only iterate over the macros found in this widget --- pcdswidgets/builder/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 9c2425e..706142b 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -165,7 +165,7 @@ def process_widget_macros(ui_info: UiInfo) -> InfoForJinja: raise TypeError(f"Invalid macro type: {value_with_macro}") ij.macro_set.update(macros_here) ij.widget_set.add(widget_name) - for macro in ij.macro_set: + for macro in macros_here: ij.macro_to_widget[macro].append(widget_name) ij.widget_to_macro[widget_name] = sorted(macros_here) ij.widget_to_pre_templ_strs[widget_name].extend(str_opts) From f313a170e912189c9ae2127e63dbca5da6c6046f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 14:48:00 -0700 Subject: [PATCH 055/104] MNT: stay in-process for generatic uic files --- pcdswidgets/builder/build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 706142b..8244e1a 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -1,12 +1,12 @@ import dataclasses import os import re -import subprocess import sys import xml.etree.ElementTree as ET from collections import defaultdict from jinja2 import Environment, PackageLoader +from qtpy.uic import compileUi def build_uic(designer_ui: str): @@ -17,7 +17,8 @@ def build_uic(designer_ui: str): some_name.ui -> some_name_form.py """ form_output = f"{os.path.splitext(designer_ui)[0]}_form.py" - subprocess.run(f"pyuic5 -o {form_output} {designer_ui}".split(" ")) + with open(form_output, "w") as fd: + compileUi(designer_ui, fd) def build_base_widget(designer_ui: str): From 58f19ecb13484d14ae334320999fb8f108b85d26 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 14:56:05 -0700 Subject: [PATCH 056/104] MNT: switch to new name for ruff-check hook --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7506849..330fb13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.6 hooks: - - id: ruff # run the linter + - id: ruff-check # run the linter args: [ --fix ] # and the safe fixes - id: ruff-format # run the formatter From 650e69b3ae9adda271c4ee8420ca1abfb11cee47 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 14:56:17 -0700 Subject: [PATCH 057/104] ENH: use a real parser to give feedback when the build script is used incorrectly --- pcdswidgets/builder/build.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 8244e1a..65484c9 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -1,7 +1,7 @@ +import argparse import dataclasses import os import re -import sys import xml.etree.ElementTree as ET from collections import defaultdict @@ -184,11 +184,22 @@ def _get_macros(text_with_macro_sub: str) -> list[str]: if __name__ == "__main__": - mode = sys.argv[1] - designer_ui = sys.argv[2] - if mode == "uic": - build_uic(designer_ui) - elif mode == "base": - build_base_widget(designer_ui) + parser = argparse.ArgumentParser( + "pcdswidgets.builder.build", + description="Automatically build the form or base class files associated with a widget .ui file.", + ) + parser.add_argument( + "mode", + choices=["uic", "base"], + help="Choose 'uic' to build the pyuic form file or 'base' to build the pcdswidgets base class.", + ) + parser.add_argument("designer_ui", help="Path to the designer .ui file to use as the source for the build.") + args = parser.parse_args() + + if args.mode == "uic": + build_uic(args.designer_ui) + elif args.mode == "base": + build_base_widget(args.designer_ui) else: - raise ValueError(f"Invalid mode {mode}, must be uic or base") + # Currently unreachable, probably + raise ValueError(f"Invalid mode {args.mode}, must be uic or base") From caffd98dfc4d16259820d96d8ea1a3def6b97914 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 15:06:50 -0700 Subject: [PATCH 058/104] MNT: skip checking the generated ui directory for designer widgets --- pcdswidgets/entrypoint_widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/entrypoint_widgets.py index 07162b1..f7990b6 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/entrypoint_widgets.py @@ -23,6 +23,7 @@ SKIP_MODULES = [ ".tests", ".demo", + ".ui", ] From 725f47e29b16775f77de2017e421e09117ebca5f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 16:02:21 -0700 Subject: [PATCH 059/104] TST: first pass at some probably broken tests for the builder --- base_env_vars.sh | 2 +- pcdswidgets/builder/build.py | 2 +- pcdswidgets/builder/ui/pytest.ui | 64 ++++++++++++ pcdswidgets/tests/test_designer_widget.py | 122 ++++++++++++++++++++++ uv.lock | 4 + 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 pcdswidgets/builder/ui/pytest.ui create mode 100644 pcdswidgets/tests/test_designer_widget.py diff --git a/base_env_vars.sh b/base_env_vars.sh index 318cdf5..b032d73 100644 --- a/base_env_vars.sh +++ b/base_env_vars.sh @@ -6,7 +6,7 @@ # Disable GL because it crashes on our servers QT_XCB_GL_INTEGRATION=none # Pick a base env that has designer configured appropriately and built to match python + pyqt versions -BASE_ENV="${PYPS_SITE_TOP}/conda/dev/zlentz/miniforge3/envs/ecs-base-0.0.2" +BASE_ENV="${PYPS_SITE_TOP}/conda/dev/zlentz/miniforge3/envs/ecs-base-0.0.3" export QT_XCB_GL_INTEGRATION export BASE_ENV diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 65484c9..eb596bd 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -6,7 +6,7 @@ from collections import defaultdict from jinja2 import Environment, PackageLoader -from qtpy.uic import compileUi +from qtpy.uic import compileUi # type: ignore def build_uic(designer_ui: str): diff --git a/pcdswidgets/builder/ui/pytest.ui b/pcdswidgets/builder/ui/pytest.ui new file mode 100644 index 0000000..7be97c8 --- /dev/null +++ b/pcdswidgets/builder/ui/pytest.ui @@ -0,0 +1,64 @@ + + + Form + + + + 0 + 0 + 400 + 191 + + + + Form + + + + + + ${NAME} + + + Name: ${NAME} + + + + + + + Num: ${NUM} + + + + + + + ${NAME}:${NUM} + + + + + + + + One: ${ONE} + + + + + Two: ${TWO} + + + + + ${ONE}:${TWO} + + + + + + + + + diff --git a/pcdswidgets/tests/test_designer_widget.py b/pcdswidgets/tests/test_designer_widget.py new file mode 100644 index 0000000..e6e9da5 --- /dev/null +++ b/pcdswidgets/tests/test_designer_widget.py @@ -0,0 +1,122 @@ +import inspect + +import pytest +from pytestqt.qtbot import QtBot +from qtpy.QtWidgets import QLabel, QListWidget + +from pcdswidgets.builder.ui.pytest_base import PytestBase + + +class TestWidget(PytestBase): ... + + +@pytest.fixture(scope="function") +def test_widget(qtbot: QtBot) -> TestWidget: + widget = TestWidget() + qtbot.add_widget(widget) + return widget + + +def test_has_expected_hints(test_widget: TestWidget): + class_hints = inspect.get_annotations(TestWidget) + obj_hints = inspect.get_annotations(test_widget) + + for hints in (class_hints, obj_hints): + for label_name in ("name_label", "num_label", "name_num_label"): + assert hints[label_name] is QLabel + assert hints["one_two_list"] is QListWidget + + +def test_has_expected_widgets(test_widget: TestWidget): + assert isinstance(test_widget.name_label, QLabel) + assert isinstance(test_widget.num_label, QLabel) + assert isinstance(test_widget.name_num_label, QLabel) + assert isinstance(test_widget.one_two_list, QListWidget) + + +def test_has_expected_macro_to_widget(test_widget: TestWidget): + assert set(test_widget._macro_to_widget.keys()) == {"NAME", "NUM", "ONE", "TWO"} + assert set(test_widget._macro_to_widget["NAME"]) == {"name_label", "name_num_label"} + assert set(test_widget._macro_to_widget["NUM"]) == {"num_label", "name_num_label"} + assert [test_widget._macro_to_widget["ONE"]] == ["one_two_list"] + assert test_widget._macro_to_widget["TWO"] == ["one_two_list"] + + +def test_has_expected_widget_to_macro(test_widget: TestWidget): + assert set(test_widget._widget_to_macro.keys()) == {"name_label", "num_label", "name_num_label", "one_two_list"} + assert test_widget._widget_to_macro["name_label"] == ["NAME"] + assert test_widget._widget_to_macro["num_label"] == ["NUM"] + assert set(test_widget._widget_to_macro["name_num_label"]) == {"NAME", "NUM"} + assert set(test_widget._widget_to_macro["one_two_list"]) == {"ONE", "TWO"} + + +def test_has_expected_widget_to_pre_template(test_widget: TestWidget): + assert set(test_widget._widget_to_pre_template.keys()) == { + "name_label", + "num_label", + "name_num_label", + "one_two_list", + } + assert set(test_widget._widget_to_pre_template["name_label"]) == {("text", "Name: ${NAME}"), ("toolTip", "${NAME}")} + assert test_widget._widget_to_pre_template["num_label"] == [("text", "Num: ${NUM}")] + assert test_widget._widget_to_pre_template["name_num_label"] == [("text", "${NAME}:${NUM}")] + assert test_widget._widget_to_pre_template["one_two_list"] == [ + ("text", ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"]) + ] + + +def test_has_expected_macro_values(test_widget: TestWidget): + assert test_widget._macro_values == { + "NAME": "", + "NUM": "", + "ONE": "", + "TWO": "", + } + + +def test_macro_substitution_labels(test_widget: TestWidget): + assert test_widget.name_label.text() == "Name: ${NAME}" + assert test_widget.num_label.text() == "Num: ${NUM}" + assert test_widget.name_num_label.text() == "${NAME}:${NUM}" + + test_widget.name = "Jimmy" + + assert test_widget.name_label.text() == "Name: Jimmy" + assert test_widget.num_label.text() == "Num: ${NUM}" + assert test_widget.name_num_label.text() == "${NAME}:${NUM}" + + test_widget.num = "02" + + assert test_widget.name_label.text() == "Name: Jimmy" + assert test_widget.num_label.text() == "Num: 02" + assert test_widget.name_num_label.text() == "Jimmy:02" + + test_widget.name = "Steven" + + assert test_widget.name_label.text() == "Name: Steven" + assert test_widget.num_label.text() == "Num: 02" + assert test_widget.name_num_label.text() == "Steven:02" + + +def test_macro_substitution_list_widget(test_widget: TestWidget): + assert test_widget.one_two_list.item(0).text() == "One: ${ONE}" + assert test_widget.one_two_list.item(1).text() == "Two: ${TWO}" + assert test_widget.one_two_list.item(2).text() == "${ONE}:${TWO}" + + test_widget.one = "UNO" + + assert test_widget.one_two_list.item(0).text() == "One: ${ONE}" + assert test_widget.one_two_list.item(1).text() == "Two: ${TWO}" + assert test_widget.one_two_list.item(2).text() == "${ONE}:${TWO}" + + test_widget.two = "DOS" + + assert test_widget.one_two_list.item(0).text() == "One: UNO" + assert test_widget.one_two_list.item(1).text() == "Two: DOS" + assert test_widget.one_two_list.item(2).text() == "UNO:DOS" + + test_widget.one = "ICHI" + + assert test_widget.one_two_list.item(0).text() == "One: ICHI" + assert test_widget.one_two_list.item(1).text() == "Two: DOS" + assert test_widget.one_two_list.item(2).text() == "ICHI:DOS" diff --git a/uv.lock b/uv.lock index 8cbc7d1..c8b32f8 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +[options] +exclude-newer = "2026-03-20T22:52:20.814700128Z" +exclude-newer-span = "P7D" + [[package]] name = "alabaster" version = "1.0.0" From 56553374a3e5d309c649913829ddf28b2654d9b3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 16:19:29 -0700 Subject: [PATCH 060/104] TST: fix names in test ui --- pcdswidgets/builder/ui/pytest.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pcdswidgets/builder/ui/pytest.ui b/pcdswidgets/builder/ui/pytest.ui index 7be97c8..8f4ed1a 100644 --- a/pcdswidgets/builder/ui/pytest.ui +++ b/pcdswidgets/builder/ui/pytest.ui @@ -15,7 +15,7 @@ - + ${NAME} @@ -25,21 +25,21 @@ - + Num: ${NUM} - + ${NAME}:${NUM} - + One: ${ONE} From be2989fb80e4b2a86466d2a0e80704fa52f99798 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 16:19:53 -0700 Subject: [PATCH 061/104] FIX: make macro regex non-greedy to be able to parse multiple macros in one string --- pcdswidgets/builder/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index eb596bd..758a91b 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -175,7 +175,7 @@ def process_widget_macros(ui_info: UiInfo) -> InfoForJinja: return ij -macro_re = re.compile(r"\${(\S+)}") +macro_re = re.compile(r"\${(\S+?)}") def _get_macros(text_with_macro_sub: str) -> list[str]: From 431a5daa7baa24d82d58a6539f7630adc8a98f7b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:02:44 -0700 Subject: [PATCH 062/104] DOC: update the readme with dev instructions and a new limitation --- README.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2864660..ff00de4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,32 @@ Pick your favorite: - conda install pcdswidgets ### Dev -pip install -e . +A helper script is included here: `build_local_venv.sh` (or, `make venv`). + +This will create a `.venv` virtual environment that will be ready to go +to help you run designer and test your custom widgets. +To work, this requires a suitable base environment to already exist on +your system: one with pyqt and designer python plugin support, +which I've found to be tricky to set up in a scripted way in recent years. + +These base environments are stored centrally at LCLS and are +specified in `base_env_vars.sh`. + +You can run the `build_local_venv.sh` again (or, `make venv`) +to update the environment with any new widgets you've added since the last run. + +Once this environment is created, you can use `try_in_designer.sh` to +make sure your widgets are exporting cleanly in an editable way in designer. + +You can also use `try_in_pydm.sh` to launch a version of `pydm` that includes +your new widgets. + +You can alternatively build your own environment: + +- pip install -e . +- uv sync + +Or whatever your favorite method is. ## Adding Widgets @@ -96,16 +121,18 @@ The steps are: - Iterate, update the widget until you like it. 3. Bring it here - Copy your .ui file in the pcdswidgets/builder/ui folder. -4. make +4. `make` - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. 5. Create a widget class - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - Keep these in separate files to avoid circular import errors from including widgets inside widgets - Import from the _base module created from your .ui file and subclass -6. make, again +6. `make`, again - This will include your widget in pyproject.toml +7. `make venv` + - The recommended way to update your testing virtual environment. -If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets. +If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets and pydm. #### Widget Classes @@ -142,3 +169,4 @@ and look through everything with the `fa` prefix to browse options. #### Limitations - Widgets that contain PyDMEmbeddedWidget are not supported: bootstrap these by turning the contents into widgets themselves. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. +- Only direct QString and QStringList properties are supported. We still need to implement support for item-based QString widgets such as QListWidget. From 7fdc87230246834c8e3d737a2b588fed3085486a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:03:05 -0700 Subject: [PATCH 063/104] STY: nitpick the newline spacing for generated base file --- pcdswidgets/builder/ui_base_widget.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcdswidgets/builder/ui_base_widget.j2 b/pcdswidgets/builder/ui_base_widget.j2 index ecc086a..6bf6124 100644 --- a/pcdswidgets/builder/ui_base_widget.j2 +++ b/pcdswidgets/builder/ui_base_widget.j2 @@ -77,8 +77,8 @@ class {{ base_cls }}(DesignerWidget): "{{ macro }}": "", {% endfor %} } - {% for macro in macro_names %} + def get_{{ macro.lower() }}(self) -> str: return self._get_macro("{{ macro }}") From dd7ecf8c9911ac2020328ae012fc2136b5f138df Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:03:22 -0700 Subject: [PATCH 064/104] TST: switch the test case to the variant we already support in the code --- pcdswidgets/builder/ui/pytest.ui | 82 ++++++++++++++++++----- pcdswidgets/tests/test_designer_widget.py | 55 +++++++-------- 2 files changed, 89 insertions(+), 48 deletions(-) diff --git a/pcdswidgets/builder/ui/pytest.ui b/pcdswidgets/builder/ui/pytest.ui index 8f4ed1a..e45b9a5 100644 --- a/pcdswidgets/builder/ui/pytest.ui +++ b/pcdswidgets/builder/ui/pytest.ui @@ -39,26 +39,76 @@ - - - - One: ${ONE} - - - - - Two: ${TWO} - - - - - ${ONE}:${TWO} - - + + + + + + false + + + true + + + + + + + + + + + + false + + + false + + + Are you sure you want to proceed? + + + + + + true + + + false + + + + + + + echo ${ONE} + echo ${TWO} + echo ${ONE}:${TWO} + + + + false + + + + + + + + + false + + + + PyDMShellCommand + QPushButton +
pydm.widgets.shell_command
+
+
diff --git a/pcdswidgets/tests/test_designer_widget.py b/pcdswidgets/tests/test_designer_widget.py index e6e9da5..373b724 100644 --- a/pcdswidgets/tests/test_designer_widget.py +++ b/pcdswidgets/tests/test_designer_widget.py @@ -1,8 +1,9 @@ import inspect import pytest +from pydm.widgets.shell_command import PyDMShellCommand from pytestqt.qtbot import QtBot -from qtpy.QtWidgets import QLabel, QListWidget +from qtpy.QtWidgets import QLabel from pcdswidgets.builder.ui.pytest_base import PytestBase @@ -18,28 +19,26 @@ def test_widget(qtbot: QtBot) -> TestWidget: def test_has_expected_hints(test_widget: TestWidget): - class_hints = inspect.get_annotations(TestWidget) - obj_hints = inspect.get_annotations(test_widget) + hints = inspect.get_annotations(TestWidget) - for hints in (class_hints, obj_hints): - for label_name in ("name_label", "num_label", "name_num_label"): - assert hints[label_name] is QLabel - assert hints["one_two_list"] is QListWidget + for label_name in ("name_label", "num_label", "name_num_label"): + assert hints[label_name] is QLabel + assert hints["one_two_shell"] is PyDMShellCommand def test_has_expected_widgets(test_widget: TestWidget): assert isinstance(test_widget.name_label, QLabel) assert isinstance(test_widget.num_label, QLabel) assert isinstance(test_widget.name_num_label, QLabel) - assert isinstance(test_widget.one_two_list, QListWidget) + assert isinstance(test_widget.one_two_shell, PyDMShellCommand) def test_has_expected_macro_to_widget(test_widget: TestWidget): assert set(test_widget._macro_to_widget.keys()) == {"NAME", "NUM", "ONE", "TWO"} assert set(test_widget._macro_to_widget["NAME"]) == {"name_label", "name_num_label"} assert set(test_widget._macro_to_widget["NUM"]) == {"num_label", "name_num_label"} - assert [test_widget._macro_to_widget["ONE"]] == ["one_two_list"] - assert test_widget._macro_to_widget["TWO"] == ["one_two_list"] + assert [test_widget._macro_to_widget["ONE"]] == ["one_two_shell"] + assert test_widget._macro_to_widget["TWO"] == ["one_two_shell"] def test_has_expected_widget_to_macro(test_widget: TestWidget): @@ -47,7 +46,7 @@ def test_has_expected_widget_to_macro(test_widget: TestWidget): assert test_widget._widget_to_macro["name_label"] == ["NAME"] assert test_widget._widget_to_macro["num_label"] == ["NUM"] assert set(test_widget._widget_to_macro["name_num_label"]) == {"NAME", "NUM"} - assert set(test_widget._widget_to_macro["one_two_list"]) == {"ONE", "TWO"} + assert set(test_widget._widget_to_macro["one_two_shell"]) == {"ONE", "TWO"} def test_has_expected_widget_to_pre_template(test_widget: TestWidget): @@ -55,13 +54,13 @@ def test_has_expected_widget_to_pre_template(test_widget: TestWidget): "name_label", "num_label", "name_num_label", - "one_two_list", + "one_two_shell", } assert set(test_widget._widget_to_pre_template["name_label"]) == {("text", "Name: ${NAME}"), ("toolTip", "${NAME}")} assert test_widget._widget_to_pre_template["num_label"] == [("text", "Num: ${NUM}")] assert test_widget._widget_to_pre_template["name_num_label"] == [("text", "${NAME}:${NUM}")] - assert test_widget._widget_to_pre_template["one_two_list"] == [ - ("text", ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"]) + assert test_widget._widget_to_pre_template["one_two_shell"] == [ + ("commands", ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"]) ] @@ -79,19 +78,19 @@ def test_macro_substitution_labels(test_widget: TestWidget): assert test_widget.num_label.text() == "Num: ${NUM}" assert test_widget.name_num_label.text() == "${NAME}:${NUM}" - test_widget.name = "Jimmy" + test_widget.setProperty("name", "Jimmy") assert test_widget.name_label.text() == "Name: Jimmy" assert test_widget.num_label.text() == "Num: ${NUM}" assert test_widget.name_num_label.text() == "${NAME}:${NUM}" - test_widget.num = "02" + test_widget.setProperty("num", "02") assert test_widget.name_label.text() == "Name: Jimmy" assert test_widget.num_label.text() == "Num: 02" assert test_widget.name_num_label.text() == "Jimmy:02" - test_widget.name = "Steven" + test_widget.setProperty("name", "Steven") assert test_widget.name_label.text() == "Name: Steven" assert test_widget.num_label.text() == "Num: 02" @@ -99,24 +98,16 @@ def test_macro_substitution_labels(test_widget: TestWidget): def test_macro_substitution_list_widget(test_widget: TestWidget): - assert test_widget.one_two_list.item(0).text() == "One: ${ONE}" - assert test_widget.one_two_list.item(1).text() == "Two: ${TWO}" - assert test_widget.one_two_list.item(2).text() == "${ONE}:${TWO}" + assert test_widget.one_two_shell.readCommands() == ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"] - test_widget.one = "UNO" + test_widget.setProperty("one", "UNO") - assert test_widget.one_two_list.item(0).text() == "One: ${ONE}" - assert test_widget.one_two_list.item(1).text() == "Two: ${TWO}" - assert test_widget.one_two_list.item(2).text() == "${ONE}:${TWO}" + assert test_widget.one_two_shell.readCommands() == ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"] - test_widget.two = "DOS" + test_widget.setProperty("two", "DOS") - assert test_widget.one_two_list.item(0).text() == "One: UNO" - assert test_widget.one_two_list.item(1).text() == "Two: DOS" - assert test_widget.one_two_list.item(2).text() == "UNO:DOS" + assert test_widget.one_two_shell.readCommands() == ["One: UNO", "Two: DOS", "UNO:DOS"] - test_widget.one = "ICHI" + test_widget.setProperty("one", "ICHI") - assert test_widget.one_two_list.item(0).text() == "One: ICHI" - assert test_widget.one_two_list.item(1).text() == "Two: DOS" - assert test_widget.one_two_list.item(2).text() == "ICHI:DOS" + assert test_widget.one_two_shell.readCommands() == ["One: ICHI", "Two: DOS", "ICHI:DOS"] From a6a4057b11b635520994df67b00d5b0d73727f80 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:07:05 -0700 Subject: [PATCH 065/104] TST: fix lingering typos and misunderstandings about how inspect works --- pcdswidgets/tests/test_designer_widget.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pcdswidgets/tests/test_designer_widget.py b/pcdswidgets/tests/test_designer_widget.py index 373b724..8ec0863 100644 --- a/pcdswidgets/tests/test_designer_widget.py +++ b/pcdswidgets/tests/test_designer_widget.py @@ -18,12 +18,12 @@ def test_widget(qtbot: QtBot) -> TestWidget: return widget -def test_has_expected_hints(test_widget: TestWidget): - hints = inspect.get_annotations(TestWidget) +def test_has_expected_hints(): + hints = inspect.get_annotations(PytestBase) for label_name in ("name_label", "num_label", "name_num_label"): - assert hints[label_name] is QLabel - assert hints["one_two_shell"] is PyDMShellCommand + assert hints[label_name] == "QLabel" + assert hints["one_two_shell"] == "PyDMShellCommand" def test_has_expected_widgets(test_widget: TestWidget): @@ -37,12 +37,12 @@ def test_has_expected_macro_to_widget(test_widget: TestWidget): assert set(test_widget._macro_to_widget.keys()) == {"NAME", "NUM", "ONE", "TWO"} assert set(test_widget._macro_to_widget["NAME"]) == {"name_label", "name_num_label"} assert set(test_widget._macro_to_widget["NUM"]) == {"num_label", "name_num_label"} - assert [test_widget._macro_to_widget["ONE"]] == ["one_two_shell"] + assert test_widget._macro_to_widget["ONE"] == ["one_two_shell"] assert test_widget._macro_to_widget["TWO"] == ["one_two_shell"] def test_has_expected_widget_to_macro(test_widget: TestWidget): - assert set(test_widget._widget_to_macro.keys()) == {"name_label", "num_label", "name_num_label", "one_two_list"} + assert set(test_widget._widget_to_macro.keys()) == {"name_label", "num_label", "name_num_label", "one_two_shell"} assert test_widget._widget_to_macro["name_label"] == ["NAME"] assert test_widget._widget_to_macro["num_label"] == ["NUM"] assert set(test_widget._widget_to_macro["name_num_label"]) == {"NAME", "NUM"} @@ -60,7 +60,7 @@ def test_has_expected_widget_to_pre_template(test_widget: TestWidget): assert test_widget._widget_to_pre_template["num_label"] == [("text", "Num: ${NUM}")] assert test_widget._widget_to_pre_template["name_num_label"] == [("text", "${NAME}:${NUM}")] assert test_widget._widget_to_pre_template["one_two_shell"] == [ - ("commands", ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"]) + ("commands", ["echo ${ONE}", "echo ${TWO}", "echo ${ONE}:${TWO}"]) ] @@ -98,16 +98,16 @@ def test_macro_substitution_labels(test_widget: TestWidget): def test_macro_substitution_list_widget(test_widget: TestWidget): - assert test_widget.one_two_shell.readCommands() == ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"] + assert test_widget.one_two_shell.readCommands() == ["echo ${ONE}", "echo ${TWO}", "echo ${ONE}:${TWO}"] test_widget.setProperty("one", "UNO") - assert test_widget.one_two_shell.readCommands() == ["One: ${ONE}", "Two: ${TWO}", "${ONE}:${TWO}"] + assert test_widget.one_two_shell.readCommands() == ["echo ${ONE}", "echo ${TWO}", "echo ${ONE}:${TWO}"] test_widget.setProperty("two", "DOS") - assert test_widget.one_two_shell.readCommands() == ["One: UNO", "Two: DOS", "UNO:DOS"] + assert test_widget.one_two_shell.readCommands() == ["echo UNO", "echo DOS", "echo UNO:DOS"] test_widget.setProperty("one", "ICHI") - assert test_widget.one_two_shell.readCommands() == ["One: ICHI", "Two: DOS", "ICHI:DOS"] + assert test_widget.one_two_shell.readCommands() == ["echo ICHI", "echo DOS", "echo ICHI:DOS"] From ca3a53fa1766d94eb5df89854ad713097a2bd873 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:11:59 -0700 Subject: [PATCH 066/104] BLD: try specifying qtpy as a build dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 064a285..6d7af42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2", "jinja2>=3", "pyqt5>=5.15.11",] +requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2", "jinja2>=3", "pyqt5>=5.15.11", "qtpy>=2.4.3"] [project] classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Python :: 3",] From 895aa147225f4c8688ae14268d4aec529bccfe3e Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 27 Mar 2026 17:16:18 -0700 Subject: [PATCH 067/104] BLD: also add qtpy as a conda build dependency --- conda-recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 625215d..a87aa5f 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -20,6 +20,7 @@ requirements: - setuptools_scm - pip - jinja2 >=3 + - qtpy - pyqt >=5 run: - python >=3.9 From 951cacbc72256fb9e30d154fa94660c78df02f40 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 2 Apr 2026 14:50:02 -0700 Subject: [PATCH 068/104] DOC: update some missing info on env setup for --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff00de4..dbbfb00 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ You can alternatively build your own environment: Or whatever your favorite method is. +Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS! + ## Adding Widgets ### Widget Sizing @@ -107,6 +109,8 @@ If the widget has been added and is included in the pyproject.toml file, it will ### Adding a Composite Widget This is how you would convert a .ui file with macro substitution that is normally used with PyDMEmbeddedDisplay into a designer widget served from here. +Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS! + This is not required, but you would do this to make your widget globally available and easier to add to screens. This requires only basic Python knowledge. @@ -121,19 +125,36 @@ The steps are: - Iterate, update the widget until you like it. 3. Bring it here - Copy your .ui file in the pcdswidgets/builder/ui folder. -4. `make` +4. Activate your virtual environment + - `make venv` + - `source .venv/bin/activate` +5. `make` - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. -5. Create a widget class +6. Create a widget class - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - Keep these in separate files to avoid circular import errors from including widgets inside widgets - Import from the _base module created from your .ui file and subclass -6. `make`, again +7. `make`, again - This will include your widget in pyproject.toml -7. `make venv` +8. `make venv`, one last time - The recommended way to update your testing virtual environment. If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets and pydm. +You can then use: +``` +try_in_designer.sh +``` +To open designer with your new widget +(Which, reminder: only works on rocky9 at LCLS) + + +You can also: +``` +try_in_pydm.sh +``` +To run a pydm screen that has your new widget. + #### Widget Classes The widget class looks something like: From dc2a5ce01d8f1f4893b55d73169da66377439b88 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 7 Apr 2026 17:12:50 -0700 Subject: [PATCH 069/104] TST: formally address container widget sizing in the test suite --- pcdswidgets/tests/test_entrypoint_widgets.py | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index e1c9ce9..6c07030 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -20,6 +20,10 @@ def test_entrypoint_has_all_widgets(): assert pydm_widgets.select(name=name)[name].value == entrypoint +container_widgets = [ + "FilterSortWidgetTable", +] + # Don't check widgets from before we made sizing/naming standards exempt_widgets = [ "ApertureValve", @@ -31,7 +35,6 @@ def test_entrypoint_has_all_widgets(): "ControlValve", "EPSByteIndicator", "FastShutter", - "FilterSortWidgetTable", "GetterPump", "HotCathodeComboGauge", "HotCathodeGauge", @@ -46,7 +49,7 @@ def test_entrypoint_has_all_widgets(): "RoughGauge", "ScrollPump", "TurboPump", -] +] + container_widgets @pytest.mark.parametrize( @@ -88,3 +91,17 @@ def test_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): assert widget.maximumWidth() <= max_w, f"{widget_name}'s maximum width is too large." assert widget.minimumHeight() >= min_h, f"{widget_name}'s minimum height is too small." assert widget.maximumHeight() <= max_h, f"{widget_name}'s maximum height is too large." + + +@pytest.mark.parametrize("widget_name,WidgetCls", [elem for elem in iter_all_widgets() if elem[0] in container_widgets]) +def test_container_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtbot): + """ + Ensure that container widgets have no maximum size. + """ + widget = WidgetCls() + qtbot.addWidget(widget) + + # The max size is currently 16777215 + # Pick a smaller but still absurd number as the threshold + assert widget.maximumWidth() >= 100000, f"{widget_name}'s minimum width is too small." + assert widget.maximumHeight() >= 100000, f"{widget_name}'s minimum height is too small." From 08e49f3b879d658d5f545b33835ee83ae9d5156a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 7 Apr 2026 17:35:17 -0700 Subject: [PATCH 070/104] DOC: update pull request template for a custom-made pre-merge checklist, remove issue template to use org defaults --- .github/ISSUE_TEMPLATE.md | 26 -------------------------- .github/PULL_REQUEST_TEMPLATE.md | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 29 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b7ee0ca..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,26 +0,0 @@ - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - -1. -2. -3. - -## Context - - - -## Your Environment - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 657111f..cd79aca 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,15 @@ - +## Screenshots + + +## Pre-merge Checklist +- [ ] Screenshots of these widgets in designer are included above (`try_in_designer.sh`) +- [ ] Screenshots of these widgets working in PyDM are included above (`try_in_pydm.sh`) +- [ ] Code works interactively +- [ ] Code contains descriptive docstrings, including context and API +- [ ] New/changed functions and methods are covered in the test suite where possible +- [ ] New/changed widgets are part of the test suite (semi-automatic) +- [ ] Test suite passes locally +- [ ] Test suite passes on GitHub Actions From 48d4d57665768e98c92247c580b57e678ce5180c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 7 Apr 2026 17:37:09 -0700 Subject: [PATCH 071/104] DOC: correct error, use correct font awesome prefix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbbfb00..52ac604 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ in the "icon" key of the `_qt_designer_` variable: If this is a string, we'll convert it to a QIcon using Pydm's IconFont. This uses a portable version of fontawesome, try running `qta-browser` -and look through everything with the `fa` prefix to browse options. +and look through everything with the `fa5s` prefix to browse options. #### Limitations From 1dc000c6b0a94ec9efdb286465b5d8880582e319 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 7 Apr 2026 17:38:11 -0700 Subject: [PATCH 072/104] MNT: sanitize pythonpath variable in try script to avoid pollution --- try_in_designer.sh | 1 + try_in_pydm.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/try_in_designer.sh b/try_in_designer.sh index f2117e6..354cfc1 100755 --- a/try_in_designer.sh +++ b/try_in_designer.sh @@ -8,6 +8,7 @@ set -e THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" cd "${THIS_DIR}" +unset PYTHONPATH source base_env_vars.sh source .venv/bin/activate PYVER="$(cat .python-version)" diff --git a/try_in_pydm.sh b/try_in_pydm.sh index 8efa659..5841470 100755 --- a/try_in_pydm.sh +++ b/try_in_pydm.sh @@ -8,6 +8,7 @@ set -e THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" cd "${THIS_DIR}" +unset PYTHONPATH source base_env_vars.sh source .venv/bin/activate From 22acd5f23bb9f2cb2f87e958771470993f1cc8a2 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 09:36:14 -0700 Subject: [PATCH 073/104] TST: fix typo in assert Co-authored-by: KaushikMalapati <80156796+KaushikMalapati@users.noreply.github.com> --- pcdswidgets/tests/test_entrypoint_widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/test_entrypoint_widgets.py index 6c07030..fef9b66 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/test_entrypoint_widgets.py @@ -103,5 +103,5 @@ def test_container_widget_sizing(widget_name: str, WidgetCls: type[QWidget], qtb # The max size is currently 16777215 # Pick a smaller but still absurd number as the threshold - assert widget.maximumWidth() >= 100000, f"{widget_name}'s minimum width is too small." - assert widget.maximumHeight() >= 100000, f"{widget_name}'s minimum height is too small." + assert widget.maximumWidth() >= 100000, f"{widget_name}'s maximum width is too small." + assert widget.maximumHeight() >= 100000, f"{widget_name}'s maximum height is too small." From 0914cf529bd56f2ee180cc9009ec9b4a5653f769 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 11:18:17 -0700 Subject: [PATCH 074/104] DOC: clarify the off-limits folders for widgets --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 52ac604..65f2556 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ The steps are: - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - Keep these in separate files to avoid circular import errors from including widgets inside widgets - Import from the _base module created from your .ui file and subclass + - Note: do not put this in the tests, demo, or ui folders! The tests folder is not scanned for production-level widgets! 7. `make`, again - This will include your widget in pyproject.toml 8. `make venv`, one last time From 0cc39d255231aad546685d70f56c30f4948e489f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 18:22:24 -0700 Subject: [PATCH 075/104] WIP: large reorganization/rework/extension from review for better UX, untested --- Makefile | 41 ++++--- README.md | 5 +- pcdswidgets/builder/build.py | 114 ++++++++++++++++-- .../entrypoint_finder.py} | 6 +- pcdswidgets/builder/ui_main_widget.j2 | 15 +++ .../{builder/ui => generated}/__init__.py | 0 pcdswidgets/motion/__init__.py | 0 pcdswidgets/motion/motor_record_full.py | 9 -- pcdswidgets/motion/motor_record_row.py | 9 -- .../motion/motor_record_tc_interlock_row.py | 9 -- pcdswidgets/motion/smaract_open_loop_row.py | 9 -- .../{builder/ui => tests/builder}/pytest.ui | 0 .../tests/{ => builder}/test_builder.py | 3 +- .../{ => builder}/test_designer_widget.py | 3 +- .../{ => builder}/test_entrypoint_widgets.py | 2 +- .../common/motor_record_classic_full.ui} | 0 .../common/motor_record_classic_row.ui} | 0 .../motor_record_classic_tc_interlock_row.ui} | 2 +- .../smaract/smaract_open_loop_classic_row.ui} | 0 setup.py | 17 --- 20 files changed, 156 insertions(+), 88 deletions(-) rename pcdswidgets/{entrypoint_widgets.py => builder/entrypoint_finder.py} (95%) create mode 100644 pcdswidgets/builder/ui_main_widget.j2 rename pcdswidgets/{builder/ui => generated}/__init__.py (100%) delete mode 100644 pcdswidgets/motion/__init__.py delete mode 100644 pcdswidgets/motion/motor_record_full.py delete mode 100644 pcdswidgets/motion/motor_record_row.py delete mode 100644 pcdswidgets/motion/motor_record_tc_interlock_row.py delete mode 100644 pcdswidgets/motion/smaract_open_loop_row.py rename pcdswidgets/{builder/ui => tests/builder}/pytest.ui (100%) rename pcdswidgets/tests/{ => builder}/test_builder.py (94%) rename pcdswidgets/tests/{ => builder}/test_designer_widget.py (99%) rename pcdswidgets/tests/{ => builder}/test_entrypoint_widgets.py (97%) rename pcdswidgets/{builder/ui/motor_record_full.ui => ui/motion/common/motor_record_classic_full.ui} (100%) rename pcdswidgets/{builder/ui/motor_record_row.ui => ui/motion/common/motor_record_classic_row.ui} (100%) rename pcdswidgets/{builder/ui/motor_record_tc_interlock_row.ui => ui/motion/common/motor_record_classic_tc_interlock_row.ui} (98%) rename pcdswidgets/{builder/ui/smaract_open_loop_row.ui => ui/motion/smaract/smaract_open_loop_classic_row.ui} (100%) delete mode 100644 setup.py diff --git a/Makefile b/Makefile index bf75b87..5e7a529 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,38 @@ -.PHONY: all build clean venv +.PHONY: all build venv venv-again -UI_SOURCE := $(wildcard pcdswidgets/builder/ui/*.ui) +UI_SOURCE := $(wildcard pcdswidgets/ui/*/*/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) -JINJA_SOURCE := $(wildcard pcdswidgets/builder/*.j2) -PY_FORM := $(UI_SOURCE:.ui=_form.py) -PY_BASE := $(UI_SOURCE:.ui=_base.py) +PY_FORM := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_form.py) +PY_BASE := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_base.py) +PY_MAIN := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/%.py) -all: build pyproject.toml +all: venv build pyproject.toml venv-again -# make build is for pip, etc. so pyproject.toml doesn't change at build time -build: $(PY_FORM) $(PY_BASE) +build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) -clean: - rm $(PY_FORM) - rm $(PY_BASE) +# Need to re-run form and base if the ui file is updated +$(PY_FORM): pcdswidgets/generated/%_form.py: pcdswidgets/ui/%.ui + @source .venv/bin/activate + python -m pcdswidgets.builder.build uic $^ -$(PY_FORM): $(UI_SOURCE) $(PY_SOURCE) $(JINJA_SOURCE) - python -m pcdswidgets.builder.build uic $(@:_form.py=.ui) +$(PY_BASE): pcdswidgets/generated/%_base.py: pcdswidgets/ui/%.ui + @source .venv/bin/activate + python -m pcdswidgets.builder.build base $^ -$(PY_BASE): $(UI_SOURCE) $(PY_SOURCE) $(JINJA_SOURCE) - python -m pcdswidgets.builder.build base $(@:_base.py=.ui) +# Only run if the target is missing: user can edit these +$(PY_MAIN): + @source .venv/bin/activate + python -m pcdswidgets.builder.build main $(@:pcdswidgets/%.py=pcdswidgets/ui/%.ui) +# Rerun if any python file is updated pyproject.toml: $(PY_SOURCE) - python -m pcdswidgets.entrypoint_widgets + @source .venv/bin/activate + python -m pcdswidgets.builder.entrypoint_finder venv: ./build_local_venv.sh + +# For running again after pyproject.toml is regenerated +venv-again: + ./build_local_venv.sh diff --git a/README.md b/README.md index 65f2556..c229bae 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,12 @@ These may have a variety of sizes because we had no standards, and will not be c ### Widget Naming Device control widgets should be named based on the type of device that they control. The name should be specific enough to distinguish it from other widgets, but general enough to cover all devices that can be used. -Widgets are named using CamelCase and must end with the size, e.g. `MotorRecordFull` +Widgets are named using CamelCase and must end with the size, e.g. `MotorRecordClassicFull`. +Avoid using names that might preclude different styles or versions of the same. For example, `MyDeviceFull` isn't specific enough. Give is a name like `MyDeviceRetroFull` or something like this that is more apt for your use case, so someone else could charitably make `MyDeviceResizableFull` and we can easily differentiate them. There is no need to end a widget name with "Widget". -Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_record_full.ui` for `MotorRecordFull`, as well as the module that contains the widget which should be called `motor_record_full.py`. +Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_record_classic_full.ui` for `MotorRecordClassicFull`, as well as the module that contains the widget which should be called `motor_record_classic_full.py`. Widgets should never be renamed between tags, this will break existing screens. diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 758a91b..321d247 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -4,24 +4,28 @@ import re import xml.etree.ElementTree as ET from collections import defaultdict +from pathlib import Path from jinja2 import Environment, PackageLoader from qtpy.uic import compileUi # type: ignore -def build_uic(designer_ui: str): +def build_uic(designer_ui: str, output_dir: str = ""): """ Use the standard uic parser to create a .py file with a .ui file's widget layouts. The files are named systematically with patterns like: some_name.ui -> some_name_form.py """ - form_output = f"{os.path.splitext(designer_ui)[0]}_form.py" - with open(form_output, "w") as fd: + output_dir_path = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) + output_dir_path.mkdir(parents=True) + output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_form.py") + with open(output_file, "w") as fd: compileUi(designer_ui, fd) + build_inits(base_dir=output_dir) -def build_base_widget(designer_ui: str): +def build_base_widget(designer_ui: str, output_dir: str | Path = ""): """ Create a .py file with a suitable base widget for inclusion in designer. @@ -40,7 +44,7 @@ def build_base_widget(designer_ui: str): # Bring the info into a good form for the jinja template ui_name = os.path.basename(designer_ui) - base_cls = "".join(part.title() for part in ui_name.removesuffix(".ui").split("_")) + "Base" + base_cls = get_base_class_name(designer_ui=designer_ui) info_for_jinja = process_widget_macros(ui_info) macro_names = sorted(info_for_jinja.macro_set) @@ -63,9 +67,93 @@ def build_base_widget(designer_ui: str): widget_to_pre_templ_strs=info_for_jinja.widget_to_pre_templ_strs, widget_to_pre_templ_lists=info_for_jinja.widget_to_pre_templ_lists, ) - dst_path = designer_ui.removesuffix(".ui") + "_base.py" - with open(dst_path, "w") as fd: + output_dir = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) + output_dir.mkdir(parents=True) + output_file = output_dir / os.path.basename(designer_ui).replace(".ui", "_base.py") + with open(output_file, "w") as fd: fd.write(jinja_output) + build_inits(base_dir=output_dir) + + +def build_main_widget(designer_ui: str, output_dir: str | Path = ""): + """ + Create a .py file that will be included in designer as-is. + + The files are named systematically with patterns like: + some_name.ui -> some_name.py + + See ui_main_widget.j2, which is the jinja template for these output files. + """ + # Collect some info + designer_path = Path(designer_ui) + module_parts = ["pcdswidgets"] + seen_ui = False + for path_part in designer_path.parts: + if path_part == "ui": + seen_ui = True + elif seen_ui: + module_parts.append(path_part) + module_parts.append(os.path.basename(designer_ui).replace(".ui", "base")) + absolute_import_path = ".".join(module_parts) + group_parts = module_parts[1:-2] + default_group = f"PCDS {' '.join(group_parts)}" + # Fill the template + jinja_template = "ui_base_widget.j2" + env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) + template = env.get_template(jinja_template) + jinja_output = template.render( + absolute_import_path=absolute_import_path, + base_cls=get_base_class_name(designer_ui=designer_ui), + main_cls=get_main_class_name(designer_ui=designer_ui), + default_group=default_group, + ) + output_dir = get_output_path(designer_ui=designer_ui, default_base="", output_dir=output_dir) + output_dir.mkdir(parents=True) + output_file = output_dir / os.path.basename(designer_ui).replace(".ui", ".py") + with open(output_file, "w") as fd: + fd.write(jinja_output) + build_inits(base_dir=output_dir) + + +def build_inits(base_dir: str | Path): + """ + Create blank __init__.py files wherever they are needed in generated directories. + + This makes Python treat these directories as Python modules. + """ + candidates: set[Path] = set() + base_dir = Path(base_dir) + for path in base_dir.rglob("*"): + if not path.name.startswith(".") and "__pycache__" not in path.parts: + candidates.add(path.with_name("__init__.py")) + for cand_path in candidates: + cand_path.touch() + + +def get_output_path(designer_ui: str | Path, default_base: str, output_dir: str | Path = "") -> Path: + if output_dir: + return Path(output_dir) + else: + designer_ui_path = Path(designer_ui) + designer_parts = list(designer_ui_path.parts) + if default_base: + for idx, part in enumerate(designer_parts): + if part == "ui": + designer_parts[idx] = default_base + break + else: + designer_parts.remove("ui") + return designer_ui_path.with_segments(*designer_parts[:-1]) + + +def get_base_class_name(designer_ui: str) -> str: + return get_main_class_name(designer_ui=designer_ui) + "Base" + + +def get_main_class_name(designer_ui: str): + ui_name = os.path.basename(designer_ui) + ui_parts = ui_name.removesuffix(".ui").split("_") + return "".join(part.title() for part in ui_parts) @dataclasses.dataclass @@ -190,8 +278,12 @@ def _get_macros(text_with_macro_sub: str) -> list[str]: ) parser.add_argument( "mode", - choices=["uic", "base"], - help="Choose 'uic' to build the pyuic form file or 'base' to build the pcdswidgets base class.", + choices=["uic", "base", "main"], + help=( + "Choose 'uic' to build the pyuic form, " + "'base' to build the pcdswidgets base class, " + "or 'main' to build the pcdswidgets main (user editable) class." + ), ) parser.add_argument("designer_ui", help="Path to the designer .ui file to use as the source for the build.") args = parser.parse_args() @@ -200,6 +292,8 @@ def _get_macros(text_with_macro_sub: str) -> list[str]: build_uic(args.designer_ui) elif args.mode == "base": build_base_widget(args.designer_ui) + elif args.mode == "main": + build_main_widget(args.designer_ui) else: # Currently unreachable, probably - raise ValueError(f"Invalid mode {args.mode}, must be uic or base") + raise ValueError(f"Invalid mode {args.mode}, must be uic, base, or main") diff --git a/pcdswidgets/entrypoint_widgets.py b/pcdswidgets/builder/entrypoint_finder.py similarity index 95% rename from pcdswidgets/entrypoint_widgets.py rename to pcdswidgets/builder/entrypoint_finder.py index f7990b6..f9a3d70 100644 --- a/pcdswidgets/entrypoint_widgets.py +++ b/pcdswidgets/builder/entrypoint_finder.py @@ -2,7 +2,7 @@ Helper module for creating the [project.entry-points."pydm.widget"] section in pyproject.toml -python -m pcdswidgets.entrypoint_widgets +python -m pcdswidgets.builder.entrypoint_finder """ import importlib @@ -16,6 +16,8 @@ import tomlkit.items as tki from qtpy.QtWidgets import QWidget +import pcdswidgets + SKIP_WIDGETS = [ "PCDSSymbolBase", ] @@ -77,7 +79,7 @@ def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: def get_pyproj_path() -> Path: - return Path(__file__).parent.parent / "pyproject.toml" + return Path(pcdswidgets.__file__).parent / "pyproject.toml" def get_current_widget_table() -> tuple[tki.Table, tk.TOMLDocument]: diff --git a/pcdswidgets/builder/ui_main_widget.j2 b/pcdswidgets/builder/ui_main_widget.j2 new file mode 100644 index 0000000..a2cd87c --- /dev/null +++ b/pcdswidgets/builder/ui_main_widget.j2 @@ -0,0 +1,15 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" +from {{ absolute_import_path }} import {{ base_cls }} +from pcdswigets.builder.designer_options import DesignerOptions + + +class {{ main_cls }}({{ base_cls }}): + designer_options = DesignerOptions( + group="{{ default_group }}", + is_containter=False, + icon=None, + ) diff --git a/pcdswidgets/builder/ui/__init__.py b/pcdswidgets/generated/__init__.py similarity index 100% rename from pcdswidgets/builder/ui/__init__.py rename to pcdswidgets/generated/__init__.py diff --git a/pcdswidgets/motion/__init__.py b/pcdswidgets/motion/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pcdswidgets/motion/motor_record_full.py b/pcdswidgets/motion/motor_record_full.py deleted file mode 100644 index 646e8e3..0000000 --- a/pcdswidgets/motion/motor_record_full.py +++ /dev/null @@ -1,9 +0,0 @@ -from pcdswidgets.builder.ui.motor_record_full_base import MotorRecordFullBase - - -class MotorRecordFull(MotorRecordFullBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - "icon": "expand-arrows-alt", - } diff --git a/pcdswidgets/motion/motor_record_row.py b/pcdswidgets/motion/motor_record_row.py deleted file mode 100644 index 24719f8..0000000 --- a/pcdswidgets/motion/motor_record_row.py +++ /dev/null @@ -1,9 +0,0 @@ -from pcdswidgets.builder.ui.motor_record_row_base import MotorRecordRowBase - - -class MotorRecordRow(MotorRecordRowBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - "icon": "arrows-alt-h", - } diff --git a/pcdswidgets/motion/motor_record_tc_interlock_row.py b/pcdswidgets/motion/motor_record_tc_interlock_row.py deleted file mode 100644 index 5cc067a..0000000 --- a/pcdswidgets/motion/motor_record_tc_interlock_row.py +++ /dev/null @@ -1,9 +0,0 @@ -from pcdswidgets.builder.ui.motor_record_tc_interlock_row_base import MotorRecordTcInterlockRowBase - - -class MotorRecordTcInterlockRow(MotorRecordTcInterlockRowBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - "icon": "arrows-alt-h", - } diff --git a/pcdswidgets/motion/smaract_open_loop_row.py b/pcdswidgets/motion/smaract_open_loop_row.py deleted file mode 100644 index 15f4684..0000000 --- a/pcdswidgets/motion/smaract_open_loop_row.py +++ /dev/null @@ -1,9 +0,0 @@ -from pcdswidgets.builder.ui.smaract_open_loop_row_base import SmaractOpenLoopRowBase - - -class SmaractOpenLoopRow(SmaractOpenLoopRowBase): - _qt_designer_ = { - "group": "PCDS Motion", - "is_container": False, - "icon": "arrows-alt-h", - } diff --git a/pcdswidgets/builder/ui/pytest.ui b/pcdswidgets/tests/builder/pytest.ui similarity index 100% rename from pcdswidgets/builder/ui/pytest.ui rename to pcdswidgets/tests/builder/pytest.ui diff --git a/pcdswidgets/tests/test_builder.py b/pcdswidgets/tests/builder/test_builder.py similarity index 94% rename from pcdswidgets/tests/test_builder.py rename to pcdswidgets/tests/builder/test_builder.py index 299d311..dd9aeaf 100644 --- a/pcdswidgets/tests/test_builder.py +++ b/pcdswidgets/tests/builder/test_builder.py @@ -4,9 +4,10 @@ import pytest +import pcdswidgets from pcdswidgets.builder.designer_widget import DesignerWidget -UI_SOURCES = sorted((Path(__file__).parent.parent / "builder" / "ui").glob("*.ui")) +UI_SOURCES = sorted((Path(pcdswidgets.__file__) / "ui").rglob("*.ui")) @pytest.mark.parametrize("ui_source", UI_SOURCES) diff --git a/pcdswidgets/tests/test_designer_widget.py b/pcdswidgets/tests/builder/test_designer_widget.py similarity index 99% rename from pcdswidgets/tests/test_designer_widget.py rename to pcdswidgets/tests/builder/test_designer_widget.py index 8ec0863..a8b08e9 100644 --- a/pcdswidgets/tests/test_designer_widget.py +++ b/pcdswidgets/tests/builder/test_designer_widget.py @@ -1,12 +1,11 @@ import inspect import pytest +from pcdswidgets.builder.ui.pytest_base import PytestBase from pydm.widgets.shell_command import PyDMShellCommand from pytestqt.qtbot import QtBot from qtpy.QtWidgets import QLabel -from pcdswidgets.builder.ui.pytest_base import PytestBase - class TestWidget(PytestBase): ... diff --git a/pcdswidgets/tests/test_entrypoint_widgets.py b/pcdswidgets/tests/builder/test_entrypoint_widgets.py similarity index 97% rename from pcdswidgets/tests/test_entrypoint_widgets.py rename to pcdswidgets/tests/builder/test_entrypoint_widgets.py index fef9b66..e83de9e 100644 --- a/pcdswidgets/tests/test_entrypoint_widgets.py +++ b/pcdswidgets/tests/builder/test_entrypoint_widgets.py @@ -4,7 +4,7 @@ from pydm.config import ENTRYPOINT_WIDGET from qtpy.QtWidgets import QWidget -from pcdswidgets.entrypoint_widgets import get_widget_entrypoint_data, iter_all_widgets +from pcdswidgets.builder.entrypoint_finder import get_widget_entrypoint_data, iter_all_widgets def test_entrypoint_has_all_widgets(): diff --git a/pcdswidgets/builder/ui/motor_record_full.ui b/pcdswidgets/ui/motion/common/motor_record_classic_full.ui similarity index 100% rename from pcdswidgets/builder/ui/motor_record_full.ui rename to pcdswidgets/ui/motion/common/motor_record_classic_full.ui diff --git a/pcdswidgets/builder/ui/motor_record_row.ui b/pcdswidgets/ui/motion/common/motor_record_classic_row.ui similarity index 100% rename from pcdswidgets/builder/ui/motor_record_row.ui rename to pcdswidgets/ui/motion/common/motor_record_classic_row.ui diff --git a/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui b/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui similarity index 98% rename from pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui rename to pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui index 3eb2194..28147e8 100644 --- a/pcdswidgets/builder/ui/motor_record_tc_interlock_row.ui +++ b/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui @@ -253,7 +253,7 @@ MotorRecordRow QWidget -
pcdswidgets.motion.motor_record_row
+
pcdswidgets.motion.common.motor_record_classic_row
diff --git a/pcdswidgets/builder/ui/smaract_open_loop_row.ui b/pcdswidgets/ui/motion/smaract/smaract_open_loop_classic_row.ui similarity index 100% rename from pcdswidgets/builder/ui/smaract_open_loop_row.ui rename to pcdswidgets/ui/motion/smaract/smaract_open_loop_classic_row.ui diff --git a/setup.py b/setup.py deleted file mode 100644 index f6367a2..0000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -import subprocess - -from setuptools import setup -from setuptools.command.build_py import build_py - - -class MakeBuildFirst(build_py): - """ - Use the instructions in the Makefile to generate needed .py files from the .ui source - """ - - def run(self, *args, **kwargs): - subprocess.check_call(("make", "build")) - return super().run(*args, **kwargs) - - -setup(cmdclass={"build_py": MakeBuildFirst}) From 03cb5c85442da81e32e9ead31e9adbcf57babb03 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 18:28:09 -0700 Subject: [PATCH 076/104] FIX: use path to python executable, not source, doesn't work like that here --- Makefile | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 5e7a529..c0d8c57 100644 --- a/Makefile +++ b/Makefile @@ -13,22 +13,18 @@ build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) # Need to re-run form and base if the ui file is updated $(PY_FORM): pcdswidgets/generated/%_form.py: pcdswidgets/ui/%.ui - @source .venv/bin/activate - python -m pcdswidgets.builder.build uic $^ + .venv/bin/python -m pcdswidgets.builder.build uic $^ $(PY_BASE): pcdswidgets/generated/%_base.py: pcdswidgets/ui/%.ui - @source .venv/bin/activate - python -m pcdswidgets.builder.build base $^ + .venv/bin/python -m pcdswidgets.builder.build base $^ # Only run if the target is missing: user can edit these $(PY_MAIN): - @source .venv/bin/activate - python -m pcdswidgets.builder.build main $(@:pcdswidgets/%.py=pcdswidgets/ui/%.ui) + .venv/bin/python -m pcdswidgets.builder.build main $(@:pcdswidgets/%.py=pcdswidgets/ui/%.ui) # Rerun if any python file is updated pyproject.toml: $(PY_SOURCE) - @source .venv/bin/activate - python -m pcdswidgets.builder.entrypoint_finder + .venv/bin/python -m pcdswidgets.builder.entrypoint_finder venv: ./build_local_venv.sh From 5dc699ec21db13a32ba4adfe3d7503a48f28183a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 18:39:56 -0700 Subject: [PATCH 077/104] FIX: fix some of the more obvious problems in builder --- pcdswidgets/builder/build.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 321d247..0322e72 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -18,14 +18,14 @@ def build_uic(designer_ui: str, output_dir: str = ""): some_name.ui -> some_name_form.py """ output_dir_path = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) - output_dir_path.mkdir(parents=True) + output_dir_path.mkdir(parents=True, exist_ok=True) output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_form.py") with open(output_file, "w") as fd: compileUi(designer_ui, fd) - build_inits(base_dir=output_dir) + build_inits(base_dir=output_dir_path) -def build_base_widget(designer_ui: str, output_dir: str | Path = ""): +def build_base_widget(designer_ui: str, output_dir: str = ""): """ Create a .py file with a suitable base widget for inclusion in designer. @@ -67,15 +67,15 @@ def build_base_widget(designer_ui: str, output_dir: str | Path = ""): widget_to_pre_templ_strs=info_for_jinja.widget_to_pre_templ_strs, widget_to_pre_templ_lists=info_for_jinja.widget_to_pre_templ_lists, ) - output_dir = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) - output_dir.mkdir(parents=True) - output_file = output_dir / os.path.basename(designer_ui).replace(".ui", "_base.py") + output_dir_path = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) + output_dir_path.mkdir(parents=True, exist_ok=True) + output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_base.py") with open(output_file, "w") as fd: fd.write(jinja_output) - build_inits(base_dir=output_dir) + build_inits(base_dir=output_dir_path) -def build_main_widget(designer_ui: str, output_dir: str | Path = ""): +def build_main_widget(designer_ui: str, output_dir: str = ""): """ Create a .py file that will be included in designer as-is. @@ -98,7 +98,7 @@ def build_main_widget(designer_ui: str, output_dir: str | Path = ""): group_parts = module_parts[1:-2] default_group = f"PCDS {' '.join(group_parts)}" # Fill the template - jinja_template = "ui_base_widget.j2" + jinja_template = "ui_main_widget.j2" env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) template = env.get_template(jinja_template) jinja_output = template.render( @@ -107,12 +107,12 @@ def build_main_widget(designer_ui: str, output_dir: str | Path = ""): main_cls=get_main_class_name(designer_ui=designer_ui), default_group=default_group, ) - output_dir = get_output_path(designer_ui=designer_ui, default_base="", output_dir=output_dir) - output_dir.mkdir(parents=True) - output_file = output_dir / os.path.basename(designer_ui).replace(".ui", ".py") + output_dir_path = get_output_path(designer_ui=designer_ui, default_base="", output_dir=output_dir) + output_dir_path.mkdir(parents=True, exist_ok=True) + output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", ".py") with open(output_file, "w") as fd: fd.write(jinja_output) - build_inits(base_dir=output_dir) + build_inits(base_dir=output_dir_path) def build_inits(base_dir: str | Path): From b15f28152499e95f6208e6e50fc94fcb96e0947c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 19:42:22 -0700 Subject: [PATCH 078/104] FIX: checkpoint, many things work now. Probably not the unit tests. --- Makefile | 13 ++++++--- pcdswidgets/builder/build.py | 29 +++++++++++++------ pcdswidgets/builder/designer_widget.py | 27 ++++++++++++----- pcdswidgets/builder/entrypoint_finder.py | 2 +- pcdswidgets/builder/ui_main_widget.j2 | 4 +-- .../motor_record_classic_tc_interlock_row.ui | 4 +-- pyproject.toml | 8 ++--- 7 files changed, 58 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index c0d8c57..147a098 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build venv venv-again +.PHONY: all build venv venv-2 venv-3 UI_SOURCE := $(wildcard pcdswidgets/ui/*/*/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) @@ -7,7 +7,8 @@ PY_FORM := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_form.py) PY_BASE := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_base.py) PY_MAIN := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/%.py) -all: venv build pyproject.toml venv-again +# We need to update the venv before and after each of our steps +all: venv build venv-2 pyproject.toml venv-3 build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) @@ -29,6 +30,10 @@ pyproject.toml: $(PY_SOURCE) venv: ./build_local_venv.sh -# For running again after pyproject.toml is regenerated -venv-again: +# For running the second time after the py files are generated +venv-2: + ./build_local_venv.sh + +# For running the third time after pyproject.toml is regenerated +venv-3: ./build_local_venv.sh diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 0322e72..cfb33fc 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -9,6 +9,8 @@ from jinja2 import Environment, PackageLoader from qtpy.uic import compileUi # type: ignore +import pcdswidgets + def build_uic(designer_ui: str, output_dir: str = ""): """ @@ -22,7 +24,7 @@ def build_uic(designer_ui: str, output_dir: str = ""): output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_form.py") with open(output_file, "w") as fd: compileUi(designer_ui, fd) - build_inits(base_dir=output_dir_path) + build_inits(base_dir=up_but_not_top(output_dir_path)) def build_base_widget(designer_ui: str, output_dir: str = ""): @@ -72,7 +74,7 @@ def build_base_widget(designer_ui: str, output_dir: str = ""): output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_base.py") with open(output_file, "w") as fd: fd.write(jinja_output) - build_inits(base_dir=output_dir_path) + build_inits(base_dir=up_but_not_top(output_dir_path)) def build_main_widget(designer_ui: str, output_dir: str = ""): @@ -86,17 +88,16 @@ def build_main_widget(designer_ui: str, output_dir: str = ""): """ # Collect some info designer_path = Path(designer_ui) - module_parts = ["pcdswidgets"] + module_parts = ["pcdswidgets", "generated"] seen_ui = False - for path_part in designer_path.parts: + for path_part in designer_path.parts[:-1]: if path_part == "ui": seen_ui = True elif seen_ui: module_parts.append(path_part) - module_parts.append(os.path.basename(designer_ui).replace(".ui", "base")) + module_parts.append(os.path.basename(designer_ui).replace(".ui", "_base")) absolute_import_path = ".".join(module_parts) - group_parts = module_parts[1:-2] - default_group = f"PCDS {' '.join(group_parts)}" + default_group = f"PCDS {module_parts[2].title()} {module_parts[3].title()}" # Fill the template jinja_template = "ui_main_widget.j2" env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) @@ -112,7 +113,7 @@ def build_main_widget(designer_ui: str, output_dir: str = ""): output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", ".py") with open(output_file, "w") as fd: fd.write(jinja_output) - build_inits(base_dir=output_dir_path) + build_inits(base_dir=up_but_not_top(output_dir_path)) def build_inits(base_dir: str | Path): @@ -124,12 +125,22 @@ def build_inits(base_dir: str | Path): candidates: set[Path] = set() base_dir = Path(base_dir) for path in base_dir.rglob("*"): - if not path.name.startswith(".") and "__pycache__" not in path.parts: + if not str(path).startswith(".") and "__pycache__" not in path.parts: candidates.add(path.with_name("__init__.py")) for cand_path in candidates: cand_path.touch() +def up_but_not_top(base_dir: str | Path): + this_path = Path(base_dir).resolve() + pcdswidgets_base_path = Path(pcdswidgets.__file__).parent + while this_path.parent != pcdswidgets_base_path: + this_path = this_path.parent + if this_path.parent == this_path: + return Path(base_dir) + return this_path + + def get_output_path(designer_ui: str | Path, default_base: str, output_dir: str | Path = "") -> Path: if output_dir: return Path(output_dir) diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index f6298d8..4e78e55 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -11,6 +11,8 @@ from pydm.widgets.qtplugin_extensions import RulesExtension from qtpy.QtWidgets import QAction, QDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget +from .designer_options import DesignerOptions + ifont = IconFont() @@ -25,6 +27,8 @@ class DesignerWidget(QWidget, PyDMPrimitiveWidget): # type: ignore # Loaded from uic ui_form: ClassVar[type[_UiForm]] + # Used to generate _qt_designer_ + designer_options: ClassVar[DesignerOptions] # Tells PyDM to include in designer _qt_designer_: ClassVar[dict[str, Any]] # Macro name to widget names that include that macro @@ -38,7 +42,22 @@ class DesignerWidget(QWidget, PyDMPrimitiveWidget): # type: ignore def __init_subclass__(cls): super().__init_subclass__() - # Extend the _qt_designer_ marker if it exists to include a quick editor for macro vals + # Create _qt_designer_ for pydm if designer_options is present + if hasattr(cls, "designer_options"): + cls._qt_designer_ = { + "group": cls.designer_options.group, + "is_container": cls.designer_options.is_container, + } + icon_name = cls.designer_options.icon + if icon_name is not None: + cls._qt_designer_["icon"] = icon_name + # Interpret strings as icons so we don't have to import IconFont everywhere + try: + if isinstance(cls._qt_designer_["icon"], str): + cls._qt_designer_["icon"] = ifont.icon(cls._qt_designer_["icon"]) + except (AttributeError, KeyError): + ... + # Include a quick editor for macro vals new_ext = [MacroEditExtension, RulesExtension] try: cls._qt_designer_["extensions"].extend(new_ext) @@ -47,12 +66,6 @@ def __init_subclass__(cls): cls._qt_designer_["extensions"] = new_ext except AttributeError: ... - # Interpret strings as icons so we don't have to import IconFont everywhere - try: - if isinstance(cls._qt_designer_["icon"], str): - cls._qt_designer_["icon"] = ifont.icon(cls._qt_designer_["icon"]) - except (AttributeError, KeyError): - ... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pcdswidgets/builder/entrypoint_finder.py b/pcdswidgets/builder/entrypoint_finder.py index f9a3d70..196e9d7 100644 --- a/pcdswidgets/builder/entrypoint_finder.py +++ b/pcdswidgets/builder/entrypoint_finder.py @@ -79,7 +79,7 @@ def iter_submodules(package: str = "pcdswidgets") -> Iterator[ModuleType]: def get_pyproj_path() -> Path: - return Path(pcdswidgets.__file__).parent / "pyproject.toml" + return Path(pcdswidgets.__file__).parent.parent / "pyproject.toml" def get_current_widget_table() -> tuple[tki.Table, tk.TOMLDocument]: diff --git a/pcdswidgets/builder/ui_main_widget.j2 b/pcdswidgets/builder/ui_main_widget.j2 index a2cd87c..4738f85 100644 --- a/pcdswidgets/builder/ui_main_widget.j2 +++ b/pcdswidgets/builder/ui_main_widget.j2 @@ -3,13 +3,13 @@ Originally generated from jinja template ui_main_widget.j2 This file can be safely edited to change the runtime behavior of the widget. """ +from pcdswidgets.builder.designer_options import DesignerOptions from {{ absolute_import_path }} import {{ base_cls }} -from pcdswigets.builder.designer_options import DesignerOptions class {{ main_cls }}({{ base_cls }}): designer_options = DesignerOptions( group="{{ default_group }}", - is_containter=False, + is_container=False, icon=None, ) diff --git a/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui b/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui index 28147e8..b01cdf6 100644 --- a/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui +++ b/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui @@ -66,7 +66,7 @@ 5
- + @@ -251,7 +251,7 @@
pydm.widgets.byte
- MotorRecordRow + MotorRecordClassicRow QWidget
pcdswidgets.motion.common.motor_record_classic_row
diff --git a/pyproject.toml b/pyproject.toml index 6d7af42..a6980a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,9 @@ GetterPump = "pcdswidgets.vacuum.pumps:GetterPump" HotCathodeComboGauge = "pcdswidgets.vacuum.gauges:HotCathodeComboGauge" HotCathodeGauge = "pcdswidgets.vacuum.gauges:HotCathodeGauge" IonPump = "pcdswidgets.vacuum.pumps:IonPump" -MotorRecordFull = "pcdswidgets.motion.motor_record_full:MotorRecordFull" -MotorRecordRow = "pcdswidgets.motion.motor_record_row:MotorRecordRow" -MotorRecordTcInterlockRow = "pcdswidgets.motion.motor_record_tc_interlock_row:MotorRecordTcInterlockRow" +MotorRecordClassicFull = "pcdswidgets.motion.common.motor_record_classic_full:MotorRecordClassicFull" +MotorRecordClassicRow = "pcdswidgets.motion.common.motor_record_classic_row:MotorRecordClassicRow" +MotorRecordClassicTcInterlockRow = "pcdswidgets.motion.common.motor_record_classic_tc_interlock_row:MotorRecordClassicTcInterlockRow" NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" @@ -54,7 +54,7 @@ RGA = "pcdswidgets.vacuum.others:RGA" RightAngleManualValve = "pcdswidgets.vacuum.valves:RightAngleManualValve" RoughGauge = "pcdswidgets.vacuum.gauges:RoughGauge" ScrollPump = "pcdswidgets.vacuum.pumps:ScrollPump" -SmaractOpenLoopRow = "pcdswidgets.motion.smaract_open_loop_row:SmaractOpenLoopRow" +SmaractOpenLoopClassicRow = "pcdswidgets.motion.smaract.smaract_open_loop_classic_row:SmaractOpenLoopClassicRow" TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] From 6b89e362588f0fe871d876a026661ac6b8ac142a Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 8 Apr 2026 19:44:24 -0700 Subject: [PATCH 079/104] FIX: files missed in last commit --- pcdswidgets/builder/designer_options.py | 8 + pcdswidgets/generated/motion/__init__.py | 0 .../generated/motion/common/__init__.py | 0 .../common/motor_record_classic_full_base.py | 139 +++++++++ .../common/motor_record_classic_full_form.py | 282 ++++++++++++++++++ .../common/motor_record_classic_row_base.py | 155 ++++++++++ .../common/motor_record_classic_row_form.py | 275 +++++++++++++++++ ...or_record_classic_tc_interlock_row_base.py | 78 +++++ ...or_record_classic_tc_interlock_row_form.py | 108 +++++++ .../generated/motion/smaract/__init__.py | 0 .../smaract_open_loop_classic_row_base.py | 115 +++++++ .../smaract_open_loop_classic_row_form.py | 182 +++++++++++ pcdswidgets/motion/__init__.py | 0 pcdswidgets/motion/common/__init__.py | 0 .../common/motor_record_classic_full.py | 16 + .../motion/common/motor_record_classic_row.py | 16 + .../motor_record_classic_tc_interlock_row.py | 18 ++ pcdswidgets/motion/smaract/__init__.py | 0 .../smaract/smaract_open_loop_classic_row.py | 16 + pyproject.toml | 2 +- 20 files changed, 1409 insertions(+), 1 deletion(-) create mode 100644 pcdswidgets/builder/designer_options.py create mode 100644 pcdswidgets/generated/motion/__init__.py create mode 100644 pcdswidgets/generated/motion/common/__init__.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_full_base.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_full_form.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_row_base.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_row_form.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py create mode 100644 pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py create mode 100644 pcdswidgets/generated/motion/smaract/__init__.py create mode 100644 pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py create mode 100644 pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py create mode 100644 pcdswidgets/motion/__init__.py create mode 100644 pcdswidgets/motion/common/__init__.py create mode 100644 pcdswidgets/motion/common/motor_record_classic_full.py create mode 100644 pcdswidgets/motion/common/motor_record_classic_row.py create mode 100644 pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py create mode 100644 pcdswidgets/motion/smaract/__init__.py create mode 100644 pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py diff --git a/pcdswidgets/builder/designer_options.py b/pcdswidgets/builder/designer_options.py new file mode 100644 index 0000000..27e5f63 --- /dev/null +++ b/pcdswidgets/builder/designer_options.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class DesignerOptions: + group: str + is_container: bool + icon: str | None diff --git a/pcdswidgets/generated/motion/__init__.py b/pcdswidgets/generated/motion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/generated/motion/common/__init__.py b/pcdswidgets/generated/motion/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py new file mode 100644 index 0000000..74e9c81 --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py @@ -0,0 +1,139 @@ +""" +Generated by jinja from ui_base_widget.j2 with: +ui_name = motor_record_classic_full.ui +form_cls = Ui_Form +base_cls = MotorRecordClassicFullBase +macro_names = ['MOTOR'] +widget_names = ['PyDMByteIndicator_mvn', 'PyDMLabel_egu', 'PyDMLabel_name', 'PyDMLabel_rbv', 'PyDMLineEdit_setpoint', 'PyDMLineEdit_twVal', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand_expert'] +widget_name_to_class = {'Form': 'QWidget', 'gridFrame': 'QFrame', 'PyDMLabel_name': 'PyDMLabel', 'PyDMByteIndicator_mvn': 'PyDMByteIndicator', 'PyDMByteIndicator_lls': 'PyDMByteIndicator', 'PyDMLabel_rbv': 'PyDMLabel', 'PyDMByteIndicator_hls': 'PyDMByteIndicator', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMLineEdit_setpoint': 'PyDMLineEdit', 'PyDMLabel_egu': 'PyDMLabel', 'PyDMPushButton_twkL': 'PyDMPushButton', 'PyDMLineEdit_twVal': 'PyDMLineEdit', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMShellCommand_expert': 'PyDMShellCommand'} + +Other long required variables: +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" + +from pydm.widgets import * +from qtpy.QtWidgets import * + +from pcdswidgets.builder.designer_widget import DesignerWidget + +from .motor_record_classic_full_form import Ui_Form + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class MotorRecordClassicFullBase(DesignerWidget): + PyDMByteIndicator_mvn: "PyDMByteIndicator" + PyDMLabel_egu: "PyDMLabel" + PyDMLabel_name: "PyDMLabel" + PyDMLabel_rbv: "PyDMLabel" + PyDMLineEdit_setpoint: "PyDMLineEdit" + PyDMLineEdit_twVal: "PyDMLineEdit" + PyDMPushButton_stop: "PyDMPushButton" + PyDMPushButton_twkL: "PyDMPushButton" + PyDMPushButton_twkR: "PyDMPushButton" + PyDMShellCommand_expert: "PyDMShellCommand" + + ui_form = Ui_Form + _macro_to_widget = { + "MOTOR": [ + "PyDMLabel_name", + "PyDMByteIndicator_mvn", + "PyDMLabel_rbv", + "PyDMPushButton_stop", + "PyDMLineEdit_setpoint", + "PyDMLabel_egu", + "PyDMPushButton_twkL", + "PyDMLineEdit_twVal", + "PyDMPushButton_twkR", + "PyDMShellCommand_expert", + ], + } + _widget_to_macro = { + "PyDMByteIndicator_mvn": [ + "MOTOR", + ], + "PyDMLabel_egu": [ + "MOTOR", + ], + "PyDMLabel_name": [ + "MOTOR", + ], + "PyDMLabel_rbv": [ + "MOTOR", + ], + "PyDMLineEdit_setpoint": [ + "MOTOR", + ], + "PyDMLineEdit_twVal": [ + "MOTOR", + ], + "PyDMPushButton_stop": [ + "MOTOR", + ], + "PyDMPushButton_twkL": [ + "MOTOR", + ], + "PyDMPushButton_twkR": [ + "MOTOR", + ], + "PyDMShellCommand_expert": [ + "MOTOR", + ], + } + _widget_to_pre_template = { + "PyDMByteIndicator_mvn": [ + ("channel", "ca://${MOTOR}.MOVN"), + ], + "PyDMLabel_egu": [ + ("channel", "ca://${MOTOR}.EGU"), + ], + "PyDMLabel_name": [ + ("channel", "ca://${MOTOR}.DESC"), + ], + "PyDMLabel_rbv": [ + ("channel", "ca://${MOTOR}.RBV"), + ], + "PyDMLineEdit_setpoint": [ + ("channel", "ca://${MOTOR}.VAL"), + ], + "PyDMLineEdit_twVal": [ + ("channel", "ca://${MOTOR}.TWV"), + ], + "PyDMPushButton_stop": [ + ("channel", "ca://${MOTOR}.STOP"), + ], + "PyDMPushButton_twkL": [ + ("channel", "ca://${MOTOR}.TWR"), + ], + "PyDMPushButton_twkR": [ + ("channel", "ca://${MOTOR}.TWF"), + ], + "PyDMShellCommand_expert": [ + ( + "commands", + [ + "motor-expert-screen ${MOTOR}", + ], + ), + ], + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { + "MOTOR": "", + } + + def get_motor(self) -> str: + return self._get_macro("MOTOR") + + def set_motor(self, value: str) -> None: + self._set_macro("MOTOR", value) + + motor = pyqtProperty(str, get_motor, set_motor) diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py new file mode 100644 index 0000000..8aaeb59 --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_full.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 125) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + Form.setMinimumSize(QtCore.QSize(400, 125)) + Form.setMaximumSize(QtCore.QSize(400, 125)) + self.horizontalLayout = QtWidgets.QHBoxLayout(Form) + self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.horizontalLayout.setContentsMargins(1, 1, 0, 1) + self.horizontalLayout.setSpacing(1) + self.horizontalLayout.setObjectName("horizontalLayout") + self.gridFrame = QtWidgets.QFrame(Form) + self.gridFrame.setMinimumSize(QtCore.QSize(0, 0)) + self.gridFrame.setFrameShape(QtWidgets.QFrame.Box) + self.gridFrame.setFrameShadow(QtWidgets.QFrame.Sunken) + self.gridFrame.setObjectName("gridFrame") + self.gridLayout_2 = QtWidgets.QGridLayout(self.gridFrame) + self.gridLayout_2.setContentsMargins(1, -1, -1, -1) + self.gridLayout_2.setSpacing(1) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setContentsMargins(0, 0, -1, -1) + self.horizontalLayout_2.setSpacing(0) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.PyDMLabel_name = PyDMLabel(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_name.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_name.setSizePolicy(sizePolicy) + self.PyDMLabel_name.setMinimumSize(QtCore.QSize(75, 0)) + self.PyDMLabel_name.setMaximumSize(QtCore.QSize(300, 16777215)) + font = QtGui.QFont() + font.setPointSize(12) + font.setBold(True) + font.setWeight(75) + self.PyDMLabel_name.setFont(font) + self.PyDMLabel_name.setToolTip("") + self.PyDMLabel_name.setAlarmSensitiveBorder(False) + self.PyDMLabel_name.setDisplayFormat(PyDMLabel.String) + self.PyDMLabel_name.setObjectName("PyDMLabel_name") + self.horizontalLayout_2.addWidget(self.PyDMLabel_name) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 0, 0, 1, 1) + self.horizontalLayout_6 = QtWidgets.QHBoxLayout() + self.horizontalLayout_6.setContentsMargins(0, -1, -1, -1) + self.horizontalLayout_6.setSpacing(0) + self.horizontalLayout_6.setObjectName("horizontalLayout_6") + self.PyDMByteIndicator_mvn = PyDMByteIndicator(self.gridFrame) + self.PyDMByteIndicator_mvn.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMByteIndicator_mvn.sizePolicy().hasHeightForWidth()) + self.PyDMByteIndicator_mvn.setSizePolicy(sizePolicy) + self.PyDMByteIndicator_mvn.setMinimumSize(QtCore.QSize(25, 0)) + self.PyDMByteIndicator_mvn.setMaximumSize(QtCore.QSize(25, 16777215)) + self.PyDMByteIndicator_mvn.setToolTip("") + self.PyDMByteIndicator_mvn.setAlarmSensitiveBorder(False) + self.PyDMByteIndicator_mvn.setShowLabels(False) + self.PyDMByteIndicator_mvn.setCircles(True) + self.PyDMByteIndicator_mvn.setObjectName("PyDMByteIndicator_mvn") + self.horizontalLayout_6.addWidget(self.PyDMByteIndicator_mvn) + self.gridLayout_2.addLayout(self.horizontalLayout_6, 0, 2, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setContentsMargins(7, 0, 7, -1) + self.horizontalLayout_4.setSpacing(7) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.PyDMByteIndicator_lls = PyDMByteIndicator(self.gridFrame) + self.PyDMByteIndicator_lls.setMaximumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator_lls.setToolTip("") + self.PyDMByteIndicator_lls.setShowLabels(False) + self.PyDMByteIndicator_lls.setObjectName("PyDMByteIndicator_lls") + self.horizontalLayout_4.addWidget(self.PyDMByteIndicator_lls) + self.PyDMLabel_rbv = PyDMLabel(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_rbv.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_rbv.setSizePolicy(sizePolicy) + self.PyDMLabel_rbv.setMinimumSize(QtCore.QSize(150, 0)) + self.PyDMLabel_rbv.setMaximumSize(QtCore.QSize(300, 16777215)) + font = QtGui.QFont() + font.setPointSize(14) + font.setBold(False) + font.setWeight(50) + self.PyDMLabel_rbv.setFont(font) + self.PyDMLabel_rbv.setToolTip("") + self.PyDMLabel_rbv.setAlignment(QtCore.Qt.AlignCenter) + self.PyDMLabel_rbv.setDisplayFormat(PyDMLabel.Decimal) + self.PyDMLabel_rbv.setObjectName("PyDMLabel_rbv") + self.horizontalLayout_4.addWidget(self.PyDMLabel_rbv) + self.PyDMByteIndicator_hls = PyDMByteIndicator(self.gridFrame) + self.PyDMByteIndicator_hls.setMaximumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator_hls.setToolTip("") + self.PyDMByteIndicator_hls.setShowLabels(False) + self.PyDMByteIndicator_hls.setObjectName("PyDMByteIndicator_hls") + self.horizontalLayout_4.addWidget(self.PyDMByteIndicator_hls) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 0, 1, 1, 1) + self.horizontalLayout_7 = QtWidgets.QHBoxLayout() + self.horizontalLayout_7.setObjectName("horizontalLayout_7") + self.PyDMPushButton_stop = PyDMPushButton(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMPushButton_stop.sizePolicy().hasHeightForWidth()) + self.PyDMPushButton_stop.setSizePolicy(sizePolicy) + self.PyDMPushButton_stop.setMinimumSize(QtCore.QSize(50, 0)) + self.PyDMPushButton_stop.setMaximumSize(QtCore.QSize(60, 16777215)) + self.PyDMPushButton_stop.setToolTip("") + self.PyDMPushButton_stop.setStyleSheet("background-color: rgb(170, 0, 0);") + self.PyDMPushButton_stop.setObjectName("PyDMPushButton_stop") + self.horizontalLayout_7.addWidget(self.PyDMPushButton_stop) + self.gridLayout_2.addLayout(self.horizontalLayout_7, 1, 2, 1, 1) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout() + self.horizontalLayout_5.setContentsMargins(30, -1, 30, -1) + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.PyDMLineEdit_setpoint = PyDMLineEdit(self.gridFrame) + self.PyDMLineEdit_setpoint.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLineEdit_setpoint.sizePolicy().hasHeightForWidth()) + self.PyDMLineEdit_setpoint.setSizePolicy(sizePolicy) + self.PyDMLineEdit_setpoint.setMinimumSize(QtCore.QSize(150, 0)) + self.PyDMLineEdit_setpoint.setMaximumSize(QtCore.QSize(300, 16777215)) + font = QtGui.QFont() + font.setPointSize(12) + self.PyDMLineEdit_setpoint.setFont(font) + self.PyDMLineEdit_setpoint.setToolTip("") + self.PyDMLineEdit_setpoint.setMaxLength(12) + self.PyDMLineEdit_setpoint.setAlignment(QtCore.Qt.AlignCenter) + self.PyDMLineEdit_setpoint.setPrecision(0) + self.PyDMLineEdit_setpoint.setPrecisionFromPV(True) + self.PyDMLineEdit_setpoint.setAlarmSensitiveBorder(False) + self.PyDMLineEdit_setpoint.setDisplayFormat(PyDMLineEdit.Decimal) + self.PyDMLineEdit_setpoint.setObjectName("PyDMLineEdit_setpoint") + self.horizontalLayout_5.addWidget(self.PyDMLineEdit_setpoint) + self.gridLayout_2.addLayout(self.horizontalLayout_5, 1, 1, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setContentsMargins(0, 0, -1, -1) + self.horizontalLayout_3.setSpacing(7) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.PyDMLabel_egu = PyDMLabel(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_egu.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_egu.setSizePolicy(sizePolicy) + self.PyDMLabel_egu.setMinimumSize(QtCore.QSize(60, 0)) + self.PyDMLabel_egu.setMaximumSize(QtCore.QSize(150, 16777215)) + font = QtGui.QFont() + font.setPointSize(10) + self.PyDMLabel_egu.setFont(font) + self.PyDMLabel_egu.setToolTip("") + self.PyDMLabel_egu.setPrecision(0) + self.PyDMLabel_egu.setShowUnits(False) + self.PyDMLabel_egu.setPrecisionFromPV(True) + self.PyDMLabel_egu.setAlarmSensitiveContent(False) + self.PyDMLabel_egu.setAlarmSensitiveBorder(False) + self.PyDMLabel_egu.setPyDMToolTip("") + self.PyDMLabel_egu.setObjectName("PyDMLabel_egu") + self.horizontalLayout_3.addWidget(self.PyDMLabel_egu) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 1, 0, 1, 1) + self.horizontalLayout_8 = QtWidgets.QHBoxLayout() + self.horizontalLayout_8.setObjectName("horizontalLayout_8") + self.gridLayout_2.addLayout(self.horizontalLayout_8, 2, 0, 1, 1) + self.horizontalLayout_9 = QtWidgets.QHBoxLayout() + self.horizontalLayout_9.setContentsMargins(5, -1, 5, -1) + self.horizontalLayout_9.setSpacing(3) + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.PyDMPushButton_twkL = PyDMPushButton(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMPushButton_twkL.sizePolicy().hasHeightForWidth()) + self.PyDMPushButton_twkL.setSizePolicy(sizePolicy) + self.PyDMPushButton_twkL.setMinimumSize(QtCore.QSize(35, 0)) + self.PyDMPushButton_twkL.setMaximumSize(QtCore.QSize(35, 16777215)) + self.PyDMPushButton_twkL.setToolTip("") + self.PyDMPushButton_twkL.setRelativeChange(True) + self.PyDMPushButton_twkL.setObjectName("PyDMPushButton_twkL") + self.horizontalLayout_9.addWidget(self.PyDMPushButton_twkL) + self.PyDMLineEdit_twVal = PyDMLineEdit(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLineEdit_twVal.sizePolicy().hasHeightForWidth()) + self.PyDMLineEdit_twVal.setSizePolicy(sizePolicy) + self.PyDMLineEdit_twVal.setMinimumSize(QtCore.QSize(70, 0)) + self.PyDMLineEdit_twVal.setMaximumSize(QtCore.QSize(200, 16777215)) + font = QtGui.QFont() + font.setPointSize(12) + self.PyDMLineEdit_twVal.setFont(font) + self.PyDMLineEdit_twVal.setToolTip("") + self.PyDMLineEdit_twVal.setAlignment(QtCore.Qt.AlignCenter) + self.PyDMLineEdit_twVal.setPrecision(0) + self.PyDMLineEdit_twVal.setPrecisionFromPV(True) + self.PyDMLineEdit_twVal.setAlarmSensitiveBorder(False) + self.PyDMLineEdit_twVal.setObjectName("PyDMLineEdit_twVal") + self.horizontalLayout_9.addWidget(self.PyDMLineEdit_twVal) + self.PyDMPushButton_twkR = PyDMPushButton(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMPushButton_twkR.sizePolicy().hasHeightForWidth()) + self.PyDMPushButton_twkR.setSizePolicy(sizePolicy) + self.PyDMPushButton_twkR.setMinimumSize(QtCore.QSize(35, 0)) + self.PyDMPushButton_twkR.setMaximumSize(QtCore.QSize(35, 16777215)) + self.PyDMPushButton_twkR.setToolTip("") + self.PyDMPushButton_twkR.setRelativeChange(True) + self.PyDMPushButton_twkR.setObjectName("PyDMPushButton_twkR") + self.horizontalLayout_9.addWidget(self.PyDMPushButton_twkR) + self.gridLayout_2.addLayout(self.horizontalLayout_9, 2, 1, 1, 1) + self.horizontalLayout_10 = QtWidgets.QHBoxLayout() + self.horizontalLayout_10.setObjectName("horizontalLayout_10") + self.PyDMShellCommand_expert = PyDMShellCommand(self.gridFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMShellCommand_expert.sizePolicy().hasHeightForWidth()) + self.PyDMShellCommand_expert.setSizePolicy(sizePolicy) + self.PyDMShellCommand_expert.setMinimumSize(QtCore.QSize(30, 0)) + self.PyDMShellCommand_expert.setMaximumSize(QtCore.QSize(60, 16777215)) + self.PyDMShellCommand_expert.setToolTip("") + self.PyDMShellCommand_expert.setCommands(["motor-expert-screen ${MOTOR}"]) + self.PyDMShellCommand_expert.setObjectName("PyDMShellCommand_expert") + self.horizontalLayout_10.addWidget(self.PyDMShellCommand_expert) + self.gridLayout_2.addLayout(self.horizontalLayout_10, 2, 2, 1, 1) + self.gridLayout_2.setColumnStretch(0, 4) + self.gridLayout_2.setColumnStretch(1, 6) + self.gridLayout_2.setColumnStretch(2, 1) + self.gridLayout_2.setRowStretch(0, 3) + self.gridLayout_2.setRowStretch(1, 2) + self.gridLayout_2.setRowStretch(2, 2) + self.horizontalLayout.addWidget(self.gridFrame) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.PyDMLabel_name.setChannel(_translate("Form", "ca://${MOTOR}.DESC")) + self.PyDMByteIndicator_mvn.setChannel(_translate("Form", "ca://${MOTOR}.MOVN")) + self.PyDMLabel_rbv.setChannel(_translate("Form", "ca://${MOTOR}.RBV")) + self.PyDMPushButton_stop.setText(_translate("Form", "Stop")) + self.PyDMPushButton_stop.setChannel(_translate("Form", "ca://${MOTOR}.STOP")) + self.PyDMPushButton_stop.setPressValue(_translate("Form", "1")) + self.PyDMLineEdit_setpoint.setChannel(_translate("Form", "ca://${MOTOR}.VAL")) + self.PyDMLabel_egu.setChannel(_translate("Form", "ca://${MOTOR}.EGU")) + self.PyDMPushButton_twkL.setText(_translate("Form", "<<")) + self.PyDMPushButton_twkL.setChannel(_translate("Form", "ca://${MOTOR}.TWR")) + self.PyDMPushButton_twkL.setPressValue(_translate("Form", "1")) + self.PyDMLineEdit_twVal.setChannel(_translate("Form", "ca://${MOTOR}.TWV")) + self.PyDMPushButton_twkR.setText(_translate("Form", ">>")) + self.PyDMPushButton_twkR.setChannel(_translate("Form", "ca://${MOTOR}.TWF")) + self.PyDMPushButton_twkR.setPressValue(_translate("Form", "1")) + + +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py new file mode 100644 index 0000000..0ad3817 --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py @@ -0,0 +1,155 @@ +""" +Generated by jinja from ui_base_widget.j2 with: +ui_name = motor_record_classic_row.ui +form_cls = Ui_Form +base_cls = MotorRecordClassicRowBase +macro_names = ['MOTOR'] +widget_names = ['PyDMByteIndicator', 'PyDMByteIndicator_2', 'PyDMByteIndicator_mvn', 'PyDMLabel', 'PyDMLabel_name', 'PyDMLabel_rbv', 'PyDMLineEdit', 'PyDMLineEdit_setpoint', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand'] +widget_name_to_class = {'Form': 'QWidget', 'main_frame': 'QFrame', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMByteIndicator_2': 'PyDMByteIndicator', 'PyDMLabel_name': 'PyDMLabel', 'PyDMLabel': 'PyDMLabel', 'PyDMShellCommand': 'PyDMShellCommand', 'PyDMLineEdit': 'PyDMLineEdit', 'PyDMByteIndicator': 'PyDMByteIndicator', 'PyDMLabel_rbv': 'PyDMLabel', 'PyDMByteIndicator_mvn': 'PyDMByteIndicator', 'PyDMPushButton_twkL': 'PyDMPushButton', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMLineEdit_setpoint': 'PyDMLineEdit'} + +Other long required variables: +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" + +from pydm.widgets import * +from qtpy.QtWidgets import * + +from pcdswidgets.builder.designer_widget import DesignerWidget + +from .motor_record_classic_row_form import Ui_Form + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class MotorRecordClassicRowBase(DesignerWidget): + PyDMByteIndicator: "PyDMByteIndicator" + PyDMByteIndicator_2: "PyDMByteIndicator" + PyDMByteIndicator_mvn: "PyDMByteIndicator" + PyDMLabel: "PyDMLabel" + PyDMLabel_name: "PyDMLabel" + PyDMLabel_rbv: "PyDMLabel" + PyDMLineEdit: "PyDMLineEdit" + PyDMLineEdit_setpoint: "PyDMLineEdit" + PyDMPushButton_stop: "PyDMPushButton" + PyDMPushButton_twkL: "PyDMPushButton" + PyDMPushButton_twkR: "PyDMPushButton" + PyDMShellCommand: "PyDMShellCommand" + + ui_form = Ui_Form + _macro_to_widget = { + "MOTOR": [ + "PyDMPushButton_twkR", + "PyDMByteIndicator_2", + "PyDMLabel_name", + "PyDMLabel", + "PyDMShellCommand", + "PyDMLineEdit", + "PyDMByteIndicator", + "PyDMLabel_rbv", + "PyDMByteIndicator_mvn", + "PyDMPushButton_twkL", + "PyDMPushButton_stop", + "PyDMLineEdit_setpoint", + ], + } + _widget_to_macro = { + "PyDMByteIndicator": [ + "MOTOR", + ], + "PyDMByteIndicator_2": [ + "MOTOR", + ], + "PyDMByteIndicator_mvn": [ + "MOTOR", + ], + "PyDMLabel": [ + "MOTOR", + ], + "PyDMLabel_name": [ + "MOTOR", + ], + "PyDMLabel_rbv": [ + "MOTOR", + ], + "PyDMLineEdit": [ + "MOTOR", + ], + "PyDMLineEdit_setpoint": [ + "MOTOR", + ], + "PyDMPushButton_stop": [ + "MOTOR", + ], + "PyDMPushButton_twkL": [ + "MOTOR", + ], + "PyDMPushButton_twkR": [ + "MOTOR", + ], + "PyDMShellCommand": [ + "MOTOR", + ], + } + _widget_to_pre_template = { + "PyDMByteIndicator": [ + ("channel", "ca://${MOTOR}.LLS"), + ], + "PyDMByteIndicator_2": [ + ("channel", "ca://${MOTOR}.HLS"), + ], + "PyDMByteIndicator_mvn": [ + ("channel", "ca://${MOTOR}.MOVN"), + ], + "PyDMLabel": [ + ("channel", "ca://${MOTOR}.EGU"), + ], + "PyDMLabel_name": [ + ("channel", "ca://${MOTOR}.DESC"), + ], + "PyDMLabel_rbv": [ + ("channel", "ca://${MOTOR}.RBV"), + ], + "PyDMLineEdit": [ + ("channel", "ca://${MOTOR}.TWV"), + ], + "PyDMLineEdit_setpoint": [ + ("channel", "ca://${MOTOR}.VAL"), + ], + "PyDMPushButton_stop": [ + ("channel", "ca://${MOTOR}.STOP"), + ], + "PyDMPushButton_twkL": [ + ("channel", "ca://${MOTOR}.TWR"), + ], + "PyDMPushButton_twkR": [ + ("channel", "ca://${MOTOR}.TWF"), + ], + "PyDMShellCommand": [ + ( + "commands", + [ + "motor-expert-screen ${MOTOR}", + ], + ), + ], + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { + "MOTOR": "", + } + + def get_motor(self) -> str: + return self._get_macro("MOTOR") + + def set_motor(self, value: str) -> None: + self._set_macro("MOTOR", value) + + motor = pyqtProperty(str, get_motor, set_motor) diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py new file mode 100644 index 0000000..b693e08 --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_row.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(753, 45) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + Form.setMinimumSize(QtCore.QSize(700, 45)) + Form.setMaximumSize(QtCore.QSize(800, 50)) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + self.main_frame = QtWidgets.QFrame(Form) + font = QtGui.QFont() + font.setPointSize(11) + self.main_frame.setFont(font) + self.main_frame.setFrameShape(QtWidgets.QFrame.Box) + self.main_frame.setFrameShadow(QtWidgets.QFrame.Sunken) + self.main_frame.setObjectName("main_frame") + self.gridLayout = QtWidgets.QGridLayout(self.main_frame) + self.gridLayout.setContentsMargins(1, 5, 5, 5) + self.gridLayout.setSpacing(5) + self.gridLayout.setObjectName("gridLayout") + self.PyDMPushButton_twkR = PyDMPushButton(self.main_frame) + self.PyDMPushButton_twkR.setMinimumSize(QtCore.QSize(35, 0)) + self.PyDMPushButton_twkR.setToolTip("") + self.PyDMPushButton_twkR.setAlarmSensitiveContent(False) + self.PyDMPushButton_twkR.setAlarmSensitiveBorder(False) + self.PyDMPushButton_twkR.setPyDMToolTip("") + self.PyDMPushButton_twkR.setPasswordProtected(False) + self.PyDMPushButton_twkR.setPassword("") + self.PyDMPushButton_twkR.setProtectedPassword("") + self.PyDMPushButton_twkR.setShowConfirmDialog(False) + self.PyDMPushButton_twkR.setRelativeChange(False) + self.PyDMPushButton_twkR.setWriteWhenRelease(False) + self.PyDMPushButton_twkR.setObjectName("PyDMPushButton_twkR") + self.gridLayout.addWidget(self.PyDMPushButton_twkR, 0, 10, 1, 1) + self.PyDMByteIndicator_2 = PyDMByteIndicator(self.main_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMByteIndicator_2.sizePolicy().hasHeightForWidth()) + self.PyDMByteIndicator_2.setSizePolicy(sizePolicy) + self.PyDMByteIndicator_2.setMaximumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator_2.setToolTip("") + self.PyDMByteIndicator_2.setAlarmSensitiveContent(False) + self.PyDMByteIndicator_2.setAlarmSensitiveBorder(True) + self.PyDMByteIndicator_2.setPyDMToolTip("") + self.PyDMByteIndicator_2.setOnColor(QtGui.QColor(255, 165, 0)) + self.PyDMByteIndicator_2.setShowLabels(False) + self.PyDMByteIndicator_2.setBigEndian(False) + self.PyDMByteIndicator_2.setCircles(False) + self.PyDMByteIndicator_2.setNumBits(1) + self.PyDMByteIndicator_2.setLabels(["Bit 0"]) + self.PyDMByteIndicator_2.setObjectName("PyDMByteIndicator_2") + self.gridLayout.addWidget(self.PyDMByteIndicator_2, 0, 5, 1, 1) + self.PyDMLabel_name = PyDMLabel(self.main_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_name.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_name.setSizePolicy(sizePolicy) + self.PyDMLabel_name.setMinimumSize(QtCore.QSize(125, 0)) + self.PyDMLabel_name.setMaximumSize(QtCore.QSize(125, 16777215)) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.PyDMLabel_name.setFont(font) + self.PyDMLabel_name.setToolTip("") + self.PyDMLabel_name.setAlarmSensitiveBorder(False) + self.PyDMLabel_name.setDisplayFormat(PyDMLabel.String) + self.PyDMLabel_name.setObjectName("PyDMLabel_name") + self.gridLayout.addWidget(self.PyDMLabel_name, 0, 0, 1, 1) + self.PyDMLabel = PyDMLabel(self.main_frame) + self.PyDMLabel.setMinimumSize(QtCore.QSize(75, 0)) + self.PyDMLabel.setMaximumSize(QtCore.QSize(75, 16777215)) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(False) + font.setWeight(50) + self.PyDMLabel.setFont(font) + self.PyDMLabel.setToolTip("") + self.PyDMLabel.setPrecision(0) + self.PyDMLabel.setShowUnits(False) + self.PyDMLabel.setPrecisionFromPV(True) + self.PyDMLabel.setAlarmSensitiveContent(False) + self.PyDMLabel.setAlarmSensitiveBorder(False) + self.PyDMLabel.setObjectName("PyDMLabel") + self.gridLayout.addWidget(self.PyDMLabel, 0, 1, 1, 1) + self.PyDMShellCommand = PyDMShellCommand(self.main_frame) + self.PyDMShellCommand.setToolTip("") + self.PyDMShellCommand.setAlarmSensitiveBorder(False) + self.PyDMShellCommand.setCommands(["motor-expert-screen ${MOTOR}"]) + self.PyDMShellCommand.setObjectName("PyDMShellCommand") + self.gridLayout.addWidget(self.PyDMShellCommand, 0, 13, 1, 1) + self.PyDMLineEdit = PyDMLineEdit(self.main_frame) + self.PyDMLineEdit.setMinimumSize(QtCore.QSize(60, 0)) + self.PyDMLineEdit.setMaximumSize(QtCore.QSize(150, 16777215)) + font = QtGui.QFont() + font.setPointSize(11) + self.PyDMLineEdit.setFont(font) + self.PyDMLineEdit.setToolTip("") + self.PyDMLineEdit.setPrecision(0) + self.PyDMLineEdit.setShowUnits(False) + self.PyDMLineEdit.setPrecisionFromPV(True) + self.PyDMLineEdit.setAlarmSensitiveContent(False) + self.PyDMLineEdit.setAlarmSensitiveBorder(False) + self.PyDMLineEdit.setObjectName("PyDMLineEdit") + self.gridLayout.addWidget(self.PyDMLineEdit, 0, 9, 1, 1) + self.PyDMByteIndicator = PyDMByteIndicator(self.main_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMByteIndicator.sizePolicy().hasHeightForWidth()) + self.PyDMByteIndicator.setSizePolicy(sizePolicy) + self.PyDMByteIndicator.setMaximumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator.setToolTip("") + self.PyDMByteIndicator.setAlarmSensitiveContent(False) + self.PyDMByteIndicator.setAlarmSensitiveBorder(True) + self.PyDMByteIndicator.setPyDMToolTip("") + self.PyDMByteIndicator.setOnColor(QtGui.QColor(255, 165, 0)) + self.PyDMByteIndicator.setShowLabels(False) + self.PyDMByteIndicator.setBigEndian(False) + self.PyDMByteIndicator.setCircles(False) + self.PyDMByteIndicator.setNumBits(1) + self.PyDMByteIndicator.setShift(0) + self.PyDMByteIndicator.setLabels(["Bit 0"]) + self.PyDMByteIndicator.setObjectName("PyDMByteIndicator") + self.gridLayout.addWidget(self.PyDMByteIndicator, 0, 3, 1, 1) + self.PyDMLabel_rbv = PyDMLabel(self.main_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_rbv.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_rbv.setSizePolicy(sizePolicy) + self.PyDMLabel_rbv.setMinimumSize(QtCore.QSize(125, 0)) + font = QtGui.QFont() + font.setPointSize(14) + font.setBold(False) + font.setWeight(50) + self.PyDMLabel_rbv.setFont(font) + self.PyDMLabel_rbv.setToolTip("") + self.PyDMLabel_rbv.setAlignment(QtCore.Qt.AlignCenter) + self.PyDMLabel_rbv.setPrecision(0) + self.PyDMLabel_rbv.setShowUnits(False) + self.PyDMLabel_rbv.setPrecisionFromPV(True) + self.PyDMLabel_rbv.setAlarmSensitiveContent(False) + self.PyDMLabel_rbv.setAlarmSensitiveBorder(True) + self.PyDMLabel_rbv.setDisplayFormat(PyDMLabel.Decimal) + self.PyDMLabel_rbv.setObjectName("PyDMLabel_rbv") + self.gridLayout.addWidget(self.PyDMLabel_rbv, 0, 4, 1, 1) + self.PyDMByteIndicator_mvn = PyDMByteIndicator(self.main_frame) + self.PyDMByteIndicator_mvn.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMByteIndicator_mvn.sizePolicy().hasHeightForWidth()) + self.PyDMByteIndicator_mvn.setSizePolicy(sizePolicy) + self.PyDMByteIndicator_mvn.setMinimumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator_mvn.setMaximumSize(QtCore.QSize(15, 15)) + self.PyDMByteIndicator_mvn.setToolTip("") + self.PyDMByteIndicator_mvn.setAlarmSensitiveContent(False) + self.PyDMByteIndicator_mvn.setAlarmSensitiveBorder(True) + self.PyDMByteIndicator_mvn.setShowLabels(False) + self.PyDMByteIndicator_mvn.setBigEndian(False) + self.PyDMByteIndicator_mvn.setCircles(True) + self.PyDMByteIndicator_mvn.setNumBits(1) + self.PyDMByteIndicator_mvn.setShift(0) + self.PyDMByteIndicator_mvn.setLabels(["Bit 0"]) + self.PyDMByteIndicator_mvn.setObjectName("PyDMByteIndicator_mvn") + self.gridLayout.addWidget(self.PyDMByteIndicator_mvn, 0, 7, 1, 1) + self.PyDMPushButton_twkL = PyDMPushButton(self.main_frame) + self.PyDMPushButton_twkL.setMinimumSize(QtCore.QSize(35, 0)) + self.PyDMPushButton_twkL.setToolTip("") + self.PyDMPushButton_twkL.setAlarmSensitiveContent(False) + self.PyDMPushButton_twkL.setAlarmSensitiveBorder(False) + self.PyDMPushButton_twkL.setPyDMToolTip("") + self.PyDMPushButton_twkL.setPasswordProtected(False) + self.PyDMPushButton_twkL.setPassword("") + self.PyDMPushButton_twkL.setProtectedPassword("") + self.PyDMPushButton_twkL.setShowConfirmDialog(False) + self.PyDMPushButton_twkL.setRelativeChange(False) + self.PyDMPushButton_twkL.setWriteWhenRelease(False) + self.PyDMPushButton_twkL.setObjectName("PyDMPushButton_twkL") + self.gridLayout.addWidget(self.PyDMPushButton_twkL, 0, 8, 1, 1) + self.PyDMPushButton_stop = PyDMPushButton(self.main_frame) + self.PyDMPushButton_stop.setMinimumSize(QtCore.QSize(40, 0)) + self.PyDMPushButton_stop.setMaximumSize(QtCore.QSize(40, 16777215)) + self.PyDMPushButton_stop.setToolTip("") + self.PyDMPushButton_stop.setStyleSheet("background-color: rgb(170, 0, 0);") + self.PyDMPushButton_stop.setObjectName("PyDMPushButton_stop") + self.gridLayout.addWidget(self.PyDMPushButton_stop, 0, 12, 1, 1) + self.PyDMLineEdit_setpoint = PyDMLineEdit(self.main_frame) + self.PyDMLineEdit_setpoint.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLineEdit_setpoint.sizePolicy().hasHeightForWidth()) + self.PyDMLineEdit_setpoint.setSizePolicy(sizePolicy) + self.PyDMLineEdit_setpoint.setMinimumSize(QtCore.QSize(75, 0)) + font = QtGui.QFont() + font.setPointSize(11) + self.PyDMLineEdit_setpoint.setFont(font) + self.PyDMLineEdit_setpoint.setToolTip("") + self.PyDMLineEdit_setpoint.setPrecision(0) + self.PyDMLineEdit_setpoint.setShowUnits(False) + self.PyDMLineEdit_setpoint.setPrecisionFromPV(True) + self.PyDMLineEdit_setpoint.setAlarmSensitiveContent(False) + self.PyDMLineEdit_setpoint.setAlarmSensitiveBorder(False) + self.PyDMLineEdit_setpoint.setDisplayFormat(PyDMLineEdit.Default) + self.PyDMLineEdit_setpoint.setObjectName("PyDMLineEdit_setpoint") + self.gridLayout.addWidget(self.PyDMLineEdit_setpoint, 0, 6, 1, 1) + self.gridLayout.setColumnStretch(0, 4) + self.gridLayout.setColumnStretch(1, 1) + self.gridLayout.setColumnStretch(3, 1) + self.gridLayout.setColumnStretch(4, 5) + self.gridLayout.setColumnStretch(6, 3) + self.gridLayout.setColumnStretch(8, 1) + self.gridLayout.setColumnStretch(9, 2) + self.gridLayout.setColumnStretch(10, 1) + self.gridLayout.setColumnStretch(12, 1) + self.gridLayout.setColumnStretch(13, 1) + self.verticalLayout.addWidget(self.main_frame) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.PyDMPushButton_twkR.setText(_translate("Form", ">>")) + self.PyDMPushButton_twkR.setChannel(_translate("Form", "ca://${MOTOR}.TWF")) + self.PyDMPushButton_twkR.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) + self.PyDMPushButton_twkR.setPressValue(_translate("Form", "1")) + self.PyDMPushButton_twkR.setReleaseValue(_translate("Form", "None")) + self.PyDMByteIndicator_2.setChannel(_translate("Form", "ca://${MOTOR}.HLS")) + self.PyDMLabel_name.setChannel(_translate("Form", "ca://${MOTOR}.DESC")) + self.PyDMLabel.setChannel(_translate("Form", "ca://${MOTOR}.EGU")) + self.PyDMLineEdit.setChannel(_translate("Form", "ca://${MOTOR}.TWV")) + self.PyDMByteIndicator.setChannel(_translate("Form", "ca://${MOTOR}.LLS")) + self.PyDMLabel_rbv.setChannel(_translate("Form", "ca://${MOTOR}.RBV")) + self.PyDMByteIndicator_mvn.setChannel(_translate("Form", "ca://${MOTOR}.MOVN")) + self.PyDMPushButton_twkL.setText(_translate("Form", "<<")) + self.PyDMPushButton_twkL.setChannel(_translate("Form", "ca://${MOTOR}.TWR")) + self.PyDMPushButton_twkL.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) + self.PyDMPushButton_twkL.setPressValue(_translate("Form", "1")) + self.PyDMPushButton_twkL.setReleaseValue(_translate("Form", "None")) + self.PyDMPushButton_stop.setText(_translate("Form", "Stop")) + self.PyDMPushButton_stop.setChannel(_translate("Form", "ca://${MOTOR}.STOP")) + self.PyDMPushButton_stop.setPressValue(_translate("Form", "1")) + self.PyDMLineEdit_setpoint.setChannel(_translate("Form", "ca://${MOTOR}.VAL")) + + +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py new file mode 100644 index 0000000..ebf1f1a --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py @@ -0,0 +1,78 @@ +""" +Generated by jinja from ui_base_widget.j2 with: +ui_name = motor_record_classic_tc_interlock_row.ui +form_cls = Ui_Form +base_cls = MotorRecordClassicTcInterlockRowBase +macro_names = ['MOTOR'] +widget_names = ['MotorRecordClassicRow', 'interlock_indicator', 'temperature_label'] +widget_name_to_class = {'Form': 'QWidget', 'frame': 'QFrame', 'MotorRecordClassicRow': 'MotorRecordClassicRow', 'label': 'QLabel', 'temperature_label': 'PyDMLabel', 'label_2': 'QLabel', 'interlock_indicator': 'PyDMByteIndicator'} + +Other long required variables: +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" + +from pydm.widgets import * +from qtpy.QtWidgets import * + +from pcdswidgets.builder.designer_widget import DesignerWidget + +from .motor_record_classic_tc_interlock_row_form import Ui_Form + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class MotorRecordClassicTcInterlockRowBase(DesignerWidget): + MotorRecordClassicRow: "MotorRecordClassicRow" + interlock_indicator: "PyDMByteIndicator" + temperature_label: "PyDMLabel" + + ui_form = Ui_Form + _macro_to_widget = { + "MOTOR": [ + "MotorRecordClassicRow", + "temperature_label", + "interlock_indicator", + ], + } + _widget_to_macro = { + "MotorRecordClassicRow": [ + "MOTOR", + ], + "interlock_indicator": [ + "MOTOR", + ], + "temperature_label": [ + "MOTOR", + ], + } + _widget_to_pre_template = { + "MotorRecordClassicRow": [ + ("motor", "${MOTOR}"), + ], + "interlock_indicator": [ + ("channel", "ca://${MOTOR}:ILOCK:ACTIVE_RBV"), + ], + "temperature_label": [ + ("channel", "ca://${MOTOR}:ILOCK:TC_TEMP_RBV"), + ], + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { + "MOTOR": "", + } + + def get_motor(self) -> str: + return self._get_macro("MOTOR") + + def set_motor(self, value: str) -> None: + self._set_macro("MOTOR", value) + + motor = pyqtProperty(str, get_motor, set_motor) diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py new file mode 100644 index 0000000..7f2e28b --- /dev/null +++ b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(725, 100) + Form.setMinimumSize(QtCore.QSize(725, 100)) + Form.setMaximumSize(QtCore.QSize(800, 100)) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setContentsMargins(5, 5, 5, 5) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(Form) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setContentsMargins(5, 5, 5, 5) + self.verticalLayout_2.setSpacing(0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.MotorRecordClassicRow = MotorRecordClassicRow(self.frame) + self.MotorRecordClassicRow.setToolTip("") + self.MotorRecordClassicRow.setObjectName("MotorRecordClassicRow") + self.verticalLayout_2.addWidget(self.MotorRecordClassicRow) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(6) + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setPointSize(10) + self.label.setFont(font) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.temperature_label = PyDMLabel(self.frame) + self.temperature_label.setMaximumSize(QtCore.QSize(100, 16777215)) + font = QtGui.QFont() + font.setPointSize(10) + self.temperature_label.setFont(font) + self.temperature_label.setToolTip("") + self.temperature_label.setPrecision(1) + self.temperature_label.setShowUnits(False) + self.temperature_label.setPrecisionFromPV(False) + self.temperature_label.setAlarmSensitiveContent(False) + self.temperature_label.setAlarmSensitiveBorder(True) + self.temperature_label.setPyDMToolTip("") + self.temperature_label.setEnableRichText(False) + self.temperature_label.setObjectName("temperature_label") + self.horizontalLayout.addWidget(self.temperature_label) + self.label_2 = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setPointSize(10) + self.label_2.setFont(font) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.interlock_indicator = PyDMByteIndicator(self.frame) + self.interlock_indicator.setMinimumSize(QtCore.QSize(15, 15)) + self.interlock_indicator.setMaximumSize(QtCore.QSize(15, 15)) + font = QtGui.QFont() + font.setPointSize(1) + self.interlock_indicator.setFont(font) + self.interlock_indicator.setToolTip("") + self.interlock_indicator.setAlarmSensitiveContent(False) + self.interlock_indicator.setAlarmSensitiveBorder(True) + self.interlock_indicator.setPyDMToolTip("") + self.interlock_indicator.setOnColor(QtGui.QColor(255, 0, 0)) + self.interlock_indicator.setOffColor(QtGui.QColor(0, 255, 0)) + self.interlock_indicator.setShowLabels(False) + self.interlock_indicator.setBigEndian(False) + self.interlock_indicator.setCircles(False) + self.interlock_indicator.setLabelPosition(QtWidgets.QTabWidget.West) + self.interlock_indicator.setNumBits(1) + self.interlock_indicator.setShift(0) + self.interlock_indicator.setLabels(["Bit 0"]) + self.interlock_indicator.setObjectName("interlock_indicator") + self.horizontalLayout.addWidget(self.interlock_indicator) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.verticalLayout_2.addLayout(self.horizontalLayout) + self.verticalLayout.addWidget(self.frame) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.MotorRecordClassicRow.setProperty("motor", _translate("Form", "${MOTOR}")) + self.label.setText(_translate("Form", "Motor temperature: ")) + self.temperature_label.setChannel(_translate("Form", "ca://${MOTOR}:ILOCK:TC_TEMP_RBV")) + self.label_2.setText(_translate("Form", "Interlock:")) + self.interlock_indicator.setChannel(_translate("Form", "ca://${MOTOR}:ILOCK:ACTIVE_RBV")) + + +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel + +from pcdswidgets.motion.common.motor_record_classic_row import MotorRecordClassicRow diff --git a/pcdswidgets/generated/motion/smaract/__init__.py b/pcdswidgets/generated/motion/smaract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py new file mode 100644 index 0000000..4b15665 --- /dev/null +++ b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py @@ -0,0 +1,115 @@ +""" +Generated by jinja from ui_base_widget.j2 with: +ui_name = smaract_open_loop_classic_row.ui +form_cls = Ui_Form +base_cls = SmaractOpenLoopClassicRowBase +macro_names = ['MOTOR'] +widget_names = ['PyDMLabel_name', 'PyDMLineEdit', 'PyDMLineEdit_setpoint', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand'] +widget_name_to_class = {'Form': 'QWidget', 'PyDMShellCommand': 'PyDMShellCommand', 'PyDMLabel_name': 'PyDMLabel', 'PyDMLineEdit_setpoint': 'PyDMLineEdit', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMLineEdit': 'PyDMLineEdit', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMPushButton_twkL': 'PyDMPushButton', 'label': 'QLabel'} + +Other long required variables: +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" + +from pydm.widgets import * +from qtpy.QtWidgets import * + +from pcdswidgets.builder.designer_widget import DesignerWidget + +from .smaract_open_loop_classic_row_form import Ui_Form + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class SmaractOpenLoopClassicRowBase(DesignerWidget): + PyDMLabel_name: "PyDMLabel" + PyDMLineEdit: "PyDMLineEdit" + PyDMLineEdit_setpoint: "PyDMLineEdit" + PyDMPushButton_stop: "PyDMPushButton" + PyDMPushButton_twkL: "PyDMPushButton" + PyDMPushButton_twkR: "PyDMPushButton" + PyDMShellCommand: "PyDMShellCommand" + + ui_form = Ui_Form + _macro_to_widget = { + "MOTOR": [ + "PyDMShellCommand", + "PyDMLabel_name", + "PyDMLineEdit_setpoint", + "PyDMPushButton_twkR", + "PyDMLineEdit", + "PyDMPushButton_stop", + "PyDMPushButton_twkL", + ], + } + _widget_to_macro = { + "PyDMLabel_name": [ + "MOTOR", + ], + "PyDMLineEdit": [ + "MOTOR", + ], + "PyDMLineEdit_setpoint": [ + "MOTOR", + ], + "PyDMPushButton_stop": [ + "MOTOR", + ], + "PyDMPushButton_twkL": [ + "MOTOR", + ], + "PyDMPushButton_twkR": [ + "MOTOR", + ], + "PyDMShellCommand": [ + "MOTOR", + ], + } + _widget_to_pre_template = { + "PyDMLabel_name": [ + ("channel", "ca://${MOTOR}.DESC"), + ], + "PyDMLineEdit": [ + ("channel", "ca://${MOTOR}:STEP_COUNT"), + ], + "PyDMLineEdit_setpoint": [ + ("channel", "ca://${MOTOR}:TOTAL_STEP_COUNT"), + ], + "PyDMPushButton_stop": [ + ("channel", "ca://${MOTOR}.STOP"), + ], + "PyDMPushButton_twkL": [ + ("channel", "ca://${MOTOR}:STEP_REVERSE.PROC"), + ], + "PyDMPushButton_twkR": [ + ("channel", "ca://${MOTOR}:STEP_FORWARD.PROC"), + ], + "PyDMShellCommand": [ + ( + "commands", + [ + "edm -eolc -x -m MOTOR=${MOTOR} /reg/g/pcds/epics/ioc/common/smaract/R1.0.8/motorScreens/mcs2_openloop.edl", + ], + ), + ], + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { + "MOTOR": "", + } + + def get_motor(self) -> str: + return self._get_macro("MOTOR") + + def set_motor(self, value: str) -> None: + self._set_macro("MOTOR", value) + + motor = pyqtProperty(str, get_motor, set_motor) diff --git a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py new file mode 100644 index 0000000..720a26e --- /dev/null +++ b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/smaract/smaract_open_loop_classic_row.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(640, 40) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + Form.setMinimumSize(QtCore.QSize(640, 40)) + Form.setMaximumSize(QtCore.QSize(800, 45)) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setContentsMargins(0, -1, 5, 5) + self.gridLayout.setSpacing(5) + self.gridLayout.setObjectName("gridLayout") + self.PyDMShellCommand = PyDMShellCommand(Form) + self.PyDMShellCommand.setToolTip("") + self.PyDMShellCommand.setShowIcon(True) + self.PyDMShellCommand.setRedirectCommandOutput(False) + self.PyDMShellCommand.setAllowMultipleExecutions(False) + self.PyDMShellCommand.setTitles([]) + self.PyDMShellCommand.setCommands( + [ + "edm -eolc -x -m MOTOR=${MOTOR} /reg/g/pcds/epics/ioc/common/smaract/R1.0.8/motorScreens/mcs2_openloop.edl" + ] + ) + self.PyDMShellCommand.setObjectName("PyDMShellCommand") + self.gridLayout.addWidget(self.PyDMShellCommand, 0, 8, 1, 1) + self.PyDMLabel_name = PyDMLabel(Form) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLabel_name.sizePolicy().hasHeightForWidth()) + self.PyDMLabel_name.setSizePolicy(sizePolicy) + self.PyDMLabel_name.setMinimumSize(QtCore.QSize(200, 0)) + font = QtGui.QFont() + font.setPointSize(12) + font.setBold(True) + font.setWeight(75) + self.PyDMLabel_name.setFont(font) + self.PyDMLabel_name.setToolTip("") + self.PyDMLabel_name.setPrecision(0) + self.PyDMLabel_name.setShowUnits(False) + self.PyDMLabel_name.setPrecisionFromPV(True) + self.PyDMLabel_name.setAlarmSensitiveContent(False) + self.PyDMLabel_name.setAlarmSensitiveBorder(True) + self.PyDMLabel_name.setDisplayFormat(PyDMLabel.String) + self.PyDMLabel_name.setObjectName("PyDMLabel_name") + self.gridLayout.addWidget(self.PyDMLabel_name, 0, 0, 1, 1) + self.PyDMLineEdit_setpoint = PyDMLineEdit(Form) + self.PyDMLineEdit_setpoint.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PyDMLineEdit_setpoint.sizePolicy().hasHeightForWidth()) + self.PyDMLineEdit_setpoint.setSizePolicy(sizePolicy) + self.PyDMLineEdit_setpoint.setMinimumSize(QtCore.QSize(100, 0)) + self.PyDMLineEdit_setpoint.setMaximumSize(QtCore.QSize(16777215, 16777215)) + font = QtGui.QFont() + font.setPointSize(12) + self.PyDMLineEdit_setpoint.setFont(font) + self.PyDMLineEdit_setpoint.setToolTip("") + self.PyDMLineEdit_setpoint.setMaxLength(12) + self.PyDMLineEdit_setpoint.setPrecision(0) + self.PyDMLineEdit_setpoint.setShowUnits(False) + self.PyDMLineEdit_setpoint.setPrecisionFromPV(True) + self.PyDMLineEdit_setpoint.setAlarmSensitiveContent(False) + self.PyDMLineEdit_setpoint.setAlarmSensitiveBorder(True) + self.PyDMLineEdit_setpoint.setDisplayFormat(PyDMLineEdit.Decimal) + self.PyDMLineEdit_setpoint.setObjectName("PyDMLineEdit_setpoint") + self.gridLayout.addWidget(self.PyDMLineEdit_setpoint, 0, 2, 1, 1) + self.PyDMPushButton_twkR = PyDMPushButton(Form) + self.PyDMPushButton_twkR.setMinimumSize(QtCore.QSize(32, 0)) + self.PyDMPushButton_twkR.setToolTip("") + self.PyDMPushButton_twkR.setAlarmSensitiveContent(False) + self.PyDMPushButton_twkR.setAlarmSensitiveBorder(False) + self.PyDMPushButton_twkR.setPasswordProtected(False) + self.PyDMPushButton_twkR.setPassword("") + self.PyDMPushButton_twkR.setProtectedPassword("") + self.PyDMPushButton_twkR.setShowConfirmDialog(False) + self.PyDMPushButton_twkR.setRelativeChange(True) + self.PyDMPushButton_twkR.setWriteWhenRelease(False) + self.PyDMPushButton_twkR.setObjectName("PyDMPushButton_twkR") + self.gridLayout.addWidget(self.PyDMPushButton_twkR, 0, 6, 1, 1) + self.PyDMLineEdit = PyDMLineEdit(Form) + self.PyDMLineEdit.setMinimumSize(QtCore.QSize(50, 0)) + self.PyDMLineEdit.setMaximumSize(QtCore.QSize(75, 16777215)) + font = QtGui.QFont() + font.setPointSize(12) + self.PyDMLineEdit.setFont(font) + self.PyDMLineEdit.setToolTip("") + self.PyDMLineEdit.setPrecision(0) + self.PyDMLineEdit.setShowUnits(False) + self.PyDMLineEdit.setPrecisionFromPV(True) + self.PyDMLineEdit.setAlarmSensitiveContent(False) + self.PyDMLineEdit.setAlarmSensitiveBorder(False) + self.PyDMLineEdit.setObjectName("PyDMLineEdit") + self.gridLayout.addWidget(self.PyDMLineEdit, 0, 5, 1, 1) + self.PyDMPushButton_stop = PyDMPushButton(Form) + self.PyDMPushButton_stop.setMinimumSize(QtCore.QSize(40, 0)) + self.PyDMPushButton_stop.setMaximumSize(QtCore.QSize(40, 16777215)) + self.PyDMPushButton_stop.setToolTip("") + self.PyDMPushButton_stop.setStyleSheet("background-color: rgb(170, 0, 0);") + self.PyDMPushButton_stop.setAlarmSensitiveContent(False) + self.PyDMPushButton_stop.setAlarmSensitiveBorder(False) + self.PyDMPushButton_stop.setPasswordProtected(False) + self.PyDMPushButton_stop.setPassword("") + self.PyDMPushButton_stop.setProtectedPassword("") + self.PyDMPushButton_stop.setShowConfirmDialog(False) + self.PyDMPushButton_stop.setRelativeChange(False) + self.PyDMPushButton_stop.setWriteWhenRelease(False) + self.PyDMPushButton_stop.setObjectName("PyDMPushButton_stop") + self.gridLayout.addWidget(self.PyDMPushButton_stop, 0, 7, 1, 1) + self.PyDMPushButton_twkL = PyDMPushButton(Form) + self.PyDMPushButton_twkL.setMinimumSize(QtCore.QSize(32, 0)) + self.PyDMPushButton_twkL.setToolTip("") + self.PyDMPushButton_twkL.setAlarmSensitiveContent(False) + self.PyDMPushButton_twkL.setAlarmSensitiveBorder(False) + self.PyDMPushButton_twkL.setPasswordProtected(False) + self.PyDMPushButton_twkL.setPassword("") + self.PyDMPushButton_twkL.setProtectedPassword("") + self.PyDMPushButton_twkL.setShowConfirmDialog(False) + self.PyDMPushButton_twkL.setRelativeChange(True) + self.PyDMPushButton_twkL.setWriteWhenRelease(False) + self.PyDMPushButton_twkL.setObjectName("PyDMPushButton_twkL") + self.gridLayout.addWidget(self.PyDMPushButton_twkL, 0, 4, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setMaximumSize(QtCore.QSize(75, 16777215)) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 1, 1, 1) + self.gridLayout.setColumnStretch(0, 4) + self.gridLayout.setColumnStretch(2, 5) + self.verticalLayout.addLayout(self.gridLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.PyDMLabel_name.setChannel(_translate("Form", "ca://${MOTOR}.DESC")) + self.PyDMLineEdit_setpoint.setChannel(_translate("Form", "ca://${MOTOR}:TOTAL_STEP_COUNT")) + self.PyDMPushButton_twkR.setText(_translate("Form", ">>")) + self.PyDMPushButton_twkR.setChannel(_translate("Form", "ca://${MOTOR}:STEP_FORWARD.PROC")) + self.PyDMPushButton_twkR.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) + self.PyDMPushButton_twkR.setPressValue(_translate("Form", "1")) + self.PyDMPushButton_twkR.setReleaseValue(_translate("Form", "None")) + self.PyDMLineEdit.setChannel(_translate("Form", "ca://${MOTOR}:STEP_COUNT")) + self.PyDMPushButton_stop.setText(_translate("Form", "Stop")) + self.PyDMPushButton_stop.setChannel(_translate("Form", "ca://${MOTOR}.STOP")) + self.PyDMPushButton_stop.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) + self.PyDMPushButton_stop.setPressValue(_translate("Form", "1")) + self.PyDMPushButton_stop.setReleaseValue(_translate("Form", "None")) + self.PyDMPushButton_twkL.setText(_translate("Form", "<<")) + self.PyDMPushButton_twkL.setChannel(_translate("Form", "ca://${MOTOR}:STEP_REVERSE.PROC")) + self.PyDMPushButton_twkL.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) + self.PyDMPushButton_twkL.setPressValue(_translate("Form", "1")) + self.PyDMPushButton_twkL.setReleaseValue(_translate("Form", "None")) + self.label.setText(_translate("Form", "(steps)")) + + +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pcdswidgets/motion/__init__.py b/pcdswidgets/motion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/motion/common/__init__.py b/pcdswidgets/motion/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/motion/common/motor_record_classic_full.py b/pcdswidgets/motion/common/motor_record_classic_full.py new file mode 100644 index 0000000..4e89f13 --- /dev/null +++ b/pcdswidgets/motion/common/motor_record_classic_full.py @@ -0,0 +1,16 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" + +from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.generated.motion.common.motor_record_classic_full_base import MotorRecordClassicFullBase + + +class MotorRecordClassicFull(MotorRecordClassicFullBase): + designer_options = DesignerOptions( + group="PCDS Motion Common", + is_container=False, + icon=None, + ) diff --git a/pcdswidgets/motion/common/motor_record_classic_row.py b/pcdswidgets/motion/common/motor_record_classic_row.py new file mode 100644 index 0000000..4d5a377 --- /dev/null +++ b/pcdswidgets/motion/common/motor_record_classic_row.py @@ -0,0 +1,16 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" + +from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.generated.motion.common.motor_record_classic_row_base import MotorRecordClassicRowBase + + +class MotorRecordClassicRow(MotorRecordClassicRowBase): + designer_options = DesignerOptions( + group="PCDS Motion Common", + is_container=False, + icon=None, + ) diff --git a/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py b/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py new file mode 100644 index 0000000..5fccedc --- /dev/null +++ b/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py @@ -0,0 +1,18 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" + +from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.generated.motion.common.motor_record_classic_tc_interlock_row_base import ( + MotorRecordClassicTcInterlockRowBase, +) + + +class MotorRecordClassicTcInterlockRow(MotorRecordClassicTcInterlockRowBase): + designer_options = DesignerOptions( + group="PCDS Motion Common", + is_container=False, + icon=None, + ) diff --git a/pcdswidgets/motion/smaract/__init__.py b/pcdswidgets/motion/smaract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py new file mode 100644 index 0000000..31fe1dc --- /dev/null +++ b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py @@ -0,0 +1,16 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" + +from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.generated.motion.smaract.smaract_open_loop_classic_row_base import SmaractOpenLoopClassicRowBase + + +class SmaractOpenLoopClassicRow(SmaractOpenLoopClassicRowBase): + designer_options = DesignerOptions( + group="PCDS Motion Smaract", + is_container=False, + icon=None, + ) diff --git a/pyproject.toml b/pyproject.toml index a6980a0..af76970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ content-type = "text/markdown" [tool.ruff] line-length = 120 -exclude = [".git", "__pycache__", "build", "dist", "*/_version.py", "**/ui/*.py"] +exclude = [".git", "__pycache__", "build", "dist", "*/_version.py", "generated"] [tool.ruff.lint] select = ["C", "E", "F", "W", "B", "I"] From 0ecb99e41b0860d2ccffabd20aa5aa78c7f89446 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 17:41:55 -0700 Subject: [PATCH 080/104] ENH: formatting consistency in builds, small tweaks --- Makefile | 36 +++++++----- pcdswidgets/builder/build.py | 57 ++++++++----------- pcdswidgets/builder/inits.py | 49 ++++++++++++++++ pcdswidgets/builder/ui_base_widget.j2 | 12 ++-- pcdswidgets/builder/ui_main_widget.j2 | 1 + .../common/motor_record_classic_full_base.py | 12 ++-- .../common/motor_record_classic_full_form.py | 20 +++---- .../common/motor_record_classic_row_base.py | 12 ++-- .../common/motor_record_classic_row_form.py | 20 +++---- ...or_record_classic_tc_interlock_row_base.py | 12 ++-- ...or_record_classic_tc_interlock_row_form.py | 16 +++--- .../smaract_open_loop_classic_row_base.py | 12 ++-- .../smaract_open_loop_classic_row_form.py | 18 +++--- pyproject.toml | 5 +- uv.lock | 29 +++++++++- 15 files changed, 192 insertions(+), 119 deletions(-) create mode 100644 pcdswidgets/builder/inits.py diff --git a/Makefile b/Makefile index 147a098..42721fa 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build venv venv-2 venv-3 +.PHONY: all build inits venv venv-again UI_SOURCE := $(wildcard pcdswidgets/ui/*/*/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) @@ -7,33 +7,43 @@ PY_FORM := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_form.py) PY_BASE := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/generated/%_base.py) PY_MAIN := $(UI_SOURCE:pcdswidgets/ui/%.ui=pcdswidgets/%.py) +BIN := .venv/bin +BUILD_CMD := $(BIN)/python -m pcdswidgets.builder.build +CHECK_FIX := $(BIN)/ruff check --exit-zero --fix --quiet +FORMAT := $(BIN)/ruff format --quiet + # We need to update the venv before and after each of our steps -all: venv build venv-2 pyproject.toml venv-3 +all: venv build pyproject.toml venv-again -build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) +build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) inits # Need to re-run form and base if the ui file is updated $(PY_FORM): pcdswidgets/generated/%_form.py: pcdswidgets/ui/%.ui - .venv/bin/python -m pcdswidgets.builder.build uic $^ + $(BUILD_CMD) uic $^ + $(CHECK_FIX) $@ + $(FORMAT) $@ $(PY_BASE): pcdswidgets/generated/%_base.py: pcdswidgets/ui/%.ui - .venv/bin/python -m pcdswidgets.builder.build base $^ + $(BUILD_CMD) base $^ + $(CHECK_FIX) $@ + $(FORMAT) $@ # Only run if the target is missing: user can edit these $(PY_MAIN): - .venv/bin/python -m pcdswidgets.builder.build main $(@:pcdswidgets/%.py=pcdswidgets/ui/%.ui) + $(BUILD_CMD) main $(@:pcdswidgets/%.py=pcdswidgets/ui/%.ui) + $(CHECK_FIX) $@ + $(FORMAT) $@ + +inits: + $(BIN)/python -m pcdswidgets.builder.inits # Rerun if any python file is updated pyproject.toml: $(PY_SOURCE) - .venv/bin/python -m pcdswidgets.builder.entrypoint_finder + $(BIN)/python -m pcdswidgets.builder.entrypoint_finder venv: ./build_local_venv.sh -# For running the second time after the py files are generated -venv-2: - ./build_local_venv.sh - -# For running the third time after pyproject.toml is regenerated -venv-3: +# For running the again after pyproject.toml is generated +venv-again: ./build_local_venv.sh diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index cfb33fc..8e68207 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -4,13 +4,12 @@ import re import xml.etree.ElementTree as ET from collections import defaultdict +from io import StringIO from pathlib import Path from jinja2 import Environment, PackageLoader from qtpy.uic import compileUi # type: ignore -import pcdswidgets - def build_uic(designer_ui: str, output_dir: str = ""): """ @@ -19,12 +18,33 @@ def build_uic(designer_ui: str, output_dir: str = ""): The files are named systematically with patterns like: some_name.ui -> some_name_form.py """ + string_io = StringIO() + compileUi(designer_ui, string_io) + + comment_lines = [] + import_lines = [] + impl_lines = [] + + for line in string_io.getvalue().split("\n"): + if line.startswith("from"): + import_lines.append(line.replace("PyQt5", "qtpy")) + continue + if import_lines: + impl_lines.append(line) + elif line: + comment_lines.append(line) + + comment_lines.append("#") + comment_lines.append("# Augmented by pcdswidgets.builder.build") + comment_lines.append("# ruff: noqa: E501") + output_dir_path = get_output_path(designer_ui=designer_ui, default_base="generated", output_dir=output_dir) output_dir_path.mkdir(parents=True, exist_ok=True) output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_form.py") with open(output_file, "w") as fd: - compileUi(designer_ui, fd) - build_inits(base_dir=up_but_not_top(output_dir_path)) + fd.writelines(cl + "\n" for cl in comment_lines) + fd.writelines(il + "\n" for il in import_lines) + fd.writelines(impl + "\n" for impl in impl_lines) def build_base_widget(designer_ui: str, output_dir: str = ""): @@ -74,7 +94,6 @@ def build_base_widget(designer_ui: str, output_dir: str = ""): output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", "_base.py") with open(output_file, "w") as fd: fd.write(jinja_output) - build_inits(base_dir=up_but_not_top(output_dir_path)) def build_main_widget(designer_ui: str, output_dir: str = ""): @@ -112,33 +131,7 @@ def build_main_widget(designer_ui: str, output_dir: str = ""): output_dir_path.mkdir(parents=True, exist_ok=True) output_file = output_dir_path / os.path.basename(designer_ui).replace(".ui", ".py") with open(output_file, "w") as fd: - fd.write(jinja_output) - build_inits(base_dir=up_but_not_top(output_dir_path)) - - -def build_inits(base_dir: str | Path): - """ - Create blank __init__.py files wherever they are needed in generated directories. - - This makes Python treat these directories as Python modules. - """ - candidates: set[Path] = set() - base_dir = Path(base_dir) - for path in base_dir.rglob("*"): - if not str(path).startswith(".") and "__pycache__" not in path.parts: - candidates.add(path.with_name("__init__.py")) - for cand_path in candidates: - cand_path.touch() - - -def up_but_not_top(base_dir: str | Path): - this_path = Path(base_dir).resolve() - pcdswidgets_base_path = Path(pcdswidgets.__file__).parent - while this_path.parent != pcdswidgets_base_path: - this_path = this_path.parent - if this_path.parent == this_path: - return Path(base_dir) - return this_path + fd.write(jinja_output + "\n") def get_output_path(designer_ui: str | Path, default_base: str, output_dir: str | Path = "") -> Path: diff --git a/pcdswidgets/builder/inits.py b/pcdswidgets/builder/inits.py new file mode 100644 index 0000000..ee9caff --- /dev/null +++ b/pcdswidgets/builder/inits.py @@ -0,0 +1,49 @@ +from pathlib import Path + +import pcdswidgets + + +def main(): + """ + Backfill generated directories with __init__.py to make them python modules. + """ + for base_dir in get_generated_top_dirs(): + build_inits(base_dir=base_dir) + + +def get_generated_top_dirs() -> list[Path]: + """ + Returns the top-level directories that were originally generated. + """ + module_dir = Path(pcdswidgets.__file__).parent + top_dirs = [module_dir / "generated"] + # Other generated directories are those that mirror the ui folder filetree + for path in (module_dir / "ui").glob("*"): + if path.is_dir(): + # There is a generated directory at the top-level of the same name, without "ui" + new_dir_parts = [part for part in path.parts if part != "ui"] + top_dirs.append(path.with_segments(*new_dir_parts)) + return top_dirs + + +def build_inits(base_dir: Path): + """ + Creates blank __init__.py files wherever they are needed in generated directories. + + This makes Python treat these directories as Python modules. + + Parameters + ---------- + base_dir : Path + The directory path that contains generated python files in a nested filetree. + """ + candidates: set[Path] = set() + for path in base_dir.rglob("*"): + if "__pycache__" not in path.parts: + candidates.add(path.with_name("__init__.py")) + for cand_path in candidates: + cand_path.touch() + + +if __name__ == "__main__": + main() diff --git a/pcdswidgets/builder/ui_base_widget.j2 b/pcdswidgets/builder/ui_base_widget.j2 index 6bf6124..1d0a0cb 100644 --- a/pcdswidgets/builder/ui_base_widget.j2 +++ b/pcdswidgets/builder/ui_base_widget.j2 @@ -4,21 +4,21 @@ ui_name = {{ ui_name }} form_cls = {{ form_cls }} base_cls = {{ base_cls }} macro_names = {{ macro_names }} -widget_names = {{ widget_names }} -widget_name_to_class = {{ widget_name_to_class }} Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] macro_to_widget: dict[str, str] widget_to_macro: dict[str, str] widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] """ - -from qtpy.QtWidgets import * -from pydm.widgets import * +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 from pcdswidgets.builder.designer_widget import DesignerWidget -from .{{ ui_name.removesuffix(".ui") }}_form import {{ form_cls }} +from .{{ ui_name.removesuffix(".ui") }}_form import * try: from qtpy.QtCore import pyqtProperty diff --git a/pcdswidgets/builder/ui_main_widget.j2 b/pcdswidgets/builder/ui_main_widget.j2 index 4738f85..6184de0 100644 --- a/pcdswidgets/builder/ui_main_widget.j2 +++ b/pcdswidgets/builder/ui_main_widget.j2 @@ -3,6 +3,7 @@ Originally generated from jinja template ui_main_widget.j2 This file can be safely edited to change the runtime behavior of the widget. """ + from pcdswidgets.builder.designer_options import DesignerOptions from {{ absolute_import_path }} import {{ base_cls }} diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py index 74e9c81..2b5945a 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py @@ -4,22 +4,22 @@ form_cls = Ui_Form base_cls = MotorRecordClassicFullBase macro_names = ['MOTOR'] -widget_names = ['PyDMByteIndicator_mvn', 'PyDMLabel_egu', 'PyDMLabel_name', 'PyDMLabel_rbv', 'PyDMLineEdit_setpoint', 'PyDMLineEdit_twVal', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand_expert'] -widget_name_to_class = {'Form': 'QWidget', 'gridFrame': 'QFrame', 'PyDMLabel_name': 'PyDMLabel', 'PyDMByteIndicator_mvn': 'PyDMByteIndicator', 'PyDMByteIndicator_lls': 'PyDMByteIndicator', 'PyDMLabel_rbv': 'PyDMLabel', 'PyDMByteIndicator_hls': 'PyDMByteIndicator', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMLineEdit_setpoint': 'PyDMLineEdit', 'PyDMLabel_egu': 'PyDMLabel', 'PyDMPushButton_twkL': 'PyDMPushButton', 'PyDMLineEdit_twVal': 'PyDMLineEdit', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMShellCommand_expert': 'PyDMShellCommand'} Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] macro_to_widget: dict[str, str] widget_to_macro: dict[str, str] widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] """ - -from pydm.widgets import * -from qtpy.QtWidgets import * +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_full_form import Ui_Form +from .motor_record_classic_full_form import * try: from qtpy.QtCore import pyqtProperty diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py index 8aaeb59..dbe7d45 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- - # Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_full.ui' # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets +# +# Augmented by pcdswidgets.builder.build +# ruff: noqa: E501 +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand +from qtpy import QtCore, QtGui, QtWidgets class Ui_Form(object): @@ -273,10 +278,3 @@ def retranslateUi(self, Form): self.PyDMPushButton_twkR.setText(_translate("Form", ">>")) self.PyDMPushButton_twkR.setChannel(_translate("Form", "ca://${MOTOR}.TWF")) self.PyDMPushButton_twkR.setPressValue(_translate("Form", "1")) - - -from pydm.widgets.byte import PyDMByteIndicator -from pydm.widgets.label import PyDMLabel -from pydm.widgets.line_edit import PyDMLineEdit -from pydm.widgets.pushbutton import PyDMPushButton -from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py index 0ad3817..d94624d 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py @@ -4,22 +4,22 @@ form_cls = Ui_Form base_cls = MotorRecordClassicRowBase macro_names = ['MOTOR'] -widget_names = ['PyDMByteIndicator', 'PyDMByteIndicator_2', 'PyDMByteIndicator_mvn', 'PyDMLabel', 'PyDMLabel_name', 'PyDMLabel_rbv', 'PyDMLineEdit', 'PyDMLineEdit_setpoint', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand'] -widget_name_to_class = {'Form': 'QWidget', 'main_frame': 'QFrame', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMByteIndicator_2': 'PyDMByteIndicator', 'PyDMLabel_name': 'PyDMLabel', 'PyDMLabel': 'PyDMLabel', 'PyDMShellCommand': 'PyDMShellCommand', 'PyDMLineEdit': 'PyDMLineEdit', 'PyDMByteIndicator': 'PyDMByteIndicator', 'PyDMLabel_rbv': 'PyDMLabel', 'PyDMByteIndicator_mvn': 'PyDMByteIndicator', 'PyDMPushButton_twkL': 'PyDMPushButton', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMLineEdit_setpoint': 'PyDMLineEdit'} Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] macro_to_widget: dict[str, str] widget_to_macro: dict[str, str] widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] """ - -from pydm.widgets import * -from qtpy.QtWidgets import * +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_row_form import Ui_Form +from .motor_record_classic_row_form import * try: from qtpy.QtCore import pyqtProperty diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py index b693e08..942d33e 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- - # Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_row.ui' # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets +# +# Augmented by pcdswidgets.builder.build +# ruff: noqa: E501 +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand +from qtpy import QtCore, QtGui, QtWidgets class Ui_Form(object): @@ -266,10 +271,3 @@ def retranslateUi(self, Form): self.PyDMPushButton_stop.setChannel(_translate("Form", "ca://${MOTOR}.STOP")) self.PyDMPushButton_stop.setPressValue(_translate("Form", "1")) self.PyDMLineEdit_setpoint.setChannel(_translate("Form", "ca://${MOTOR}.VAL")) - - -from pydm.widgets.byte import PyDMByteIndicator -from pydm.widgets.label import PyDMLabel -from pydm.widgets.line_edit import PyDMLineEdit -from pydm.widgets.pushbutton import PyDMPushButton -from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py index ebf1f1a..1484d15 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py @@ -4,22 +4,22 @@ form_cls = Ui_Form base_cls = MotorRecordClassicTcInterlockRowBase macro_names = ['MOTOR'] -widget_names = ['MotorRecordClassicRow', 'interlock_indicator', 'temperature_label'] -widget_name_to_class = {'Form': 'QWidget', 'frame': 'QFrame', 'MotorRecordClassicRow': 'MotorRecordClassicRow', 'label': 'QLabel', 'temperature_label': 'PyDMLabel', 'label_2': 'QLabel', 'interlock_indicator': 'PyDMByteIndicator'} Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] macro_to_widget: dict[str, str] widget_to_macro: dict[str, str] widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] """ - -from pydm.widgets import * -from qtpy.QtWidgets import * +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_tc_interlock_row_form import Ui_Form +from .motor_record_classic_tc_interlock_row_form import * try: from qtpy.QtCore import pyqtProperty diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py index 7f2e28b..ca512ff 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py +++ b/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- - # Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui' # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. +# +# Augmented by pcdswidgets.builder.build +# ruff: noqa: E501 +from pydm.widgets.byte import PyDMByteIndicator +from pydm.widgets.label import PyDMLabel +from qtpy import QtCore, QtGui, QtWidgets - -from PyQt5 import QtCore, QtGui, QtWidgets +from pcdswidgets.motion.common.motor_record_classic_row import MotorRecordClassicRow class Ui_Form(object): @@ -100,9 +104,3 @@ def retranslateUi(self, Form): self.temperature_label.setChannel(_translate("Form", "ca://${MOTOR}:ILOCK:TC_TEMP_RBV")) self.label_2.setText(_translate("Form", "Interlock:")) self.interlock_indicator.setChannel(_translate("Form", "ca://${MOTOR}:ILOCK:ACTIVE_RBV")) - - -from pydm.widgets.byte import PyDMByteIndicator -from pydm.widgets.label import PyDMLabel - -from pcdswidgets.motion.common.motor_record_classic_row import MotorRecordClassicRow diff --git a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py index 4b15665..19438cd 100644 --- a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py +++ b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py @@ -4,22 +4,22 @@ form_cls = Ui_Form base_cls = SmaractOpenLoopClassicRowBase macro_names = ['MOTOR'] -widget_names = ['PyDMLabel_name', 'PyDMLineEdit', 'PyDMLineEdit_setpoint', 'PyDMPushButton_stop', 'PyDMPushButton_twkL', 'PyDMPushButton_twkR', 'PyDMShellCommand'] -widget_name_to_class = {'Form': 'QWidget', 'PyDMShellCommand': 'PyDMShellCommand', 'PyDMLabel_name': 'PyDMLabel', 'PyDMLineEdit_setpoint': 'PyDMLineEdit', 'PyDMPushButton_twkR': 'PyDMPushButton', 'PyDMLineEdit': 'PyDMLineEdit', 'PyDMPushButton_stop': 'PyDMPushButton', 'PyDMPushButton_twkL': 'PyDMPushButton', 'label': 'QLabel'} Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] macro_to_widget: dict[str, str] widget_to_macro: dict[str, str] widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] """ - -from pydm.widgets import * -from qtpy.QtWidgets import * +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 from pcdswidgets.builder.designer_widget import DesignerWidget -from .smaract_open_loop_classic_row_form import Ui_Form +from .smaract_open_loop_classic_row_form import * try: from qtpy.QtCore import pyqtProperty diff --git a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py index 720a26e..fe9aca7 100644 --- a/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py +++ b/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- - # Form implementation generated from reading ui file 'pcdswidgets/ui/motion/smaract/smaract_open_loop_classic_row.ui' # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets +# +# Augmented by pcdswidgets.builder.build +# ruff: noqa: E501 +from pydm.widgets.label import PyDMLabel +from pydm.widgets.line_edit import PyDMLineEdit +from pydm.widgets.pushbutton import PyDMPushButton +from pydm.widgets.shell_command import PyDMShellCommand +from qtpy import QtCore, QtGui, QtWidgets class Ui_Form(object): @@ -174,9 +178,3 @@ def retranslateUi(self, Form): self.PyDMPushButton_twkL.setPressValue(_translate("Form", "1")) self.PyDMPushButton_twkL.setReleaseValue(_translate("Form", "None")) self.label.setText(_translate("Form", "(steps)")) - - -from pydm.widgets.label import PyDMLabel -from pydm.widgets.line_edit import PyDMLineEdit -from pydm.widgets.pushbutton import PyDMPushButton -from pydm.widgets.shell_command import PyDMShellCommand diff --git a/pyproject.toml b/pyproject.toml index af76970..7605110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2", "jinja2>=3", "pyqt5>=5.15.11", "qtpy>=2.4.3"] +requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2" ] [project] classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Python :: 3",] @@ -60,6 +60,7 @@ TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] dev = [ "jinja2>=3", + "ruff>=0.15.9", "tomlkit>=0.14.0", ] doc = [ @@ -87,7 +88,7 @@ content-type = "text/markdown" [tool.ruff] line-length = 120 -exclude = [".git", "__pycache__", "build", "dist", "*/_version.py", "generated"] +exclude = [".git", "__pycache__", "build", "dist", "*/_version.py"] [tool.ruff.lint] select = ["C", "E", "F", "W", "B", "I"] diff --git a/uv.lock b/uv.lock index c8b32f8..e5a62bb 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.12" [options] -exclude-newer = "2026-03-20T22:52:20.814700128Z" +exclude-newer = "2026-04-02T21:24:40.825174043Z" exclude-newer-span = "P7D" [[package]] @@ -329,6 +329,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "jinja2" }, + { name = "ruff" }, { name = "tomlkit" }, ] doc = [ @@ -355,6 +356,7 @@ requires-dist = [ { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.5.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, { name = "qtpy", specifier = ">=2.4.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.9" }, { name = "sphinx", marker = "extra == 'doc'", specifier = ">=9.1.0" }, { name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=3.1.0" }, { name = "sphinxcontrib-jquery", marker = "extra == 'doc'", specifier = ">=4.1" }, @@ -510,6 +512,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] +[[package]] +name = "ruff" +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +] + [[package]] name = "setuptools" version = "82.0.0" From 1652da377308a90dfe618c53db965f20656835e3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 17:46:08 -0700 Subject: [PATCH 081/104] DOC: clarify makefile comment --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 42721fa..824d2e5 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,6 @@ pyproject.toml: $(PY_SOURCE) venv: ./build_local_venv.sh -# For running the again after pyproject.toml is generated +# For running the again after pyproject.toml is generated in make all venv-again: ./build_local_venv.sh From 656dd787de14d809e2577e4e26b7bbd3a4618935 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 18:15:23 -0700 Subject: [PATCH 082/104] MNT: rename all of the widget groups for consistency --- pcdswidgets/builder/build.py | 2 +- pcdswidgets/builder/entrypoint_finder.py | 4 +--- pcdswidgets/eps_byteindicator.py | 2 +- .../common/motor_record_classic_full.py | 2 +- .../motion/common/motor_record_classic_row.py | 2 +- .../motor_record_classic_tc_interlock_row.py | 2 +- .../smaract/smaract_open_loop_classic_row.py | 2 +- pcdswidgets/table.py | 2 +- pcdswidgets/vacuum/base.py | 5 ----- pcdswidgets/vacuum/gauges.py | 12 +++++----- pcdswidgets/vacuum/others.py | 2 +- pcdswidgets/vacuum/pumps.py | 8 +++---- pcdswidgets/vacuum/valves.py | 22 +++++++++---------- 13 files changed, 30 insertions(+), 37 deletions(-) diff --git a/pcdswidgets/builder/build.py b/pcdswidgets/builder/build.py index 8e68207..4c7d0b6 100644 --- a/pcdswidgets/builder/build.py +++ b/pcdswidgets/builder/build.py @@ -116,7 +116,7 @@ def build_main_widget(designer_ui: str, output_dir: str = ""): module_parts.append(path_part) module_parts.append(os.path.basename(designer_ui).replace(".ui", "_base")) absolute_import_path = ".".join(module_parts) - default_group = f"PCDS {module_parts[2].title()} {module_parts[3].title()}" + default_group = f"ECS {module_parts[2].title()} {module_parts[3].title()}" # Fill the template jinja_template = "ui_main_widget.j2" env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) diff --git a/pcdswidgets/builder/entrypoint_finder.py b/pcdswidgets/builder/entrypoint_finder.py index 196e9d7..99d0030 100644 --- a/pcdswidgets/builder/entrypoint_finder.py +++ b/pcdswidgets/builder/entrypoint_finder.py @@ -18,9 +18,7 @@ import pcdswidgets -SKIP_WIDGETS = [ - "PCDSSymbolBase", -] +SKIP_WIDGETS = [] SKIP_MODULES = [ ".tests", diff --git a/pcdswidgets/eps_byteindicator.py b/pcdswidgets/eps_byteindicator.py index 82a98c9..1cf5c19 100644 --- a/pcdswidgets/eps_byteindicator.py +++ b/pcdswidgets/eps_byteindicator.py @@ -37,7 +37,7 @@ class EPSByteIndicator(QWidget): """ _qt_designer_ = { - "group": "PCDS Utilities", + "group": "ECS EPS", "is_container": False, } diff --git a/pcdswidgets/motion/common/motor_record_classic_full.py b/pcdswidgets/motion/common/motor_record_classic_full.py index 4e89f13..04e75ab 100644 --- a/pcdswidgets/motion/common/motor_record_classic_full.py +++ b/pcdswidgets/motion/common/motor_record_classic_full.py @@ -10,7 +10,7 @@ class MotorRecordClassicFull(MotorRecordClassicFullBase): designer_options = DesignerOptions( - group="PCDS Motion Common", + group="ECS Motion Common", is_container=False, icon=None, ) diff --git a/pcdswidgets/motion/common/motor_record_classic_row.py b/pcdswidgets/motion/common/motor_record_classic_row.py index 4d5a377..44c5859 100644 --- a/pcdswidgets/motion/common/motor_record_classic_row.py +++ b/pcdswidgets/motion/common/motor_record_classic_row.py @@ -10,7 +10,7 @@ class MotorRecordClassicRow(MotorRecordClassicRowBase): designer_options = DesignerOptions( - group="PCDS Motion Common", + group="ECS Motion Common", is_container=False, icon=None, ) diff --git a/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py b/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py index 5fccedc..ea7b18d 100644 --- a/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py +++ b/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py @@ -12,7 +12,7 @@ class MotorRecordClassicTcInterlockRow(MotorRecordClassicTcInterlockRowBase): designer_options = DesignerOptions( - group="PCDS Motion Common", + group="ECS Motion Common", is_container=False, icon=None, ) diff --git a/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py index 31fe1dc..28d2a70 100644 --- a/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py +++ b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py @@ -10,7 +10,7 @@ class SmaractOpenLoopClassicRow(SmaractOpenLoopClassicRowBase): designer_options = DesignerOptions( - group="PCDS Motion Smaract", + group="ECS Motion Smaract", is_container=False, icon=None, ) diff --git a/pcdswidgets/table.py b/pcdswidgets/table.py index 8d6d7cd..5e424c7 100644 --- a/pcdswidgets/table.py +++ b/pcdswidgets/table.py @@ -23,7 +23,7 @@ class FilterSortWidgetTable(QtWidgets.QTableWidget): """ _qt_designer_ = { - "group": "PCDS Utilities", + "group": "ECS Containers", "is_container": False, } diff --git a/pcdswidgets/vacuum/base.py b/pcdswidgets/vacuum/base.py index 61c0054..2457562 100644 --- a/pcdswidgets/vacuum/base.py +++ b/pcdswidgets/vacuum/base.py @@ -48,11 +48,6 @@ class PCDSSymbolBase(QWidget, PyDMPrimitiveWidget, ContentLocation): The parent widget for this symbol. """ - _qt_designer_ = { - "group": "PCDS Symbols", - "is_container": False, - } - EXPERT_OPHYD_CLASS = "" Q_ENUMS(ContentLocation) diff --git a/pcdswidgets/vacuum/gauges.py b/pcdswidgets/vacuum/gauges.py index 7601541..ba46d87 100644 --- a/pcdswidgets/vacuum/gauges.py +++ b/pcdswidgets/vacuum/gauges.py @@ -68,7 +68,7 @@ class RoughGauge(StateMixin, LabelControl, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _state_suffix = ":STATE_RBV" @@ -153,7 +153,7 @@ class HotCathodeGauge(ButtonLabelControl, InterlockMixin, StateMixin, PCDSSymbol """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -241,7 +241,7 @@ class ColdCathodeGauge(InterlockMixin, StateMixin, ButtonLabelControl, PCDSSymbo """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -324,7 +324,7 @@ class ColdCathodeComboGauge(StateMixin, LabelControl, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _state_suffix = ":STATE_RBV" @@ -403,7 +403,7 @@ class HotCathodeComboGauge(StateMixin, LabelControl, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _state_suffix = ":STATE_RBV" @@ -482,7 +482,7 @@ class CapacitanceManometerGauge(StateMixin, LabelControl, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Gauges", + "group": "ECS Vacuum Gauges", "is_container": False, } _state_suffix = ":STATE_RBV" diff --git a/pcdswidgets/vacuum/others.py b/pcdswidgets/vacuum/others.py index a3e089c..86ca40d 100644 --- a/pcdswidgets/vacuum/others.py +++ b/pcdswidgets/vacuum/others.py @@ -36,7 +36,7 @@ class RGA(PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Others", + "group": "ECS Vacuum Others", "is_container": False, } NAME = "Residual Gas Analyzer" diff --git a/pcdswidgets/vacuum/pumps.py b/pcdswidgets/vacuum/pumps.py index 8207708..ab3e979 100644 --- a/pcdswidgets/vacuum/pumps.py +++ b/pcdswidgets/vacuum/pumps.py @@ -73,7 +73,7 @@ class IonPump(InterlockMixin, ErrorMixin, StateMixin, ButtonLabelControl, PCDSSy """ _qt_designer_ = { - "group": "PCDS Pumps", + "group": "ECS Vacuum Pumps", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -168,7 +168,7 @@ class TurboPump(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymbo """ _qt_designer_ = { - "group": "PCDS Pumps", + "group": "ECS Vacuum Pumps", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -259,7 +259,7 @@ class ScrollPump(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSymb """ _qt_designer_ = { - "group": "PCDS Pumps", + "group": "ECS Vacuum Pumps", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -317,7 +317,7 @@ class GetterPump(PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Pumps", + "group": "ECS Vacuum Pumps", "is_container": False, } NAME = "Getter Pump" diff --git a/pcdswidgets/vacuum/valves.py b/pcdswidgets/vacuum/valves.py index 96d5a76..b57d136 100644 --- a/pcdswidgets/vacuum/valves.py +++ b/pcdswidgets/vacuum/valves.py @@ -92,7 +92,7 @@ class PneumaticValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDS """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } @@ -192,7 +192,7 @@ class ApertureValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSS """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":OPN_OK_RBV" @@ -285,7 +285,7 @@ class FastShutter(InterlockMixin, ErrorMixin, StateMixin, MultipleButtonControl, """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":OPN_OK_RBV" @@ -373,7 +373,7 @@ class NeedleValve(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -456,7 +456,7 @@ class ProportionalValve(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBas """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":ILK_OK_RBV" @@ -513,7 +513,7 @@ class RightAngleManualValve(PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } NAME = "Right Angle Manual Valve" @@ -619,7 +619,7 @@ class ControlValve(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PCDSSy """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } NAME = "Control Valve with Readback" @@ -715,7 +715,7 @@ class ControlOnlyValveNC(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBa """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } NAME = "Normally Closed Control Valve with No Readback" @@ -809,7 +809,7 @@ class ControlOnlyValveNO(InterlockMixin, StateMixin, ButtonControl, PCDSSymbolBa """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } NAME = "Normally Open Control Valve with No Readback" @@ -903,7 +903,7 @@ class PneumaticValveNO(InterlockMixin, ErrorMixin, StateMixin, ButtonControl, PC """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":CLS_OK_RBV" @@ -1009,7 +1009,7 @@ class PneumaticValveDA(InterlockMixin, ErrorMixin, StateMixin, PCDSSymbolBase): """ _qt_designer_ = { - "group": "PCDS Valves", + "group": "ECS Vacuum Valves", "is_container": False, } _interlock_suffix = ":OPN_OK_RBV" From 4364529099b3ca959b185a804c20570d9dfb8265 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 18:18:05 -0700 Subject: [PATCH 083/104] MNT: no longer require external env variable --- base_env_vars.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_env_vars.sh b/base_env_vars.sh index b032d73..fbc3583 100644 --- a/base_env_vars.sh +++ b/base_env_vars.sh @@ -6,7 +6,7 @@ # Disable GL because it crashes on our servers QT_XCB_GL_INTEGRATION=none # Pick a base env that has designer configured appropriately and built to match python + pyqt versions -BASE_ENV="${PYPS_SITE_TOP}/conda/dev/zlentz/miniforge3/envs/ecs-base-0.0.3" +BASE_ENV="/cds/group/pcds/pyps/conda/dev/zlentz/miniforge3/envs/ecs-base-0.0.3" export QT_XCB_GL_INTEGRATION export BASE_ENV From 6e3d084713561231ef9282abb6035c918757a289 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 18:37:47 -0700 Subject: [PATCH 084/104] DOC: update docs for recent changes --- README.md | 109 +++++++++++------------- pcdswidgets/builder/designer_options.py | 21 ++++- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index c229bae..68c1848 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,15 @@ your new widgets. You can alternatively build your own environment: - pip install -e . + +or + - uv sync Or whatever your favorite method is. -Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS! +Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS. +This is due to complications in build process. ## Adding Widgets @@ -84,9 +88,10 @@ Widgets with ui files, such as the composite widgets, should have parity between Widgets should never be renamed between tags, this will break existing screens. Widgets named before 2026 may break some of these rules because we don't want to rename them. +Renaming a widget is to be avoided whenever possible due to breaking consumers that include the old names in their ui files. -### Adding a Symbol-based Widget +### Adding a Symbol-based Vacuum Widget This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. This will require at least some familiarity with Python and with the structure of this module. @@ -102,94 +107,84 @@ The steps are: - Include your icon as self.icon - Add relevant properties as needed, or inherit them from the existing mixins - include the _qt_designer_ class attribute -3. make, to update pyproject.toml with new widget locations +3. make, to update pyproject.toml and the venv with new widget locations If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets. ### Adding a Composite Widget -This is how you would convert a .ui file with macro substitution that is normally used with PyDMEmbeddedDisplay into a designer widget served from here. +This is how you would convert a .ui file with macro substitution that is normally used with `PyDMEmbeddedDisplay` into a designer widget served from here. Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS! -This is not required, but you would do this to make your widget globally available and easier to add to screens. +This is not required, but you would do this to make your widget globally available, trivially discoverable, and easier to add to screens. +The alternative is to pass your widget around via filepath in `PyDMEmbeddedDisplay`, which works but doesn't have the above advantages. This requires only basic Python knowledge. The steps are: 1. Create a widget as a PyDM screen - - Use qt designer to define the layout (saves a .ui file) - - Use PyDM macros to define user inputs + - Use qt `designer` to define the layout (saves a .ui file) + - Use `PyDM` macros to define user inputs 2. Try it! - - Use PyDMEmbeddedDisplay to include your widget in other screens + - Use `PyDMEmbeddedDisplay` to include your widget in other screens - Iterate, update the widget until you like it. 3. Bring it here - - Copy your .ui file in the pcdswidgets/builder/ui folder. -4. Activate your virtual environment - - `make venv` - - `source .venv/bin/activate` -5. `make` - - This will create two .py files, one with the layouts and one with some scaffolding for macro conversions. -6. Create a widget class - - Look around for examples, e.g. pcdswidgets/motion/motor_record_full.py - - Keep these in separate files to avoid circular import errors from including widgets inside widgets - - Import from the _base module created from your .ui file and subclass - - Note: do not put this in the tests, demo, or ui folders! The tests folder is not scanned for production-level widgets! -7. `make`, again - - This will include your widget in pyproject.toml -8. `make venv`, one last time - - The recommended way to update your testing virtual environment. - -If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets and pydm. - -You can then use: -``` -try_in_designer.sh -``` -To open designer with your new widget -(Which, reminder: only works on rocky9 at LCLS) - - -You can also: -``` -try_in_pydm.sh -``` -To run a pydm screen that has your new widget. + - Create a directory under ui, if needed: the form must be `pcdswidgets/ui/$subsystem/$type` + - Examples of subsystem: motion, vacuum + - Examples of type: common, smaract, beckhoff + - Pick a name for the ui file following the widget naming rules above + - Copy in your .ui file to the correct folder with the new name +4. `make` + - This should have created two python files in `generated`, which are not to be edited by hand. + - It also creates a python file in `pcdswidgets/$subsystem/$type` which can be edited by hand if you'd like to. + - It will also create some number of `__init__.py` files to make the generated filetrees valid Python modules. +5. Try it out + - Run `./try_in_designer.sh` and make a test screen. (Which, reminder: only works on rocky9 at LCLS) + - After you've made a test screen, then do `./try_in_pydm.sh my_screen.ui` for further testing. +6. Pick an icon (optional) + - You can select an icon for your widget to use in designer. See the sections below about designer settings and icons. +7. Make a PR! + - Commit + - Take some screenshots (in designer, and in pydm) + +Some notes: + +- If you edit the ui file, you should `make` again, or your changes will not take effect. +- If you change your mind about which subsystem and type directory you'd like to use, you must manually delete the generated files from the old location. #### Widget Classes -The widget class looks something like: +The widget classes look something like: ``` -from pcdswidgets.builder.ui.some_name_base import SomeNameBase - - -class SomeName(SomeNameBase): - _qt_designer_ = { - "group": "Some Category", - "is_container": False, - } +class MyClassFull(MyClassFullBase): + designer_options = DesignerOptions( + group="ECS Subsystem Type", + is_container=False, + icon=None, + ) ``` If you like, you can extend these classes to add additional python code to use at runtime. #### Icons If you want to set a non-default icon for the designer widget list, you can include a QIcon or a string -in the "icon" key of the `_qt_designer_` variable: +in the "icon" attribute of the `DesignerOptions` dataclass: ``` - _qt_designer_ = { - "group": "Some Category", - "is_container": False, - "icon": "expand-arrows-alt", - } + designer_options = DesignerOptions( + group="ECS Subsystem Type", + is_container=False, + icon="expand-arrows-alt", + ) ``` -If this is a string, we'll convert it to a QIcon using Pydm's IconFont. -This uses a portable version of fontawesome, try running `qta-browser` +If this is a string, we'll convert it to a `QIcon` using `Pydm`'s `IconFont`. +This uses a portable version of `fontawesome`, try running `qta-browser` and look through everything with the `fa5s` prefix to browse options. #### Limitations -- Widgets that contain PyDMEmbeddedWidget are not supported: bootstrap these by turning the contents into widgets themselves. +- Widgets that contain `PyDMEmbeddedWidget` are not supported: bootstrap these by turning the contents into widgets themselves. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. - Only direct QString and QStringList properties are supported. We still need to implement support for item-based QString widgets such as QListWidget. diff --git a/pcdswidgets/builder/designer_options.py b/pcdswidgets/builder/designer_options.py index 27e5f63..c5c9b30 100644 --- a/pcdswidgets/builder/designer_options.py +++ b/pcdswidgets/builder/designer_options.py @@ -1,8 +1,27 @@ from dataclasses import dataclass +from qtpy.QtGui import QIcon + @dataclass class DesignerOptions: + """ + Options for designer. + + Parameters + ---------- + group : str + Widgets with the same group will be grouped together in designer. + is_container : bool + Set this to True if this widget can contain other widgets (via drag and drop), + or False otherwise. + icon : str, icon, or None + The icon to use to represent this widget in the qt sidebar. + If a string, we'll use PyDM's iconfont to pick the matching icon. + If a QIcon, we'll use it as-is. + If None, we'll use the default icon. + """ + group: str is_container: bool - icon: str | None + icon: str | QIcon | None From 5e536b1caddf706f26111276ae9571f61da694e9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 18:47:32 -0700 Subject: [PATCH 085/104] MNT: include 7 day delay for autobuilt envs in this repo --- build_local_venv.sh | 3 ++- pyproject.toml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build_local_venv.sh b/build_local_venv.sh index 24bab92..411c958 100755 --- a/build_local_venv.sh +++ b/build_local_venv.sh @@ -41,7 +41,8 @@ elif [[ "$MODE" == "venv" ]]; then fi source .venv/bin/activate echo "Updating .venv using pip" - pip install -e '.[dev,doc,test]' + PKG_CUTOFF="$(date --date "7 days ago" --iso-8601)" + pip install -e '.[dev,doc,test]' --uploaded-prior-to "${PKG_CUTOFF}" else echo "Unhandled mode ${MODE}" fi diff --git a/pyproject.toml b/pyproject.toml index 7605110..1814978 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,3 +101,6 @@ PYQT5 = true PYSIDE2 = false PYQT6 = false PYSIDE6 = false + +[tool.uv] +exclude-newer = "7 days" From b4c13e9fa45bdb408728b4d72550299a410cfc0c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 19:17:29 -0700 Subject: [PATCH 086/104] BLD: fix venv builds using too old pip and related issues --- build_local_venv.sh | 4 ++++ pyproject.toml | 2 +- uv.lock | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build_local_venv.sh b/build_local_venv.sh index 411c958..5b87db4 100755 --- a/build_local_venv.sh +++ b/build_local_venv.sh @@ -38,6 +38,10 @@ elif [[ "$MODE" == "venv" ]]; then if [[ ! -d ".venv" ]]; then echo "Building new .venv using venv module" "$PYTHON_EXE" -m venv --system-site-packages .venv + PIP_VER="$("${PYTHON_EXE}" -c "import pip; print(pip.__version__)")" + source .venv/bin/activate + echo "Updating pip to match base env" + pip install pip=="${PIP_VER}" fi source .venv/bin/activate echo "Updating .venv using pip" diff --git a/pyproject.toml b/pyproject.toml index 1814978..c59557e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ TurboPump = "pcdswidgets.vacuum.pumps:TurboPump" [project.optional-dependencies] dev = [ "jinja2>=3", - "ruff>=0.15.9", + "ruff>=0.15.8", "tomlkit>=0.14.0", ] doc = [ diff --git a/uv.lock b/uv.lock index e5a62bb..27ef2c3 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.12" [options] -exclude-newer = "2026-04-02T21:24:40.825174043Z" +exclude-newer = "2026-04-03T02:16:30.640878921Z" exclude-newer-span = "P7D" [[package]] @@ -356,7 +356,7 @@ requires-dist = [ { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.5.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, { name = "qtpy", specifier = ">=2.4.3" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.9" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.8" }, { name = "sphinx", marker = "extra == 'doc'", specifier = ">=9.1.0" }, { name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=3.1.0" }, { name = "sphinxcontrib-jquery", marker = "extra == 'doc'", specifier = ">=4.1" }, From fe1229cfcc95ac0f4d7439e924f322ff2064f8f3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Apr 2026 19:20:30 -0700 Subject: [PATCH 087/104] DOC: update readme with one more note about designer widget order --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 68c1848..a27acd0 100644 --- a/README.md +++ b/README.md @@ -188,3 +188,4 @@ and look through everything with the `fa5s` prefix to browse options. - Widgets that contain `PyDMEmbeddedWidget` are not supported: bootstrap these by turning the contents into widgets themselves. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. - Only direct QString and QStringList properties are supported. We still need to implement support for item-based QString widgets such as QListWidget. +- The ordering of the designer widget categories is chaotic. This will require an update to PyDM to resolve. From c7f9b99f58a150a6d5bf004bf54a3f8a65468b18 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Apr 2026 11:23:31 -0700 Subject: [PATCH 088/104] DOC/MNT: update guidance and rename widgets --- README.md | 23 +++++++++++-------- ...ull_base.py => motor_classic_full_base.py} | 8 +++---- ...ull_form.py => motor_classic_full_form.py} | 2 +- ..._row_base.py => motor_classic_row_base.py} | 8 +++---- ..._row_form.py => motor_classic_row_form.py} | 2 +- ...w_base.py => motor_tc_classic_row_base.py} | 16 ++++++------- ...w_form.py => motor_tc_classic_row_form.py} | 14 +++++------ ...d_classic_row.py => motor_classic_full.py} | 4 ++-- ...d_classic_full.py => motor_classic_row.py} | 4 ++-- ...terlock_row.py => motor_tc_classic_row.py} | 6 ++--- ..._classic_full.ui => motor_classic_full.ui} | 0 ...rd_classic_row.ui => motor_classic_row.ui} | 0 ...terlock_row.ui => motor_tc_classic_row.ui} | 6 ++--- pyproject.toml | 6 ++--- 14 files changed, 51 insertions(+), 48 deletions(-) rename pcdswidgets/generated/motion/common/{motor_record_classic_full_base.py => motor_classic_full_base.py} (95%) rename pcdswidgets/generated/motion/common/{motor_record_classic_full_form.py => motor_classic_full_form.py} (99%) rename pcdswidgets/generated/motion/common/{motor_record_classic_row_base.py => motor_classic_row_base.py} (95%) rename pcdswidgets/generated/motion/common/{motor_record_classic_row_form.py => motor_classic_row_form.py} (99%) rename pcdswidgets/generated/motion/common/{motor_record_classic_tc_interlock_row_base.py => motor_tc_classic_row_base.py} (82%) rename pcdswidgets/generated/motion/common/{motor_record_classic_tc_interlock_row_form.py => motor_tc_classic_row_form.py} (90%) rename pcdswidgets/motion/common/{motor_record_classic_row.py => motor_classic_full.py} (68%) rename pcdswidgets/motion/common/{motor_record_classic_full.py => motor_classic_row.py} (67%) rename pcdswidgets/motion/common/{motor_record_classic_tc_interlock_row.py => motor_tc_classic_row.py} (61%) rename pcdswidgets/ui/motion/common/{motor_record_classic_full.ui => motor_classic_full.ui} (100%) rename pcdswidgets/ui/motion/common/{motor_record_classic_row.ui => motor_classic_row.ui} (100%) rename pcdswidgets/ui/motion/common/{motor_record_classic_tc_interlock_row.ui => motor_tc_classic_row.ui} (97%) diff --git a/README.md b/README.md index a27acd0..fbaddc6 100644 --- a/README.md +++ b/README.md @@ -76,19 +76,24 @@ These may have a variety of sizes because we had no standards, and will not be c ### Widget Naming -Device control widgets should be named based on the type of device that they control. -The name should be specific enough to distinguish it from other widgets, but general enough to cover all devices that can be used. -Widgets are named using CamelCase and must end with the size, e.g. `MotorRecordClassicFull`. -Avoid using names that might preclude different styles or versions of the same. For example, `MyDeviceFull` isn't specific enough. Give is a name like `MyDeviceRetroFull` or something like this that is more apt for your use case, so someone else could charitably make `MyDeviceResizableFull` and we can easily differentiate them. +Widget names should contain three parts: -There is no need to end a widget name with "Widget". +- Type of device controlled +- Descriptor word to differentiate this widget from other possible widgets with the same device type and size +- Size class signifier -Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_record_classic_full.ui` for `MotorRecordClassicFull`, as well as the module that contains the widget which should be called `motor_record_classic_full.py`. +Other guidelines: -Widgets should never be renamed between tags, this will break existing screens. +- The name should not be unnecessarily long, but avoid abbreviations. +- The name should use CamelCase to match qt and python class naming conventions. -Widgets named before 2026 may break some of these rules because we don't want to rename them. -Renaming a widget is to be avoided whenever possible due to breaking consumers that include the old names in their ui files. + - For example, the first widget added in 2026 was `MotorClassicFull`, because it controls generic epics record motors, is inspired by the classic EDM style, and has the full size class. + +- If multiple devices are controlled, include them in order of importance, e.g. `MotorTcClassicRow`. +- There is no need to end a widget name with "Widget". Please avoid this. +- Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_classic_full.ui` for `MotorClassicFull`. +- Widgets should never be renamed between tags, this will break existing screens. +- Widgets named before 2026 may break some of these rules because we don't want to rename them. ### Adding a Symbol-based Vacuum Widget diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py b/pcdswidgets/generated/motion/common/motor_classic_full_base.py similarity index 95% rename from pcdswidgets/generated/motion/common/motor_record_classic_full_base.py rename to pcdswidgets/generated/motion/common/motor_classic_full_base.py index 2b5945a..cc11a43 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_full_base.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_base.py @@ -1,8 +1,8 @@ """ Generated by jinja from ui_base_widget.j2 with: -ui_name = motor_record_classic_full.ui +ui_name = motor_classic_full.ui form_cls = Ui_Form -base_cls = MotorRecordClassicFullBase +base_cls = MotorClassicFullBase macro_names = ['MOTOR'] Other long required variables: @@ -19,7 +19,7 @@ from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_full_form import * +from .motor_classic_full_form import * try: from qtpy.QtCore import pyqtProperty @@ -27,7 +27,7 @@ from qtpy.QtCore import Property as pyqtProperty # type: ignore -class MotorRecordClassicFullBase(DesignerWidget): +class MotorClassicFullBase(DesignerWidget): PyDMByteIndicator_mvn: "PyDMByteIndicator" PyDMLabel_egu: "PyDMLabel" PyDMLabel_name: "PyDMLabel" diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py b/pcdswidgets/generated/motion/common/motor_classic_full_form.py similarity index 99% rename from pcdswidgets/generated/motion/common/motor_record_classic_full_form.py rename to pcdswidgets/generated/motion/common/motor_classic_full_form.py index dbe7d45..c09a549 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_full_form.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_form.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_full.ui' +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_classic_full.ui' # # Created by: PyQt5 UI code generator 5.15.9 # diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py b/pcdswidgets/generated/motion/common/motor_classic_row_base.py similarity index 95% rename from pcdswidgets/generated/motion/common/motor_record_classic_row_base.py rename to pcdswidgets/generated/motion/common/motor_classic_row_base.py index d94624d..dec1115 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_row_base.py +++ b/pcdswidgets/generated/motion/common/motor_classic_row_base.py @@ -1,8 +1,8 @@ """ Generated by jinja from ui_base_widget.j2 with: -ui_name = motor_record_classic_row.ui +ui_name = motor_classic_row.ui form_cls = Ui_Form -base_cls = MotorRecordClassicRowBase +base_cls = MotorClassicRowBase macro_names = ['MOTOR'] Other long required variables: @@ -19,7 +19,7 @@ from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_row_form import * +from .motor_classic_row_form import * try: from qtpy.QtCore import pyqtProperty @@ -27,7 +27,7 @@ from qtpy.QtCore import Property as pyqtProperty # type: ignore -class MotorRecordClassicRowBase(DesignerWidget): +class MotorClassicRowBase(DesignerWidget): PyDMByteIndicator: "PyDMByteIndicator" PyDMByteIndicator_2: "PyDMByteIndicator" PyDMByteIndicator_mvn: "PyDMByteIndicator" diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py b/pcdswidgets/generated/motion/common/motor_classic_row_form.py similarity index 99% rename from pcdswidgets/generated/motion/common/motor_record_classic_row_form.py rename to pcdswidgets/generated/motion/common/motor_classic_row_form.py index 942d33e..ee7128e 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_row_form.py +++ b/pcdswidgets/generated/motion/common/motor_classic_row_form.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_row.ui' +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_classic_row.ui' # # Created by: PyQt5 UI code generator 5.15.9 # diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py b/pcdswidgets/generated/motion/common/motor_tc_classic_row_base.py similarity index 82% rename from pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py rename to pcdswidgets/generated/motion/common/motor_tc_classic_row_base.py index 1484d15..30193a8 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_base.py +++ b/pcdswidgets/generated/motion/common/motor_tc_classic_row_base.py @@ -1,8 +1,8 @@ """ Generated by jinja from ui_base_widget.j2 with: -ui_name = motor_record_classic_tc_interlock_row.ui +ui_name = motor_tc_classic_row.ui form_cls = Ui_Form -base_cls = MotorRecordClassicTcInterlockRowBase +base_cls = MotorTcClassicRowBase macro_names = ['MOTOR'] Other long required variables: @@ -19,7 +19,7 @@ from pcdswidgets.builder.designer_widget import DesignerWidget -from .motor_record_classic_tc_interlock_row_form import * +from .motor_tc_classic_row_form import * try: from qtpy.QtCore import pyqtProperty @@ -27,21 +27,21 @@ from qtpy.QtCore import Property as pyqtProperty # type: ignore -class MotorRecordClassicTcInterlockRowBase(DesignerWidget): - MotorRecordClassicRow: "MotorRecordClassicRow" +class MotorTcClassicRowBase(DesignerWidget): + MotorClassicRow: "MotorClassicRow" interlock_indicator: "PyDMByteIndicator" temperature_label: "PyDMLabel" ui_form = Ui_Form _macro_to_widget = { "MOTOR": [ - "MotorRecordClassicRow", + "MotorClassicRow", "temperature_label", "interlock_indicator", ], } _widget_to_macro = { - "MotorRecordClassicRow": [ + "MotorClassicRow": [ "MOTOR", ], "interlock_indicator": [ @@ -52,7 +52,7 @@ class MotorRecordClassicTcInterlockRowBase(DesignerWidget): ], } _widget_to_pre_template = { - "MotorRecordClassicRow": [ + "MotorClassicRow": [ ("motor", "${MOTOR}"), ], "interlock_indicator": [ diff --git a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py b/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py similarity index 90% rename from pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py rename to pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py index ca512ff..cad932b 100644 --- a/pcdswidgets/generated/motion/common/motor_record_classic_tc_interlock_row_form.py +++ b/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui' +# Form implementation generated from reading ui file 'pcdswidgets/ui/motion/common/motor_tc_classic_row.ui' # # Created by: PyQt5 UI code generator 5.15.9 # @@ -12,7 +12,7 @@ from pydm.widgets.label import PyDMLabel from qtpy import QtCore, QtGui, QtWidgets -from pcdswidgets.motion.common.motor_record_classic_row import MotorRecordClassicRow +from pcdswidgets.motion.common.motor_classic_row import MotorClassicRow class Ui_Form(object): @@ -33,10 +33,10 @@ def setupUi(self, Form): self.verticalLayout_2.setContentsMargins(5, 5, 5, 5) self.verticalLayout_2.setSpacing(0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.MotorRecordClassicRow = MotorRecordClassicRow(self.frame) - self.MotorRecordClassicRow.setToolTip("") - self.MotorRecordClassicRow.setObjectName("MotorRecordClassicRow") - self.verticalLayout_2.addWidget(self.MotorRecordClassicRow) + self.MotorClassicRow = MotorClassicRow(self.frame) + self.MotorClassicRow.setToolTip("") + self.MotorClassicRow.setObjectName("MotorClassicRow") + self.verticalLayout_2.addWidget(self.MotorClassicRow) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setSpacing(6) self.horizontalLayout.setObjectName("horizontalLayout") @@ -99,7 +99,7 @@ def setupUi(self, Form): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) - self.MotorRecordClassicRow.setProperty("motor", _translate("Form", "${MOTOR}")) + self.MotorClassicRow.setProperty("motor", _translate("Form", "${MOTOR}")) self.label.setText(_translate("Form", "Motor temperature: ")) self.temperature_label.setChannel(_translate("Form", "ca://${MOTOR}:ILOCK:TC_TEMP_RBV")) self.label_2.setText(_translate("Form", "Interlock:")) diff --git a/pcdswidgets/motion/common/motor_record_classic_row.py b/pcdswidgets/motion/common/motor_classic_full.py similarity index 68% rename from pcdswidgets/motion/common/motor_record_classic_row.py rename to pcdswidgets/motion/common/motor_classic_full.py index 44c5859..74d8ce3 100644 --- a/pcdswidgets/motion/common/motor_record_classic_row.py +++ b/pcdswidgets/motion/common/motor_classic_full.py @@ -5,10 +5,10 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions -from pcdswidgets.generated.motion.common.motor_record_classic_row_base import MotorRecordClassicRowBase +from pcdswidgets.generated.motion.common.motor_classic_full_base import MotorClassicFullBase -class MotorRecordClassicRow(MotorRecordClassicRowBase): +class MotorClassicFull(MotorClassicFullBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, diff --git a/pcdswidgets/motion/common/motor_record_classic_full.py b/pcdswidgets/motion/common/motor_classic_row.py similarity index 67% rename from pcdswidgets/motion/common/motor_record_classic_full.py rename to pcdswidgets/motion/common/motor_classic_row.py index 04e75ab..b0ed9d6 100644 --- a/pcdswidgets/motion/common/motor_record_classic_full.py +++ b/pcdswidgets/motion/common/motor_classic_row.py @@ -5,10 +5,10 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions -from pcdswidgets.generated.motion.common.motor_record_classic_full_base import MotorRecordClassicFullBase +from pcdswidgets.generated.motion.common.motor_classic_row_base import MotorClassicRowBase -class MotorRecordClassicFull(MotorRecordClassicFullBase): +class MotorClassicRow(MotorClassicRowBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, diff --git a/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py b/pcdswidgets/motion/common/motor_tc_classic_row.py similarity index 61% rename from pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py rename to pcdswidgets/motion/common/motor_tc_classic_row.py index ea7b18d..81a8034 100644 --- a/pcdswidgets/motion/common/motor_record_classic_tc_interlock_row.py +++ b/pcdswidgets/motion/common/motor_tc_classic_row.py @@ -5,12 +5,10 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions -from pcdswidgets.generated.motion.common.motor_record_classic_tc_interlock_row_base import ( - MotorRecordClassicTcInterlockRowBase, -) +from pcdswidgets.generated.motion.common.motor_tc_classic_row_base import MotorTcClassicRowBase -class MotorRecordClassicTcInterlockRow(MotorRecordClassicTcInterlockRowBase): +class MotorTcClassicRow(MotorTcClassicRowBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, diff --git a/pcdswidgets/ui/motion/common/motor_record_classic_full.ui b/pcdswidgets/ui/motion/common/motor_classic_full.ui similarity index 100% rename from pcdswidgets/ui/motion/common/motor_record_classic_full.ui rename to pcdswidgets/ui/motion/common/motor_classic_full.ui diff --git a/pcdswidgets/ui/motion/common/motor_record_classic_row.ui b/pcdswidgets/ui/motion/common/motor_classic_row.ui similarity index 100% rename from pcdswidgets/ui/motion/common/motor_record_classic_row.ui rename to pcdswidgets/ui/motion/common/motor_classic_row.ui diff --git a/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui b/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui similarity index 97% rename from pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui rename to pcdswidgets/ui/motion/common/motor_tc_classic_row.ui index b01cdf6..042e196 100644 --- a/pcdswidgets/ui/motion/common/motor_record_classic_tc_interlock_row.ui +++ b/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui @@ -66,7 +66,7 @@ 5
- + @@ -251,9 +251,9 @@
pydm.widgets.byte
- MotorRecordClassicRow + MotorClassicRow QWidget -
pcdswidgets.motion.common.motor_record_classic_row
+
pcdswidgets.motion.common.motor_classic_row
diff --git a/pyproject.toml b/pyproject.toml index c59557e..9560dcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,9 @@ GetterPump = "pcdswidgets.vacuum.pumps:GetterPump" HotCathodeComboGauge = "pcdswidgets.vacuum.gauges:HotCathodeComboGauge" HotCathodeGauge = "pcdswidgets.vacuum.gauges:HotCathodeGauge" IonPump = "pcdswidgets.vacuum.pumps:IonPump" -MotorRecordClassicFull = "pcdswidgets.motion.common.motor_record_classic_full:MotorRecordClassicFull" -MotorRecordClassicRow = "pcdswidgets.motion.common.motor_record_classic_row:MotorRecordClassicRow" -MotorRecordClassicTcInterlockRow = "pcdswidgets.motion.common.motor_record_classic_tc_interlock_row:MotorRecordClassicTcInterlockRow" +MotorClassicFull = "pcdswidgets.motion.common.motor_classic_full:MotorClassicFull" +MotorClassicRow = "pcdswidgets.motion.common.motor_classic_row:MotorClassicRow" +MotorTcClassicRow = "pcdswidgets.motion.common.motor_tc_classic_row:MotorTcClassicRow" NeedleValve = "pcdswidgets.vacuum.valves:NeedleValve" PneumaticValve = "pcdswidgets.vacuum.valves:PneumaticValve" PneumaticValveDA = "pcdswidgets.vacuum.valves:PneumaticValveDA" From e2bd95fd42433b17f0682585019e211429b9d1ae Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Apr 2026 11:35:43 -0700 Subject: [PATCH 089/104] FIX: reduce content margins to match outer frame position to other row widgets --- .../motion/common/motor_tc_classic_row_form.py | 2 +- pcdswidgets/ui/motion/common/motor_tc_classic_row.ui | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py b/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py index cad932b..a7411a1 100644 --- a/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py +++ b/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py @@ -22,7 +22,7 @@ def setupUi(self, Form): Form.setMinimumSize(QtCore.QSize(725, 100)) Form.setMaximumSize(QtCore.QSize(800, 100)) self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setContentsMargins(5, 5, 5, 5) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(0) self.verticalLayout.setObjectName("verticalLayout") self.frame = QtWidgets.QFrame(Form) diff --git a/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui b/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui index 042e196..ecdc82b 100644 --- a/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui +++ b/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui @@ -30,16 +30,16 @@ 0
- 5 + 0 - 5 + 0 - 5 + 0 - 5 + 0 @@ -66,7 +66,7 @@ 5
- + From c5a4c9ddeb5b9ac19eb657d602fe7c0bdcfe7419 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Apr 2026 11:40:14 -0700 Subject: [PATCH 090/104] FIX: add missing HLS, LLS PVs to indicators --- .../motion/common/motor_classic_full_base.py | 16 ++++ .../motion/common/motor_classic_full_form.py | 24 ++++++ .../ui/motion/common/motor_classic_full.ui | 76 +++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/pcdswidgets/generated/motion/common/motor_classic_full_base.py b/pcdswidgets/generated/motion/common/motor_classic_full_base.py index cc11a43..07eda0d 100644 --- a/pcdswidgets/generated/motion/common/motor_classic_full_base.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_base.py @@ -28,6 +28,8 @@ class MotorClassicFullBase(DesignerWidget): + PyDMByteIndicator_hls: "PyDMByteIndicator" + PyDMByteIndicator_lls: "PyDMByteIndicator" PyDMByteIndicator_mvn: "PyDMByteIndicator" PyDMLabel_egu: "PyDMLabel" PyDMLabel_name: "PyDMLabel" @@ -44,7 +46,9 @@ class MotorClassicFullBase(DesignerWidget): "MOTOR": [ "PyDMLabel_name", "PyDMByteIndicator_mvn", + "PyDMByteIndicator_lls", "PyDMLabel_rbv", + "PyDMByteIndicator_hls", "PyDMPushButton_stop", "PyDMLineEdit_setpoint", "PyDMLabel_egu", @@ -55,6 +59,12 @@ class MotorClassicFullBase(DesignerWidget): ], } _widget_to_macro = { + "PyDMByteIndicator_hls": [ + "MOTOR", + ], + "PyDMByteIndicator_lls": [ + "MOTOR", + ], "PyDMByteIndicator_mvn": [ "MOTOR", ], @@ -87,6 +97,12 @@ class MotorClassicFullBase(DesignerWidget): ], } _widget_to_pre_template = { + "PyDMByteIndicator_hls": [ + ("channel", "ca://${MOTOR}.HLS"), + ], + "PyDMByteIndicator_lls": [ + ("channel", "ca://${MOTOR}.LLS"), + ], "PyDMByteIndicator_mvn": [ ("channel", "ca://${MOTOR}.MOVN"), ], diff --git a/pcdswidgets/generated/motion/common/motor_classic_full_form.py b/pcdswidgets/generated/motion/common/motor_classic_full_form.py index c09a549..d1a672b 100644 --- a/pcdswidgets/generated/motion/common/motor_classic_full_form.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_form.py @@ -91,7 +91,18 @@ def setupUi(self, Form): self.PyDMByteIndicator_lls = PyDMByteIndicator(self.gridFrame) self.PyDMByteIndicator_lls.setMaximumSize(QtCore.QSize(15, 15)) self.PyDMByteIndicator_lls.setToolTip("") + self.PyDMByteIndicator_lls.setAlarmSensitiveContent(False) + self.PyDMByteIndicator_lls.setAlarmSensitiveBorder(True) + self.PyDMByteIndicator_lls.setPyDMToolTip("") + self.PyDMByteIndicator_lls.setBlinkOnChange(False) + self.PyDMByteIndicator_lls.setToggleMode(False) + self.PyDMByteIndicator_lls.setBlinkInterval(99) self.PyDMByteIndicator_lls.setShowLabels(False) + self.PyDMByteIndicator_lls.setBigEndian(False) + self.PyDMByteIndicator_lls.setCircles(False) + self.PyDMByteIndicator_lls.setNumBits(1) + self.PyDMByteIndicator_lls.setShift(0) + self.PyDMByteIndicator_lls.setLabels(["Bit 0"]) self.PyDMByteIndicator_lls.setObjectName("PyDMByteIndicator_lls") self.horizontalLayout_4.addWidget(self.PyDMByteIndicator_lls) self.PyDMLabel_rbv = PyDMLabel(self.gridFrame) @@ -115,7 +126,18 @@ def setupUi(self, Form): self.PyDMByteIndicator_hls = PyDMByteIndicator(self.gridFrame) self.PyDMByteIndicator_hls.setMaximumSize(QtCore.QSize(15, 15)) self.PyDMByteIndicator_hls.setToolTip("") + self.PyDMByteIndicator_hls.setAlarmSensitiveContent(False) + self.PyDMByteIndicator_hls.setAlarmSensitiveBorder(True) + self.PyDMByteIndicator_hls.setPyDMToolTip("") + self.PyDMByteIndicator_hls.setBlinkOnChange(False) + self.PyDMByteIndicator_hls.setToggleMode(False) + self.PyDMByteIndicator_hls.setBlinkInterval(99) self.PyDMByteIndicator_hls.setShowLabels(False) + self.PyDMByteIndicator_hls.setBigEndian(False) + self.PyDMByteIndicator_hls.setCircles(False) + self.PyDMByteIndicator_hls.setNumBits(1) + self.PyDMByteIndicator_hls.setShift(0) + self.PyDMByteIndicator_hls.setLabels(["Bit 0"]) self.PyDMByteIndicator_hls.setObjectName("PyDMByteIndicator_hls") self.horizontalLayout_4.addWidget(self.PyDMByteIndicator_hls) self.gridLayout_2.addLayout(self.horizontalLayout_4, 0, 1, 1, 1) @@ -265,7 +287,9 @@ def retranslateUi(self, Form): Form.setWindowTitle(_translate("Form", "Form")) self.PyDMLabel_name.setChannel(_translate("Form", "ca://${MOTOR}.DESC")) self.PyDMByteIndicator_mvn.setChannel(_translate("Form", "ca://${MOTOR}.MOVN")) + self.PyDMByteIndicator_lls.setChannel(_translate("Form", "ca://${MOTOR}.LLS")) self.PyDMLabel_rbv.setChannel(_translate("Form", "ca://${MOTOR}.RBV")) + self.PyDMByteIndicator_hls.setChannel(_translate("Form", "ca://${MOTOR}.HLS")) self.PyDMPushButton_stop.setText(_translate("Form", "Stop")) self.PyDMPushButton_stop.setChannel(_translate("Form", "ca://${MOTOR}.STOP")) self.PyDMPushButton_stop.setPressValue(_translate("Form", "1")) diff --git a/pcdswidgets/ui/motion/common/motor_classic_full.ui b/pcdswidgets/ui/motion/common/motor_classic_full.ui index 78bd6c3..fbf0e03 100644 --- a/pcdswidgets/ui/motion/common/motor_classic_full.ui +++ b/pcdswidgets/ui/motion/common/motor_classic_full.ui @@ -200,9 +200,47 @@ + + false + + + true + + + + + + ca://${MOTOR}.LLS + + + false + + + false + + + 99 + false + + false + + + false + + + 1 + + + 0 + + + + Bit 0 + + @@ -257,9 +295,47 @@ + + false + + + true + + + + + + ca://${MOTOR}.HLS + + + false + + + false + + + 99 + false + + false + + + false + + + 1 + + + 0 + + + + Bit 0 + + From a3e2af2f8acca7669bcbe8d707eabba22157999c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Apr 2026 14:15:50 -0700 Subject: [PATCH 091/104] TST: slightly extend and repair test suite --- pcdswidgets/generated/tests/__init__.py | 0 .../generated/tests/builder/__init__.py | 0 .../builder/widget_for_builder_test_base.py | 131 ++++++++++++++++++ .../builder/widget_for_builder_test_form.py | 61 ++++++++ pcdswidgets/tests/builder/__init__.py | 0 pcdswidgets/tests/builder/test_builder.py | 75 ++++++++-- .../tests/builder/test_designer_widget.py | 24 ++-- .../tests/builder/widget_for_builder_test.py | 16 +++ .../tests/builder/widget_for_builder_test.ui} | 0 9 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 pcdswidgets/generated/tests/__init__.py create mode 100644 pcdswidgets/generated/tests/builder/__init__.py create mode 100644 pcdswidgets/generated/tests/builder/widget_for_builder_test_base.py create mode 100644 pcdswidgets/generated/tests/builder/widget_for_builder_test_form.py create mode 100644 pcdswidgets/tests/builder/__init__.py create mode 100644 pcdswidgets/tests/builder/widget_for_builder_test.py rename pcdswidgets/{tests/builder/pytest.ui => ui/tests/builder/widget_for_builder_test.ui} (100%) diff --git a/pcdswidgets/generated/tests/__init__.py b/pcdswidgets/generated/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/generated/tests/builder/__init__.py b/pcdswidgets/generated/tests/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/generated/tests/builder/widget_for_builder_test_base.py b/pcdswidgets/generated/tests/builder/widget_for_builder_test_base.py new file mode 100644 index 0000000..5a642aa --- /dev/null +++ b/pcdswidgets/generated/tests/builder/widget_for_builder_test_base.py @@ -0,0 +1,131 @@ +""" +Generated by jinja from ui_base_widget.j2 with: +ui_name = widget_for_builder_test.ui +form_cls = Ui_Form +base_cls = WidgetForBuilderTestBase +macro_names = ['NAME', 'NUM', 'ONE', 'TWO'] + +Other long required variables: +widget_names: list[str] +widget_name_to_class: dict[str, str] +macro_to_widget: dict[str, str] +widget_to_macro: dict[str, str] +widget_to_pre_templ_strs: dict[str, list[tuple[str, str]]] +widget_to_pre_templ_lists: dict[str, list[tuple[str, list[str]]]] +""" +# ruff: noqa: E501 +# ruff: noqa: F403 +# ruff: noqa: F405 + +from pcdswidgets.builder.designer_widget import DesignerWidget + +from .widget_for_builder_test_form import * + +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class WidgetForBuilderTestBase(DesignerWidget): + name_label: "QLabel" + name_num_label: "QLabel" + num_label: "QLabel" + one_two_shell: "PyDMShellCommand" + + ui_form = Ui_Form + _macro_to_widget = { + "NAME": [ + "name_label", + "name_num_label", + ], + "NUM": [ + "num_label", + "name_num_label", + ], + "ONE": [ + "one_two_shell", + ], + "TWO": [ + "one_two_shell", + ], + } + _widget_to_macro = { + "name_label": [ + "NAME", + ], + "name_num_label": [ + "NAME", + "NUM", + ], + "num_label": [ + "NUM", + ], + "one_two_shell": [ + "ONE", + "TWO", + ], + } + _widget_to_pre_template = { + "name_label": [ + ("toolTip", "${NAME}"), + ("text", "Name: ${NAME}"), + ], + "name_num_label": [ + ("text", "${NAME}:${NUM}"), + ], + "num_label": [ + ("text", "Num: ${NUM}"), + ], + "one_two_shell": [ + ( + "commands", + [ + "echo ${ONE}", + "echo ${TWO}", + "echo ${ONE}:${TWO}", + ], + ), + ], + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._macro_values = { + "NAME": "", + "NUM": "", + "ONE": "", + "TWO": "", + } + + def get_name(self) -> str: + return self._get_macro("NAME") + + def set_name(self, value: str) -> None: + self._set_macro("NAME", value) + + name = pyqtProperty(str, get_name, set_name) + + def get_num(self) -> str: + return self._get_macro("NUM") + + def set_num(self, value: str) -> None: + self._set_macro("NUM", value) + + num = pyqtProperty(str, get_num, set_num) + + def get_one(self) -> str: + return self._get_macro("ONE") + + def set_one(self, value: str) -> None: + self._set_macro("ONE", value) + + one = pyqtProperty(str, get_one, set_one) + + def get_two(self) -> str: + return self._get_macro("TWO") + + def set_two(self, value: str) -> None: + self._set_macro("TWO", value) + + two = pyqtProperty(str, get_two, set_two) diff --git a/pcdswidgets/generated/tests/builder/widget_for_builder_test_form.py b/pcdswidgets/generated/tests/builder/widget_for_builder_test_form.py new file mode 100644 index 0000000..d374a4b --- /dev/null +++ b/pcdswidgets/generated/tests/builder/widget_for_builder_test_form.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Form implementation generated from reading ui file 'pcdswidgets/ui/tests/builder/widget_for_builder_test.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. +# +# Augmented by pcdswidgets.builder.build +# ruff: noqa: E501 +from pydm.widgets.shell_command import PyDMShellCommand +from qtpy import QtCore, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 191) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setObjectName("verticalLayout") + self.name_label = QtWidgets.QLabel(Form) + self.name_label.setObjectName("name_label") + self.verticalLayout.addWidget(self.name_label) + self.num_label = QtWidgets.QLabel(Form) + self.num_label.setObjectName("num_label") + self.verticalLayout.addWidget(self.num_label) + self.name_num_label = QtWidgets.QLabel(Form) + self.name_num_label.setObjectName("name_num_label") + self.verticalLayout.addWidget(self.name_num_label) + self.one_two_shell = PyDMShellCommand(Form) + self.one_two_shell.setToolTip("") + self.one_two_shell.setAlarmSensitiveContent(False) + self.one_two_shell.setAlarmSensitiveBorder(True) + self.one_two_shell.setPyDMToolTip("") + self.one_two_shell.setChannel("") + self.one_two_shell.setPyDMIcon("") + self.one_two_shell.setShowConfirmDialog(False) + self.one_two_shell.setRunCommandsInFullShell(False) + self.one_two_shell.setEnvironmentVariables("") + self.one_two_shell.setShowIcon(True) + self.one_two_shell.setAllowMultipleExecutions(False) + self.one_two_shell.setTitles([]) + self.one_two_shell.setCommands(["echo ${ONE}", "echo ${TWO}", "echo ${ONE}:${TWO}"]) + self.one_two_shell.setPasswordProtected(False) + self.one_two_shell.setPassword("") + self.one_two_shell.setProtectedPassword("") + self.one_two_shell.setShowCurrentlyRunningIndication(False) + self.one_two_shell.setObjectName("one_two_shell") + self.verticalLayout.addWidget(self.one_two_shell) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.name_label.setToolTip(_translate("Form", "${NAME}")) + self.name_label.setText(_translate("Form", "Name: ${NAME}")) + self.num_label.setText(_translate("Form", "Num: ${NUM}")) + self.name_num_label.setText(_translate("Form", "${NAME}:${NUM}")) + self.one_two_shell.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) diff --git a/pcdswidgets/tests/builder/__init__.py b/pcdswidgets/tests/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcdswidgets/tests/builder/test_builder.py b/pcdswidgets/tests/builder/test_builder.py index dd9aeaf..4500d34 100644 --- a/pcdswidgets/tests/builder/test_builder.py +++ b/pcdswidgets/tests/builder/test_builder.py @@ -7,45 +7,90 @@ import pcdswidgets from pcdswidgets.builder.designer_widget import DesignerWidget -UI_SOURCES = sorted((Path(pcdswidgets.__file__) / "ui").rglob("*.ui")) +MODULE_ROOT = Path(pcdswidgets.__file__).parent +UI_SOURCES = sorted((MODULE_ROOT / "ui").rglob("*.ui")) + +TEST_UI = str(Path(__file__).parent / "pytest.ui") @pytest.mark.parametrize("ui_source", UI_SOURCES) def test_it_was_built(ui_source: Path): """ - Check if the current clone has had .py files built from all the source .ui files. + Check if .py files have been built from all the source .ui files. """ - base_path = ui_source.parent / (ui_source.stem + "_base.py") - form_path = ui_source.parent / (ui_source.stem + "_form.py") + subsystem, device = get_subsystem_and_device(ui_source=ui_source) + + gen_dir = MODULE_ROOT / "generated" / subsystem / device + base_path = gen_dir / (ui_source.stem + "_base.py") + form_path = gen_dir / (ui_source.stem + "_form.py") + + main_path = MODULE_ROOT / subsystem / device / (ui_source.stem + ".py") assert base_path.exists() assert form_path.exists() + assert main_path.exists() + + +def get_subsystem_and_device(ui_source: Path) -> tuple[str, str]: + subsystem = None + device = None + + seen_ui = False + for part in ui_source.parts: + if part == "ui": + seen_ui = True + elif seen_ui: + if subsystem is None: + subsystem = part + else: + device = part + break + + assert subsystem is not None + assert device is not None + + return subsystem, device @pytest.mark.parametrize("ui_source", UI_SOURCES) def test_built_is_importable(ui_source: Path): """ - Check if the .py files in the current clone have somewhat proper importable classes. + Check if the .py files have somewhat proper importable classes. """ - base_module_name = "pcdswidgets.builder.ui." + ui_source.stem + "_base" - form_module_name = "pcdswidgets.builder.ui." + ui_source.stem + "_form" + subsystem, device = get_subsystem_and_device(ui_source=ui_source) - base_module = importlib.import_module(base_module_name) - form_module = importlib.import_module(form_module_name) + form_module_name = f"pcdswidgets.generated.{subsystem}.{device}.{ui_source.stem}_form" + base_module_name = f"pcdswidgets.generated.{subsystem}.{device}.{ui_source.stem}_base" + main_module_name = f"pcdswidgets.{subsystem}.{device}.{ui_source.stem}" - base_classes = [] - for _, cls in inspect.getmembers(base_module, inspect.isclass): - if inspect.getmodule(cls) is base_module: - base_classes.append(cls) + form_module = importlib.import_module(form_module_name) + base_module = importlib.import_module(base_module_name) + main_module = importlib.import_module(main_module_name) form_classes = [] for _, cls in inspect.getmembers(form_module, inspect.isclass): if inspect.getmodule(cls) is form_module: form_classes.append(cls) - assert len(base_classes) == 1 - assert issubclass(base_classes[0], DesignerWidget) + base_classes = [] + for _, cls in inspect.getmembers(base_module, inspect.isclass): + if inspect.getmodule(cls) is base_module: + base_classes.append(cls) + + main_classes = [] + for _, cls in inspect.getmembers(main_module, inspect.isclass): + if inspect.getmodule(cls) is main_module: + main_classes.append(cls) assert len(form_classes) == 1 assert hasattr(form_classes[0], "setupUi") assert hasattr(form_classes[0], "retranslateUi") + + assert len(base_classes) == 1 + assert issubclass(base_classes[0], DesignerWidget) + assert base_classes[0].ui_form is form_classes[0] + + assert len(main_classes) == 1 + assert hasattr(main_classes[0], "designer_options") + assert hasattr(main_classes[0], "_qt_designer_") + assert issubclass(main_classes[0], base_classes[0]) diff --git a/pcdswidgets/tests/builder/test_designer_widget.py b/pcdswidgets/tests/builder/test_designer_widget.py index a8b08e9..df0942c 100644 --- a/pcdswidgets/tests/builder/test_designer_widget.py +++ b/pcdswidgets/tests/builder/test_designer_widget.py @@ -1,38 +1,38 @@ import inspect import pytest -from pcdswidgets.builder.ui.pytest_base import PytestBase from pydm.widgets.shell_command import PyDMShellCommand from pytestqt.qtbot import QtBot from qtpy.QtWidgets import QLabel +from pcdswidgets.generated.tests.builder.widget_for_builder_test_base import WidgetForBuilderTestBase -class TestWidget(PytestBase): ... +from .widget_for_builder_test import WidgetForBuilderTest @pytest.fixture(scope="function") -def test_widget(qtbot: QtBot) -> TestWidget: - widget = TestWidget() +def test_widget(qtbot: QtBot) -> WidgetForBuilderTest: + widget = WidgetForBuilderTest() qtbot.add_widget(widget) return widget def test_has_expected_hints(): - hints = inspect.get_annotations(PytestBase) + hints = inspect.get_annotations(WidgetForBuilderTestBase) for label_name in ("name_label", "num_label", "name_num_label"): assert hints[label_name] == "QLabel" assert hints["one_two_shell"] == "PyDMShellCommand" -def test_has_expected_widgets(test_widget: TestWidget): +def test_has_expected_widgets(test_widget: WidgetForBuilderTest): assert isinstance(test_widget.name_label, QLabel) assert isinstance(test_widget.num_label, QLabel) assert isinstance(test_widget.name_num_label, QLabel) assert isinstance(test_widget.one_two_shell, PyDMShellCommand) -def test_has_expected_macro_to_widget(test_widget: TestWidget): +def test_has_expected_macro_to_widget(test_widget: WidgetForBuilderTest): assert set(test_widget._macro_to_widget.keys()) == {"NAME", "NUM", "ONE", "TWO"} assert set(test_widget._macro_to_widget["NAME"]) == {"name_label", "name_num_label"} assert set(test_widget._macro_to_widget["NUM"]) == {"num_label", "name_num_label"} @@ -40,7 +40,7 @@ def test_has_expected_macro_to_widget(test_widget: TestWidget): assert test_widget._macro_to_widget["TWO"] == ["one_two_shell"] -def test_has_expected_widget_to_macro(test_widget: TestWidget): +def test_has_expected_widget_to_macro(test_widget: WidgetForBuilderTest): assert set(test_widget._widget_to_macro.keys()) == {"name_label", "num_label", "name_num_label", "one_two_shell"} assert test_widget._widget_to_macro["name_label"] == ["NAME"] assert test_widget._widget_to_macro["num_label"] == ["NUM"] @@ -48,7 +48,7 @@ def test_has_expected_widget_to_macro(test_widget: TestWidget): assert set(test_widget._widget_to_macro["one_two_shell"]) == {"ONE", "TWO"} -def test_has_expected_widget_to_pre_template(test_widget: TestWidget): +def test_has_expected_widget_to_pre_template(test_widget: WidgetForBuilderTest): assert set(test_widget._widget_to_pre_template.keys()) == { "name_label", "num_label", @@ -63,7 +63,7 @@ def test_has_expected_widget_to_pre_template(test_widget: TestWidget): ] -def test_has_expected_macro_values(test_widget: TestWidget): +def test_has_expected_macro_values(test_widget: WidgetForBuilderTest): assert test_widget._macro_values == { "NAME": "", "NUM": "", @@ -72,7 +72,7 @@ def test_has_expected_macro_values(test_widget: TestWidget): } -def test_macro_substitution_labels(test_widget: TestWidget): +def test_macro_substitution_labels(test_widget: WidgetForBuilderTest): assert test_widget.name_label.text() == "Name: ${NAME}" assert test_widget.num_label.text() == "Num: ${NUM}" assert test_widget.name_num_label.text() == "${NAME}:${NUM}" @@ -96,7 +96,7 @@ def test_macro_substitution_labels(test_widget: TestWidget): assert test_widget.name_num_label.text() == "Steven:02" -def test_macro_substitution_list_widget(test_widget: TestWidget): +def test_macro_substitution_list_widget(test_widget: WidgetForBuilderTest): assert test_widget.one_two_shell.readCommands() == ["echo ${ONE}", "echo ${TWO}", "echo ${ONE}:${TWO}"] test_widget.setProperty("one", "UNO") diff --git a/pcdswidgets/tests/builder/widget_for_builder_test.py b/pcdswidgets/tests/builder/widget_for_builder_test.py new file mode 100644 index 0000000..414f005 --- /dev/null +++ b/pcdswidgets/tests/builder/widget_for_builder_test.py @@ -0,0 +1,16 @@ +""" +Originally generated from jinja template ui_main_widget.j2 + +This file can be safely edited to change the runtime behavior of the widget. +""" + +from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.generated.tests.builder.widget_for_builder_test_base import WidgetForBuilderTestBase + + +class WidgetForBuilderTest(WidgetForBuilderTestBase): + designer_options = DesignerOptions( + group="ECS Tests Builder", + is_container=False, + icon=None, + ) diff --git a/pcdswidgets/tests/builder/pytest.ui b/pcdswidgets/ui/tests/builder/widget_for_builder_test.ui similarity index 100% rename from pcdswidgets/tests/builder/pytest.ui rename to pcdswidgets/ui/tests/builder/widget_for_builder_test.ui From d49e7fdebb82a6cdf5226dedc01076c672b5bcd7 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Apr 2026 14:22:58 -0700 Subject: [PATCH 092/104] CI: back to standard (updated) CI workflow --- .github/workflows/standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml index 28398d0..dd00043 100644 --- a/.github/workflows/standard.yml +++ b/.github/workflows/standard.yml @@ -9,7 +9,7 @@ on: jobs: standard: - uses: pcdshub/pcds-ci-helpers/.github/workflows/python-standard.yml@tst_from_installed + uses: pcdshub/pcds-ci-helpers/.github/workflows/python-standard.yml@master secrets: inherit with: # The workflow needs to know the package name. This can be determined From 0ec77b30f12dd5337e77ec088f2890c1db5be6a5 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Mon, 13 Apr 2026 15:36:29 -0700 Subject: [PATCH 093/104] FIX: use explicit recursive make steps in all target to ensure order when needed --- Makefile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 824d2e5..60d0693 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build inits venv venv-again +.PHONY: all build inits venv UI_SOURCE := $(wildcard pcdswidgets/ui/*/*/*.ui) PY_SOURCE := $(filter-out pcdswidgets/builder/ui/%.py, $(filter-out pcdswidgets/_version.py, $(shell find pcdswidgets -name "*.py"))) @@ -12,8 +12,14 @@ BUILD_CMD := $(BIN)/python -m pcdswidgets.builder.build CHECK_FIX := $(BIN)/ruff check --exit-zero --fix --quiet FORMAT := $(BIN)/ruff format --quiet -# We need to update the venv before and after each of our steps -all: venv build pyproject.toml venv-again +# We need to update the venv before doing any step and after doing all of them +# The order matters here, except the py files in the build target could be done in any order +all: + $(MAKE) venv + $(MAKE) build + $(MAKE) inits + $(MAKE) pyproject.toml + $(MAKE) venv build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) inits @@ -43,7 +49,3 @@ pyproject.toml: $(PY_SOURCE) venv: ./build_local_venv.sh - -# For running the again after pyproject.toml is generated in make all -venv-again: - ./build_local_venv.sh From 14176c702d8bd17e7674cd001338b61919d34517 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Mon, 13 Apr 2026 17:48:59 -0700 Subject: [PATCH 094/104] ENH: generate enum with icon options to make selecting icon nicer --- pcdswidgets/builder/designer_options.py | 8 +- pcdswidgets/builder/designer_widget.py | 17 +- pcdswidgets/builder/get_icon_options.py | 34 + pcdswidgets/builder/icon_options.j2 | 14 + pcdswidgets/builder/icon_options.py | 1470 +++++++++++++++++ pcdswidgets/builder/ui_main_widget.j2 | 3 +- .../motion/common/motor_classic_full.py | 3 +- .../motion/common/motor_classic_row.py | 3 +- .../motion/common/motor_tc_classic_row.py | 3 +- .../smaract/smaract_open_loop_classic_row.py | 3 +- 10 files changed, 1544 insertions(+), 14 deletions(-) create mode 100644 pcdswidgets/builder/get_icon_options.py create mode 100644 pcdswidgets/builder/icon_options.j2 create mode 100644 pcdswidgets/builder/icon_options.py diff --git a/pcdswidgets/builder/designer_options.py b/pcdswidgets/builder/designer_options.py index c5c9b30..1e03139 100644 --- a/pcdswidgets/builder/designer_options.py +++ b/pcdswidgets/builder/designer_options.py @@ -2,6 +2,8 @@ from qtpy.QtGui import QIcon +from .icon_options import IconOptions + @dataclass class DesignerOptions: @@ -15,13 +17,13 @@ class DesignerOptions: is_container : bool Set this to True if this widget can contain other widgets (via drag and drop), or False otherwise. - icon : str, icon, or None + icon : IconOptions, icon, or None The icon to use to represent this widget in the qt sidebar. - If a string, we'll use PyDM's iconfont to pick the matching icon. + If an IconOptions enum, we'll use PyDM's iconfont to pick the matching icon. If a QIcon, we'll use it as-is. If None, we'll use the default icon. """ group: str is_container: bool - icon: str | QIcon | None + icon: IconOptions | QIcon | None diff --git a/pcdswidgets/builder/designer_widget.py b/pcdswidgets/builder/designer_widget.py index 4e78e55..bfb27f3 100644 --- a/pcdswidgets/builder/designer_widget.py +++ b/pcdswidgets/builder/designer_widget.py @@ -12,6 +12,7 @@ from qtpy.QtWidgets import QAction, QDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget from .designer_options import DesignerOptions +from .icon_options import IconOptions ifont = IconFont() @@ -48,13 +49,17 @@ def __init_subclass__(cls): "group": cls.designer_options.group, "is_container": cls.designer_options.is_container, } - icon_name = cls.designer_options.icon - if icon_name is not None: - cls._qt_designer_["icon"] = icon_name - # Interpret strings as icons so we don't have to import IconFont everywhere + icon_obj = cls.designer_options.icon + if icon_obj is not None: + cls._qt_designer_["icon"] = icon_obj + # Interpret enum as icons for ease of selection try: - if isinstance(cls._qt_designer_["icon"], str): - cls._qt_designer_["icon"] = ifont.icon(cls._qt_designer_["icon"]) + icon = cls._qt_designer_["icon"] + if isinstance(icon, IconOptions): + if str(icon): + cls._qt_designer_["icon"] = ifont.icon(str(icon)) + else: + del cls._qt_designer_["icon"] except (AttributeError, KeyError): ... # Include a quick editor for macro vals diff --git a/pcdswidgets/builder/get_icon_options.py b/pcdswidgets/builder/get_icon_options.py new file mode 100644 index 0000000..dca76d4 --- /dev/null +++ b/pcdswidgets/builder/get_icon_options.py @@ -0,0 +1,34 @@ +""" +Generate a fresh version of icon_options.py + +This helps us figure out what options exist for designer icons as provided by pydm. +""" + +import json +from pathlib import Path + +from jinja2 import Environment, PackageLoader +from pydm.utilities import iconfont + + +def generate_icon_options(): + jinja_template = "icon_options.j2" + env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) + template = env.get_template(jinja_template) + jinja_output = template.render( + options=get_icon_options(), + ) + output_file = Path(__file__).parent / "icon_options.py" + with open(output_file, "w") as fd: + fd.write(jinja_output) + fd.write("\n") + + +def get_icon_options() -> list[str]: + with open(Path(iconfont.__file__).parent / "fontawesome-charmap.json", "r") as fd: + charmap: dict[str, str] = json.load(fd) + return list(charmap) + + +if __name__ == "__main__": + generate_icon_options() diff --git a/pcdswidgets/builder/icon_options.j2 b/pcdswidgets/builder/icon_options.j2 new file mode 100644 index 0000000..ec4e1b2 --- /dev/null +++ b/pcdswidgets/builder/icon_options.j2 @@ -0,0 +1,14 @@ +""" +Define the IconOptions enum, which helps us keep track of which string icon names are valid. + +This is a file generated from icon_options.j2 +""" + +from enum import StrEnum + + +class IconOptions(StrEnum): + NONE = "" +{% for opt in options %} + {{ opt.replace("-", "_").replace("5", "a5") }} = "{{ opt }}" +{% endfor %} diff --git a/pcdswidgets/builder/icon_options.py b/pcdswidgets/builder/icon_options.py new file mode 100644 index 0000000..48384ab --- /dev/null +++ b/pcdswidgets/builder/icon_options.py @@ -0,0 +1,1470 @@ +""" +Define the IconOptions enum, which helps us keep track of which string icon names are valid. + +This is a file generated from icon_options.j2 +""" + +from enum import StrEnum + + +class IconOptions(StrEnum): + NONE = "" + a500px = "500px" + accessible_icon = "accessible-icon" + accusoft = "accusoft" + acquisitions_incorporated = "acquisitions-incorporated" + ad = "ad" + address_book = "address-book" + address_card = "address-card" + adjust = "adjust" + adn = "adn" + adversal = "adversal" + affiliatetheme = "affiliatetheme" + air_freshener = "air-freshener" + airbnb = "airbnb" + algolia = "algolia" + align_center = "align-center" + align_justify = "align-justify" + align_left = "align-left" + align_right = "align-right" + alipay = "alipay" + allergies = "allergies" + amazon = "amazon" + amazon_pay = "amazon-pay" + ambulance = "ambulance" + american_sign_language_interpreting = "american-sign-language-interpreting" + amilia = "amilia" + anchor = "anchor" + android = "android" + angellist = "angellist" + angle_double_down = "angle-double-down" + angle_double_left = "angle-double-left" + angle_double_right = "angle-double-right" + angle_double_up = "angle-double-up" + angle_down = "angle-down" + angle_left = "angle-left" + angle_right = "angle-right" + angle_up = "angle-up" + angry = "angry" + angrycreative = "angrycreative" + angular = "angular" + ankh = "ankh" + app_store = "app-store" + app_store_ios = "app-store-ios" + apper = "apper" + apple = "apple" + apple_alt = "apple-alt" + apple_pay = "apple-pay" + archive = "archive" + archway = "archway" + arrow_alt_circle_down = "arrow-alt-circle-down" + arrow_alt_circle_left = "arrow-alt-circle-left" + arrow_alt_circle_right = "arrow-alt-circle-right" + arrow_alt_circle_up = "arrow-alt-circle-up" + arrow_circle_down = "arrow-circle-down" + arrow_circle_left = "arrow-circle-left" + arrow_circle_right = "arrow-circle-right" + arrow_circle_up = "arrow-circle-up" + arrow_down = "arrow-down" + arrow_left = "arrow-left" + arrow_right = "arrow-right" + arrow_up = "arrow-up" + arrows_alt = "arrows-alt" + arrows_alt_h = "arrows-alt-h" + arrows_alt_v = "arrows-alt-v" + artstation = "artstation" + assistive_listening_systems = "assistive-listening-systems" + asterisk = "asterisk" + asymmetrik = "asymmetrik" + at = "at" + atlas = "atlas" + atlassian = "atlassian" + atom = "atom" + audible = "audible" + audio_description = "audio-description" + autoprefixer = "autoprefixer" + avianex = "avianex" + aviato = "aviato" + award = "award" + aws = "aws" + baby = "baby" + baby_carriage = "baby-carriage" + backspace = "backspace" + backward = "backward" + bacon = "bacon" + bacteria = "bacteria" + bacterium = "bacterium" + bahai = "bahai" + balance_scale = "balance-scale" + balance_scale_left = "balance-scale-left" + balance_scale_right = "balance-scale-right" + ban = "ban" + band_aid = "band-aid" + bandcamp = "bandcamp" + barcode = "barcode" + bars = "bars" + baseball_ball = "baseball-ball" + basketball_ball = "basketball-ball" + bath = "bath" + battery_empty = "battery-empty" + battery_full = "battery-full" + battery_half = "battery-half" + battery_quarter = "battery-quarter" + battery_three_quarters = "battery-three-quarters" + battle_net = "battle-net" + bed = "bed" + beer = "beer" + behance = "behance" + behance_square = "behance-square" + bell = "bell" + bell_slash = "bell-slash" + bezier_curve = "bezier-curve" + bible = "bible" + bicycle = "bicycle" + biking = "biking" + bimobject = "bimobject" + binoculars = "binoculars" + biohazard = "biohazard" + birthday_cake = "birthday-cake" + bitbucket = "bitbucket" + bitcoin = "bitcoin" + bity = "bity" + black_tie = "black-tie" + blackberry = "blackberry" + blender = "blender" + blender_phone = "blender-phone" + blind = "blind" + blog = "blog" + blogger = "blogger" + blogger_b = "blogger-b" + bluetooth = "bluetooth" + bluetooth_b = "bluetooth-b" + bold = "bold" + bolt = "bolt" + bomb = "bomb" + bone = "bone" + bong = "bong" + book = "book" + book_dead = "book-dead" + book_medical = "book-medical" + book_open = "book-open" + book_reader = "book-reader" + bookmark = "bookmark" + bootstrap = "bootstrap" + border_all = "border-all" + border_none = "border-none" + border_style = "border-style" + bowling_ball = "bowling-ball" + box = "box" + box_open = "box-open" + box_tissue = "box-tissue" + boxes = "boxes" + braille = "braille" + brain = "brain" + bread_slice = "bread-slice" + briefcase = "briefcase" + briefcase_medical = "briefcase-medical" + broadcast_tower = "broadcast-tower" + broom = "broom" + brush = "brush" + btc = "btc" + buffer = "buffer" + bug = "bug" + building = "building" + bullhorn = "bullhorn" + bullseye = "bullseye" + burn = "burn" + buromobelexperte = "buromobelexperte" + bus = "bus" + bus_alt = "bus-alt" + business_time = "business-time" + buy_n_large = "buy-n-large" + buysellads = "buysellads" + calculator = "calculator" + calendar = "calendar" + calendar_alt = "calendar-alt" + calendar_check = "calendar-check" + calendar_day = "calendar-day" + calendar_minus = "calendar-minus" + calendar_plus = "calendar-plus" + calendar_times = "calendar-times" + calendar_week = "calendar-week" + camera = "camera" + camera_retro = "camera-retro" + campground = "campground" + canadian_maple_leaf = "canadian-maple-leaf" + candy_cane = "candy-cane" + cannabis = "cannabis" + capsules = "capsules" + car = "car" + car_alt = "car-alt" + car_battery = "car-battery" + car_crash = "car-crash" + car_side = "car-side" + caravan = "caravan" + caret_down = "caret-down" + caret_left = "caret-left" + caret_right = "caret-right" + caret_square_down = "caret-square-down" + caret_square_left = "caret-square-left" + caret_square_right = "caret-square-right" + caret_square_up = "caret-square-up" + caret_up = "caret-up" + carrot = "carrot" + cart_arrow_down = "cart-arrow-down" + cart_plus = "cart-plus" + cash_register = "cash-register" + cat = "cat" + cc_amazon_pay = "cc-amazon-pay" + cc_amex = "cc-amex" + cc_apple_pay = "cc-apple-pay" + cc_diners_club = "cc-diners-club" + cc_discover = "cc-discover" + cc_jcb = "cc-jcb" + cc_mastercard = "cc-mastercard" + cc_paypal = "cc-paypal" + cc_stripe = "cc-stripe" + cc_visa = "cc-visa" + centercode = "centercode" + centos = "centos" + certificate = "certificate" + chair = "chair" + chalkboard = "chalkboard" + chalkboard_teacher = "chalkboard-teacher" + charging_station = "charging-station" + chart_area = "chart-area" + chart_bar = "chart-bar" + chart_line = "chart-line" + chart_pie = "chart-pie" + check = "check" + check_circle = "check-circle" + check_double = "check-double" + check_square = "check-square" + cheese = "cheese" + chess = "chess" + chess_bishop = "chess-bishop" + chess_board = "chess-board" + chess_king = "chess-king" + chess_knight = "chess-knight" + chess_pawn = "chess-pawn" + chess_queen = "chess-queen" + chess_rook = "chess-rook" + chevron_circle_down = "chevron-circle-down" + chevron_circle_left = "chevron-circle-left" + chevron_circle_right = "chevron-circle-right" + chevron_circle_up = "chevron-circle-up" + chevron_down = "chevron-down" + chevron_left = "chevron-left" + chevron_right = "chevron-right" + chevron_up = "chevron-up" + child = "child" + chrome = "chrome" + chromecast = "chromecast" + church = "church" + circle = "circle" + circle_notch = "circle-notch" + city = "city" + clinic_medical = "clinic-medical" + clipboard = "clipboard" + clipboard_check = "clipboard-check" + clipboard_list = "clipboard-list" + clock = "clock" + clone = "clone" + closed_captioning = "closed-captioning" + cloud = "cloud" + cloud_download_alt = "cloud-download-alt" + cloud_meatball = "cloud-meatball" + cloud_moon = "cloud-moon" + cloud_moon_rain = "cloud-moon-rain" + cloud_rain = "cloud-rain" + cloud_showers_heavy = "cloud-showers-heavy" + cloud_sun = "cloud-sun" + cloud_sun_rain = "cloud-sun-rain" + cloud_upload_alt = "cloud-upload-alt" + cloudflare = "cloudflare" + cloudscale = "cloudscale" + cloudsmith = "cloudsmith" + cloudversify = "cloudversify" + cocktail = "cocktail" + code = "code" + code_branch = "code-branch" + codepen = "codepen" + codiepie = "codiepie" + coffee = "coffee" + cog = "cog" + cogs = "cogs" + coins = "coins" + columns = "columns" + comment = "comment" + comment_alt = "comment-alt" + comment_dollar = "comment-dollar" + comment_dots = "comment-dots" + comment_medical = "comment-medical" + comment_slash = "comment-slash" + comments = "comments" + comments_dollar = "comments-dollar" + compact_disc = "compact-disc" + compass = "compass" + compress = "compress" + compress_alt = "compress-alt" + compress_arrows_alt = "compress-arrows-alt" + concierge_bell = "concierge-bell" + confluence = "confluence" + connectdevelop = "connectdevelop" + contao = "contao" + cookie = "cookie" + cookie_bite = "cookie-bite" + copy = "copy" + copyright = "copyright" + cotton_bureau = "cotton-bureau" + couch = "couch" + cpanel = "cpanel" + creative_commons = "creative-commons" + creative_commons_by = "creative-commons-by" + creative_commons_nc = "creative-commons-nc" + creative_commons_nc_eu = "creative-commons-nc-eu" + creative_commons_nc_jp = "creative-commons-nc-jp" + creative_commons_nd = "creative-commons-nd" + creative_commons_pd = "creative-commons-pd" + creative_commons_pd_alt = "creative-commons-pd-alt" + creative_commons_remix = "creative-commons-remix" + creative_commons_sa = "creative-commons-sa" + creative_commons_sampling = "creative-commons-sampling" + creative_commons_sampling_plus = "creative-commons-sampling-plus" + creative_commons_share = "creative-commons-share" + creative_commons_zero = "creative-commons-zero" + credit_card = "credit-card" + critical_role = "critical-role" + crop = "crop" + crop_alt = "crop-alt" + cross = "cross" + crosshairs = "crosshairs" + crow = "crow" + crown = "crown" + crutch = "crutch" + css3 = "css3" + css3_alt = "css3-alt" + cube = "cube" + cubes = "cubes" + cut = "cut" + cuttlefish = "cuttlefish" + d_and_d = "d-and-d" + d_and_d_beyond = "d-and-d-beyond" + dailymotion = "dailymotion" + dashcube = "dashcube" + database = "database" + deaf = "deaf" + deezer = "deezer" + delicious = "delicious" + democrat = "democrat" + deploydog = "deploydog" + deskpro = "deskpro" + desktop = "desktop" + dev = "dev" + deviantart = "deviantart" + dharmachakra = "dharmachakra" + dhl = "dhl" + diagnoses = "diagnoses" + diaspora = "diaspora" + dice = "dice" + dice_d20 = "dice-d20" + dice_d6 = "dice-d6" + dice_five = "dice-five" + dice_four = "dice-four" + dice_one = "dice-one" + dice_six = "dice-six" + dice_three = "dice-three" + dice_two = "dice-two" + digg = "digg" + digital_ocean = "digital-ocean" + digital_tachograph = "digital-tachograph" + directions = "directions" + discord = "discord" + discourse = "discourse" + disease = "disease" + divide = "divide" + dizzy = "dizzy" + dna = "dna" + dochub = "dochub" + docker = "docker" + dog = "dog" + dollar_sign = "dollar-sign" + dolly = "dolly" + dolly_flatbed = "dolly-flatbed" + donate = "donate" + door_closed = "door-closed" + door_open = "door-open" + dot_circle = "dot-circle" + dove = "dove" + download = "download" + draft2digital = "draft2digital" + drafting_compass = "drafting-compass" + dragon = "dragon" + draw_polygon = "draw-polygon" + dribbble = "dribbble" + dribbble_square = "dribbble-square" + dropbox = "dropbox" + drum = "drum" + drum_steelpan = "drum-steelpan" + drumstick_bite = "drumstick-bite" + drupal = "drupal" + dumbbell = "dumbbell" + dumpster = "dumpster" + dumpster_fire = "dumpster-fire" + dungeon = "dungeon" + dyalog = "dyalog" + earlybirds = "earlybirds" + ebay = "ebay" + edge = "edge" + edge_legacy = "edge-legacy" + edit = "edit" + egg = "egg" + eject = "eject" + elementor = "elementor" + ellipsis_h = "ellipsis-h" + ellipsis_v = "ellipsis-v" + ello = "ello" + ember = "ember" + empire = "empire" + envelope = "envelope" + envelope_open = "envelope-open" + envelope_open_text = "envelope-open-text" + envelope_square = "envelope-square" + envira = "envira" + equals = "equals" + eraser = "eraser" + erlang = "erlang" + ethereum = "ethereum" + ethernet = "ethernet" + etsy = "etsy" + euro_sign = "euro-sign" + evernote = "evernote" + exchange_alt = "exchange-alt" + exclamation = "exclamation" + exclamation_circle = "exclamation-circle" + exclamation_triangle = "exclamation-triangle" + expand = "expand" + expand_alt = "expand-alt" + expand_arrows_alt = "expand-arrows-alt" + expeditedssl = "expeditedssl" + external_link_alt = "external-link-alt" + external_link_square_alt = "external-link-square-alt" + eye = "eye" + eye_dropper = "eye-dropper" + eye_slash = "eye-slash" + facebook = "facebook" + facebook_f = "facebook-f" + facebook_messenger = "facebook-messenger" + facebook_square = "facebook-square" + fan = "fan" + fantasy_flight_games = "fantasy-flight-games" + fast_backward = "fast-backward" + fast_forward = "fast-forward" + faucet = "faucet" + fax = "fax" + feather = "feather" + feather_alt = "feather-alt" + fedex = "fedex" + fedora = "fedora" + female = "female" + fighter_jet = "fighter-jet" + figma = "figma" + file = "file" + file_alt = "file-alt" + file_archive = "file-archive" + file_audio = "file-audio" + file_code = "file-code" + file_contract = "file-contract" + file_csv = "file-csv" + file_download = "file-download" + file_excel = "file-excel" + file_export = "file-export" + file_image = "file-image" + file_import = "file-import" + file_invoice = "file-invoice" + file_invoice_dollar = "file-invoice-dollar" + file_medical = "file-medical" + file_medical_alt = "file-medical-alt" + file_pdf = "file-pdf" + file_powerpoint = "file-powerpoint" + file_prescription = "file-prescription" + file_signature = "file-signature" + file_upload = "file-upload" + file_video = "file-video" + file_word = "file-word" + fill = "fill" + fill_drip = "fill-drip" + film = "film" + filter = "filter" + fingerprint = "fingerprint" + fire = "fire" + fire_alt = "fire-alt" + fire_extinguisher = "fire-extinguisher" + firefox = "firefox" + firefox_browser = "firefox-browser" + first_aid = "first-aid" + first_order = "first-order" + first_order_alt = "first-order-alt" + firstdraft = "firstdraft" + fish = "fish" + fist_raised = "fist-raised" + flag = "flag" + flag_checkered = "flag-checkered" + flag_usa = "flag-usa" + flask = "flask" + flickr = "flickr" + flipboard = "flipboard" + flushed = "flushed" + fly = "fly" + folder = "folder" + folder_minus = "folder-minus" + folder_open = "folder-open" + folder_plus = "folder-plus" + font = "font" + font_awesome = "font-awesome" + font_awesome_alt = "font-awesome-alt" + font_awesome_flag = "font-awesome-flag" + font_awesome_logo_full = "font-awesome-logo-full" + fonticons = "fonticons" + fonticons_fi = "fonticons-fi" + football_ball = "football-ball" + fort_awesome = "fort-awesome" + fort_awesome_alt = "fort-awesome-alt" + forumbee = "forumbee" + forward = "forward" + foursquare = "foursquare" + free_code_camp = "free-code-camp" + freebsd = "freebsd" + frog = "frog" + frown = "frown" + frown_open = "frown-open" + fulcrum = "fulcrum" + funnel_dollar = "funnel-dollar" + futbol = "futbol" + galactic_republic = "galactic-republic" + galactic_senate = "galactic-senate" + gamepad = "gamepad" + gas_pump = "gas-pump" + gavel = "gavel" + gem = "gem" + genderless = "genderless" + get_pocket = "get-pocket" + gg = "gg" + gg_circle = "gg-circle" + ghost = "ghost" + gift = "gift" + gifts = "gifts" + git = "git" + git_alt = "git-alt" + git_square = "git-square" + github = "github" + github_alt = "github-alt" + github_square = "github-square" + gitkraken = "gitkraken" + gitlab = "gitlab" + gitter = "gitter" + glass_cheers = "glass-cheers" + glass_martini = "glass-martini" + glass_martini_alt = "glass-martini-alt" + glass_whiskey = "glass-whiskey" + glasses = "glasses" + glide = "glide" + glide_g = "glide-g" + globe = "globe" + globe_africa = "globe-africa" + globe_americas = "globe-americas" + globe_asia = "globe-asia" + globe_europe = "globe-europe" + gofore = "gofore" + golf_ball = "golf-ball" + goodreads = "goodreads" + goodreads_g = "goodreads-g" + google = "google" + google_drive = "google-drive" + google_pay = "google-pay" + google_play = "google-play" + google_plus = "google-plus" + google_plus_g = "google-plus-g" + google_plus_square = "google-plus-square" + google_wallet = "google-wallet" + gopuram = "gopuram" + graduation_cap = "graduation-cap" + gratipay = "gratipay" + grav = "grav" + greater_than = "greater-than" + greater_than_equal = "greater-than-equal" + grimace = "grimace" + grin = "grin" + grin_alt = "grin-alt" + grin_beam = "grin-beam" + grin_beam_sweat = "grin-beam-sweat" + grin_hearts = "grin-hearts" + grin_squint = "grin-squint" + grin_squint_tears = "grin-squint-tears" + grin_stars = "grin-stars" + grin_tears = "grin-tears" + grin_tongue = "grin-tongue" + grin_tongue_squint = "grin-tongue-squint" + grin_tongue_wink = "grin-tongue-wink" + grin_wink = "grin-wink" + grip_horizontal = "grip-horizontal" + grip_lines = "grip-lines" + grip_lines_vertical = "grip-lines-vertical" + grip_vertical = "grip-vertical" + gripfire = "gripfire" + grunt = "grunt" + guilded = "guilded" + guitar = "guitar" + gulp = "gulp" + h_square = "h-square" + hacker_news = "hacker-news" + hacker_news_square = "hacker-news-square" + hackerrank = "hackerrank" + hamburger = "hamburger" + hammer = "hammer" + hamsa = "hamsa" + hand_holding = "hand-holding" + hand_holding_heart = "hand-holding-heart" + hand_holding_medical = "hand-holding-medical" + hand_holding_usd = "hand-holding-usd" + hand_holding_water = "hand-holding-water" + hand_lizard = "hand-lizard" + hand_middle_finger = "hand-middle-finger" + hand_paper = "hand-paper" + hand_peace = "hand-peace" + hand_point_down = "hand-point-down" + hand_point_left = "hand-point-left" + hand_point_right = "hand-point-right" + hand_point_up = "hand-point-up" + hand_pointer = "hand-pointer" + hand_rock = "hand-rock" + hand_scissors = "hand-scissors" + hand_sparkles = "hand-sparkles" + hand_spock = "hand-spock" + hands = "hands" + hands_helping = "hands-helping" + hands_wash = "hands-wash" + handshake = "handshake" + handshake_alt_slash = "handshake-alt-slash" + handshake_slash = "handshake-slash" + hanukiah = "hanukiah" + hard_hat = "hard-hat" + hashtag = "hashtag" + hat_cowboy = "hat-cowboy" + hat_cowboy_side = "hat-cowboy-side" + hat_wizard = "hat-wizard" + hdd = "hdd" + head_side_cough = "head-side-cough" + head_side_cough_slash = "head-side-cough-slash" + head_side_mask = "head-side-mask" + head_side_virus = "head-side-virus" + heading = "heading" + headphones = "headphones" + headphones_alt = "headphones-alt" + headset = "headset" + heart = "heart" + heart_broken = "heart-broken" + heartbeat = "heartbeat" + helicopter = "helicopter" + highlighter = "highlighter" + hiking = "hiking" + hippo = "hippo" + hips = "hips" + hire_a_helper = "hire-a-helper" + history = "history" + hive = "hive" + hockey_puck = "hockey-puck" + holly_berry = "holly-berry" + home = "home" + hooli = "hooli" + hornbill = "hornbill" + horse = "horse" + horse_head = "horse-head" + hospital = "hospital" + hospital_alt = "hospital-alt" + hospital_symbol = "hospital-symbol" + hospital_user = "hospital-user" + hot_tub = "hot-tub" + hotdog = "hotdog" + hotel = "hotel" + hotjar = "hotjar" + hourglass = "hourglass" + hourglass_end = "hourglass-end" + hourglass_half = "hourglass-half" + hourglass_start = "hourglass-start" + house_damage = "house-damage" + house_user = "house-user" + houzz = "houzz" + hryvnia = "hryvnia" + htmla5 = "html5" + hubspot = "hubspot" + i_cursor = "i-cursor" + ice_cream = "ice-cream" + icicles = "icicles" + icons = "icons" + id_badge = "id-badge" + id_card = "id-card" + id_card_alt = "id-card-alt" + ideal = "ideal" + igloo = "igloo" + image = "image" + images = "images" + imdb = "imdb" + inbox = "inbox" + indent = "indent" + industry = "industry" + infinity = "infinity" + info = "info" + info_circle = "info-circle" + innosoft = "innosoft" + instagram = "instagram" + instagram_square = "instagram-square" + instalod = "instalod" + intercom = "intercom" + internet_explorer = "internet-explorer" + invision = "invision" + ioxhost = "ioxhost" + italic = "italic" + itch_io = "itch-io" + itunes = "itunes" + itunes_note = "itunes-note" + java = "java" + jedi = "jedi" + jedi_order = "jedi-order" + jenkins = "jenkins" + jira = "jira" + joget = "joget" + joint = "joint" + joomla = "joomla" + journal_whills = "journal-whills" + js = "js" + js_square = "js-square" + jsfiddle = "jsfiddle" + kaaba = "kaaba" + kaggle = "kaggle" + key = "key" + keybase = "keybase" + keyboard = "keyboard" + keycdn = "keycdn" + khanda = "khanda" + kickstarter = "kickstarter" + kickstarter_k = "kickstarter-k" + kiss = "kiss" + kiss_beam = "kiss-beam" + kiss_wink_heart = "kiss-wink-heart" + kiwi_bird = "kiwi-bird" + korvue = "korvue" + landmark = "landmark" + language = "language" + laptop = "laptop" + laptop_code = "laptop-code" + laptop_house = "laptop-house" + laptop_medical = "laptop-medical" + laravel = "laravel" + lastfm = "lastfm" + lastfm_square = "lastfm-square" + laugh = "laugh" + laugh_beam = "laugh-beam" + laugh_squint = "laugh-squint" + laugh_wink = "laugh-wink" + layer_group = "layer-group" + leaf = "leaf" + leanpub = "leanpub" + lemon = "lemon" + less = "less" + less_than = "less-than" + less_than_equal = "less-than-equal" + level_down_alt = "level-down-alt" + level_up_alt = "level-up-alt" + life_ring = "life-ring" + lightbulb = "lightbulb" + line = "line" + link = "link" + linkedin = "linkedin" + linkedin_in = "linkedin-in" + linode = "linode" + linux = "linux" + lira_sign = "lira-sign" + list = "list" + list_alt = "list-alt" + list_ol = "list-ol" + list_ul = "list-ul" + location_arrow = "location-arrow" + lock = "lock" + lock_open = "lock-open" + long_arrow_alt_down = "long-arrow-alt-down" + long_arrow_alt_left = "long-arrow-alt-left" + long_arrow_alt_right = "long-arrow-alt-right" + long_arrow_alt_up = "long-arrow-alt-up" + low_vision = "low-vision" + luggage_cart = "luggage-cart" + lungs = "lungs" + lungs_virus = "lungs-virus" + lyft = "lyft" + magento = "magento" + magic = "magic" + magnet = "magnet" + mail_bulk = "mail-bulk" + mailchimp = "mailchimp" + male = "male" + mandalorian = "mandalorian" + map = "map" + map_marked = "map-marked" + map_marked_alt = "map-marked-alt" + map_marker = "map-marker" + map_marker_alt = "map-marker-alt" + map_pin = "map-pin" + map_signs = "map-signs" + markdown = "markdown" + marker = "marker" + mars = "mars" + mars_double = "mars-double" + mars_stroke = "mars-stroke" + mars_stroke_h = "mars-stroke-h" + mars_stroke_v = "mars-stroke-v" + mask = "mask" + mastodon = "mastodon" + maxcdn = "maxcdn" + mdb = "mdb" + medal = "medal" + medapps = "medapps" + medium = "medium" + medium_m = "medium-m" + medkit = "medkit" + medrt = "medrt" + meetup = "meetup" + megaport = "megaport" + meh = "meh" + meh_blank = "meh-blank" + meh_rolling_eyes = "meh-rolling-eyes" + memory = "memory" + mendeley = "mendeley" + menorah = "menorah" + mercury = "mercury" + meteor = "meteor" + microblog = "microblog" + microchip = "microchip" + microphone = "microphone" + microphone_alt = "microphone-alt" + microphone_alt_slash = "microphone-alt-slash" + microphone_slash = "microphone-slash" + microscope = "microscope" + microsoft = "microsoft" + minus = "minus" + minus_circle = "minus-circle" + minus_square = "minus-square" + mitten = "mitten" + mix = "mix" + mixcloud = "mixcloud" + mixer = "mixer" + mizuni = "mizuni" + mobile = "mobile" + mobile_alt = "mobile-alt" + modx = "modx" + monero = "monero" + money_bill = "money-bill" + money_bill_alt = "money-bill-alt" + money_bill_wave = "money-bill-wave" + money_bill_wave_alt = "money-bill-wave-alt" + money_check = "money-check" + money_check_alt = "money-check-alt" + monument = "monument" + moon = "moon" + mortar_pestle = "mortar-pestle" + mosque = "mosque" + motorcycle = "motorcycle" + mountain = "mountain" + mouse = "mouse" + mouse_pointer = "mouse-pointer" + mug_hot = "mug-hot" + music = "music" + napster = "napster" + neos = "neos" + network_wired = "network-wired" + neuter = "neuter" + newspaper = "newspaper" + nimblr = "nimblr" + node = "node" + node_js = "node-js" + not_equal = "not-equal" + notes_medical = "notes-medical" + npm = "npm" + ns8 = "ns8" + nutritionix = "nutritionix" + object_group = "object-group" + object_ungroup = "object-ungroup" + octopus_deploy = "octopus-deploy" + odnoklassniki = "odnoklassniki" + odnoklassniki_square = "odnoklassniki-square" + oil_can = "oil-can" + old_republic = "old-republic" + om = "om" + opencart = "opencart" + openid = "openid" + opera = "opera" + optin_monster = "optin-monster" + orcid = "orcid" + osi = "osi" + otter = "otter" + outdent = "outdent" + page4 = "page4" + pagelines = "pagelines" + pager = "pager" + paint_brush = "paint-brush" + paint_roller = "paint-roller" + palette = "palette" + palfed = "palfed" + pallet = "pallet" + paper_plane = "paper-plane" + paperclip = "paperclip" + parachute_box = "parachute-box" + paragraph = "paragraph" + parking = "parking" + passport = "passport" + pastafarianism = "pastafarianism" + paste = "paste" + patreon = "patreon" + pause = "pause" + pause_circle = "pause-circle" + paw = "paw" + paypal = "paypal" + peace = "peace" + pen = "pen" + pen_alt = "pen-alt" + pen_fancy = "pen-fancy" + pen_nib = "pen-nib" + pen_square = "pen-square" + pencil_alt = "pencil-alt" + pencil_ruler = "pencil-ruler" + penny_arcade = "penny-arcade" + people_arrows = "people-arrows" + people_carry = "people-carry" + pepper_hot = "pepper-hot" + perbyte = "perbyte" + percent = "percent" + percentage = "percentage" + periscope = "periscope" + person_booth = "person-booth" + phabricator = "phabricator" + phoenix_framework = "phoenix-framework" + phoenix_squadron = "phoenix-squadron" + phone = "phone" + phone_alt = "phone-alt" + phone_slash = "phone-slash" + phone_square = "phone-square" + phone_square_alt = "phone-square-alt" + phone_volume = "phone-volume" + photo_video = "photo-video" + php = "php" + pied_piper = "pied-piper" + pied_piper_alt = "pied-piper-alt" + pied_piper_hat = "pied-piper-hat" + pied_piper_pp = "pied-piper-pp" + pied_piper_square = "pied-piper-square" + piggy_bank = "piggy-bank" + pills = "pills" + pinterest = "pinterest" + pinterest_p = "pinterest-p" + pinterest_square = "pinterest-square" + pizza_slice = "pizza-slice" + place_of_worship = "place-of-worship" + plane = "plane" + plane_arrival = "plane-arrival" + plane_departure = "plane-departure" + plane_slash = "plane-slash" + play = "play" + play_circle = "play-circle" + playstation = "playstation" + plug = "plug" + plus = "plus" + plus_circle = "plus-circle" + plus_square = "plus-square" + podcast = "podcast" + poll = "poll" + poll_h = "poll-h" + poo = "poo" + poo_storm = "poo-storm" + poop = "poop" + portrait = "portrait" + pound_sign = "pound-sign" + power_off = "power-off" + pray = "pray" + praying_hands = "praying-hands" + prescription = "prescription" + prescription_bottle = "prescription-bottle" + prescription_bottle_alt = "prescription-bottle-alt" + print = "print" + procedures = "procedures" + product_hunt = "product-hunt" + project_diagram = "project-diagram" + pump_medical = "pump-medical" + pump_soap = "pump-soap" + pushed = "pushed" + puzzle_piece = "puzzle-piece" + python = "python" + qq = "qq" + qrcode = "qrcode" + question = "question" + question_circle = "question-circle" + quidditch = "quidditch" + quinscape = "quinscape" + quora = "quora" + quote_left = "quote-left" + quote_right = "quote-right" + quran = "quran" + r_project = "r-project" + radiation = "radiation" + radiation_alt = "radiation-alt" + rainbow = "rainbow" + random = "random" + raspberry_pi = "raspberry-pi" + ravelry = "ravelry" + react = "react" + reacteurope = "reacteurope" + readme = "readme" + rebel = "rebel" + receipt = "receipt" + record_vinyl = "record-vinyl" + recycle = "recycle" + red_river = "red-river" + reddit = "reddit" + reddit_alien = "reddit-alien" + reddit_square = "reddit-square" + redhat = "redhat" + redo = "redo" + redo_alt = "redo-alt" + registered = "registered" + remove_format = "remove-format" + renren = "renren" + reply = "reply" + reply_all = "reply-all" + replyd = "replyd" + republican = "republican" + researchgate = "researchgate" + resolving = "resolving" + restroom = "restroom" + retweet = "retweet" + rev = "rev" + ribbon = "ribbon" + ring = "ring" + road = "road" + robot = "robot" + rocket = "rocket" + rocketchat = "rocketchat" + rockrms = "rockrms" + route = "route" + rss = "rss" + rss_square = "rss-square" + ruble_sign = "ruble-sign" + ruler = "ruler" + ruler_combined = "ruler-combined" + ruler_horizontal = "ruler-horizontal" + ruler_vertical = "ruler-vertical" + running = "running" + rupee_sign = "rupee-sign" + rust = "rust" + sad_cry = "sad-cry" + sad_tear = "sad-tear" + safari = "safari" + salesforce = "salesforce" + sass = "sass" + satellite = "satellite" + satellite_dish = "satellite-dish" + save = "save" + schlix = "schlix" + school = "school" + screwdriver = "screwdriver" + scribd = "scribd" + scroll = "scroll" + sd_card = "sd-card" + search = "search" + search_dollar = "search-dollar" + search_location = "search-location" + search_minus = "search-minus" + search_plus = "search-plus" + searchengin = "searchengin" + seedling = "seedling" + sellcast = "sellcast" + sellsy = "sellsy" + server = "server" + servicestack = "servicestack" + shapes = "shapes" + share = "share" + share_alt = "share-alt" + share_alt_square = "share-alt-square" + share_square = "share-square" + shekel_sign = "shekel-sign" + shield_alt = "shield-alt" + shield_virus = "shield-virus" + ship = "ship" + shipping_fast = "shipping-fast" + shirtsinbulk = "shirtsinbulk" + shoe_prints = "shoe-prints" + shopify = "shopify" + shopping_bag = "shopping-bag" + shopping_basket = "shopping-basket" + shopping_cart = "shopping-cart" + shopware = "shopware" + shower = "shower" + shuttle_van = "shuttle-van" + sign = "sign" + sign_in_alt = "sign-in-alt" + sign_language = "sign-language" + sign_out_alt = "sign-out-alt" + signal = "signal" + signature = "signature" + sim_card = "sim-card" + simplybuilt = "simplybuilt" + sink = "sink" + sistrix = "sistrix" + sitemap = "sitemap" + sith = "sith" + skating = "skating" + sketch = "sketch" + skiing = "skiing" + skiing_nordic = "skiing-nordic" + skull = "skull" + skull_crossbones = "skull-crossbones" + skyatlas = "skyatlas" + skype = "skype" + slack = "slack" + slack_hash = "slack-hash" + slash = "slash" + sleigh = "sleigh" + sliders_h = "sliders-h" + slideshare = "slideshare" + smile = "smile" + smile_beam = "smile-beam" + smile_wink = "smile-wink" + smog = "smog" + smoking = "smoking" + smoking_ban = "smoking-ban" + sms = "sms" + snapchat = "snapchat" + snapchat_ghost = "snapchat-ghost" + snapchat_square = "snapchat-square" + snowboarding = "snowboarding" + snowflake = "snowflake" + snowman = "snowman" + snowplow = "snowplow" + soap = "soap" + socks = "socks" + solar_panel = "solar-panel" + sort = "sort" + sort_alpha_down = "sort-alpha-down" + sort_alpha_down_alt = "sort-alpha-down-alt" + sort_alpha_up = "sort-alpha-up" + sort_alpha_up_alt = "sort-alpha-up-alt" + sort_amount_down = "sort-amount-down" + sort_amount_down_alt = "sort-amount-down-alt" + sort_amount_up = "sort-amount-up" + sort_amount_up_alt = "sort-amount-up-alt" + sort_down = "sort-down" + sort_numeric_down = "sort-numeric-down" + sort_numeric_down_alt = "sort-numeric-down-alt" + sort_numeric_up = "sort-numeric-up" + sort_numeric_up_alt = "sort-numeric-up-alt" + sort_up = "sort-up" + soundcloud = "soundcloud" + sourcetree = "sourcetree" + spa = "spa" + space_shuttle = "space-shuttle" + speakap = "speakap" + speaker_deck = "speaker-deck" + spell_check = "spell-check" + spider = "spider" + spinner = "spinner" + splotch = "splotch" + spotify = "spotify" + spray_can = "spray-can" + square = "square" + square_full = "square-full" + square_root_alt = "square-root-alt" + squarespace = "squarespace" + stack_exchange = "stack-exchange" + stack_overflow = "stack-overflow" + stackpath = "stackpath" + stamp = "stamp" + star = "star" + star_and_crescent = "star-and-crescent" + star_half = "star-half" + star_half_alt = "star-half-alt" + star_of_david = "star-of-david" + star_of_life = "star-of-life" + staylinked = "staylinked" + steam = "steam" + steam_square = "steam-square" + steam_symbol = "steam-symbol" + step_backward = "step-backward" + step_forward = "step-forward" + stethoscope = "stethoscope" + sticker_mule = "sticker-mule" + sticky_note = "sticky-note" + stop = "stop" + stop_circle = "stop-circle" + stopwatch = "stopwatch" + stopwatch_20 = "stopwatch-20" + store = "store" + store_alt = "store-alt" + store_alt_slash = "store-alt-slash" + store_slash = "store-slash" + strava = "strava" + stream = "stream" + street_view = "street-view" + strikethrough = "strikethrough" + stripe = "stripe" + stripe_s = "stripe-s" + stroopwafel = "stroopwafel" + studiovinari = "studiovinari" + stumbleupon = "stumbleupon" + stumbleupon_circle = "stumbleupon-circle" + subscript = "subscript" + subway = "subway" + suitcase = "suitcase" + suitcase_rolling = "suitcase-rolling" + sun = "sun" + superpowers = "superpowers" + superscript = "superscript" + supple = "supple" + surprise = "surprise" + suse = "suse" + swatchbook = "swatchbook" + swift = "swift" + swimmer = "swimmer" + swimming_pool = "swimming-pool" + symfony = "symfony" + synagogue = "synagogue" + sync = "sync" + sync_alt = "sync-alt" + syringe = "syringe" + table = "table" + table_tennis = "table-tennis" + tablet = "tablet" + tablet_alt = "tablet-alt" + tablets = "tablets" + tachometer_alt = "tachometer-alt" + tag = "tag" + tags = "tags" + tape = "tape" + tasks = "tasks" + taxi = "taxi" + teamspeak = "teamspeak" + teeth = "teeth" + teeth_open = "teeth-open" + telegram = "telegram" + telegram_plane = "telegram-plane" + temperature_high = "temperature-high" + temperature_low = "temperature-low" + tencent_weibo = "tencent-weibo" + tenge = "tenge" + terminal = "terminal" + text_height = "text-height" + text_width = "text-width" + th = "th" + th_large = "th-large" + th_list = "th-list" + the_red_yeti = "the-red-yeti" + theater_masks = "theater-masks" + themeco = "themeco" + themeisle = "themeisle" + thermometer = "thermometer" + thermometer_empty = "thermometer-empty" + thermometer_full = "thermometer-full" + thermometer_half = "thermometer-half" + thermometer_quarter = "thermometer-quarter" + thermometer_three_quarters = "thermometer-three-quarters" + think_peaks = "think-peaks" + thumbs_down = "thumbs-down" + thumbs_up = "thumbs-up" + thumbtack = "thumbtack" + ticket_alt = "ticket-alt" + tiktok = "tiktok" + times = "times" + times_circle = "times-circle" + tint = "tint" + tint_slash = "tint-slash" + tired = "tired" + toggle_off = "toggle-off" + toggle_on = "toggle-on" + toilet = "toilet" + toilet_paper = "toilet-paper" + toilet_paper_slash = "toilet-paper-slash" + toolbox = "toolbox" + tools = "tools" + tooth = "tooth" + torah = "torah" + torii_gate = "torii-gate" + tractor = "tractor" + trade_federation = "trade-federation" + trademark = "trademark" + traffic_light = "traffic-light" + trailer = "trailer" + train = "train" + tram = "tram" + transgender = "transgender" + transgender_alt = "transgender-alt" + trash = "trash" + trash_alt = "trash-alt" + trash_restore = "trash-restore" + trash_restore_alt = "trash-restore-alt" + tree = "tree" + trello = "trello" + tripadvisor = "tripadvisor" + trophy = "trophy" + truck = "truck" + truck_loading = "truck-loading" + truck_monster = "truck-monster" + truck_moving = "truck-moving" + truck_pickup = "truck-pickup" + tshirt = "tshirt" + tty = "tty" + tumblr = "tumblr" + tumblr_square = "tumblr-square" + tv = "tv" + twitch = "twitch" + twitter = "twitter" + twitter_square = "twitter-square" + typo3 = "typo3" + uber = "uber" + ubuntu = "ubuntu" + uikit = "uikit" + umbraco = "umbraco" + umbrella = "umbrella" + umbrella_beach = "umbrella-beach" + uncharted = "uncharted" + underline = "underline" + undo = "undo" + undo_alt = "undo-alt" + uniregistry = "uniregistry" + unity = "unity" + universal_access = "universal-access" + university = "university" + unlink = "unlink" + unlock = "unlock" + unlock_alt = "unlock-alt" + unsplash = "unsplash" + untappd = "untappd" + upload = "upload" + ups = "ups" + usb = "usb" + user = "user" + user_alt = "user-alt" + user_alt_slash = "user-alt-slash" + user_astronaut = "user-astronaut" + user_check = "user-check" + user_circle = "user-circle" + user_clock = "user-clock" + user_cog = "user-cog" + user_edit = "user-edit" + user_friends = "user-friends" + user_graduate = "user-graduate" + user_injured = "user-injured" + user_lock = "user-lock" + user_md = "user-md" + user_minus = "user-minus" + user_ninja = "user-ninja" + user_nurse = "user-nurse" + user_plus = "user-plus" + user_secret = "user-secret" + user_shield = "user-shield" + user_slash = "user-slash" + user_tag = "user-tag" + user_tie = "user-tie" + user_times = "user-times" + users = "users" + users_cog = "users-cog" + users_slash = "users-slash" + usps = "usps" + ussunnah = "ussunnah" + utensil_spoon = "utensil-spoon" + utensils = "utensils" + vaadin = "vaadin" + vector_square = "vector-square" + venus = "venus" + venus_double = "venus-double" + venus_mars = "venus-mars" + vest = "vest" + vest_patches = "vest-patches" + viacoin = "viacoin" + viadeo = "viadeo" + viadeo_square = "viadeo-square" + vial = "vial" + vials = "vials" + viber = "viber" + video = "video" + video_slash = "video-slash" + vihara = "vihara" + vimeo = "vimeo" + vimeo_square = "vimeo-square" + vimeo_v = "vimeo-v" + vine = "vine" + virus = "virus" + virus_slash = "virus-slash" + viruses = "viruses" + vk = "vk" + vnv = "vnv" + voicemail = "voicemail" + volleyball_ball = "volleyball-ball" + volume_down = "volume-down" + volume_mute = "volume-mute" + volume_off = "volume-off" + volume_up = "volume-up" + vote_yea = "vote-yea" + vr_cardboard = "vr-cardboard" + vuejs = "vuejs" + walking = "walking" + wallet = "wallet" + warehouse = "warehouse" + watchman_monitoring = "watchman-monitoring" + water = "water" + wave_square = "wave-square" + waze = "waze" + weebly = "weebly" + weibo = "weibo" + weight = "weight" + weight_hanging = "weight-hanging" + weixin = "weixin" + whatsapp = "whatsapp" + whatsapp_square = "whatsapp-square" + wheelchair = "wheelchair" + whmcs = "whmcs" + wifi = "wifi" + wikipedia_w = "wikipedia-w" + wind = "wind" + window_close = "window-close" + window_maximize = "window-maximize" + window_minimize = "window-minimize" + window_restore = "window-restore" + windows = "windows" + wine_bottle = "wine-bottle" + wine_glass = "wine-glass" + wine_glass_alt = "wine-glass-alt" + wix = "wix" + wizards_of_the_coast = "wizards-of-the-coast" + wodu = "wodu" + wolf_pack_battalion = "wolf-pack-battalion" + won_sign = "won-sign" + wordpress = "wordpress" + wordpress_simple = "wordpress-simple" + wpbeginner = "wpbeginner" + wpexplorer = "wpexplorer" + wpforms = "wpforms" + wpressr = "wpressr" + wrench = "wrench" + x_ray = "x-ray" + xbox = "xbox" + xing = "xing" + xing_square = "xing-square" + y_combinator = "y-combinator" + yahoo = "yahoo" + yammer = "yammer" + yandex = "yandex" + yandex_international = "yandex-international" + yarn = "yarn" + yelp = "yelp" + yen_sign = "yen-sign" + yin_yang = "yin-yang" + yoast = "yoast" + youtube = "youtube" + youtube_square = "youtube-square" + zhihu = "zhihu" diff --git a/pcdswidgets/builder/ui_main_widget.j2 b/pcdswidgets/builder/ui_main_widget.j2 index 6184de0..b4b1aa5 100644 --- a/pcdswidgets/builder/ui_main_widget.j2 +++ b/pcdswidgets/builder/ui_main_widget.j2 @@ -5,6 +5,7 @@ This file can be safely edited to change the runtime behavior of the widget. """ from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.builder.icon_options import IconOptions from {{ absolute_import_path }} import {{ base_cls }} @@ -12,5 +13,5 @@ class {{ main_cls }}({{ base_cls }}): designer_options = DesignerOptions( group="{{ default_group }}", is_container=False, - icon=None, + icon=IconOptions.NONE, ) diff --git a/pcdswidgets/motion/common/motor_classic_full.py b/pcdswidgets/motion/common/motor_classic_full.py index 74d8ce3..8d15977 100644 --- a/pcdswidgets/motion/common/motor_classic_full.py +++ b/pcdswidgets/motion/common/motor_classic_full.py @@ -5,6 +5,7 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.builder.icon_options import IconOptions from pcdswidgets.generated.motion.common.motor_classic_full_base import MotorClassicFullBase @@ -12,5 +13,5 @@ class MotorClassicFull(MotorClassicFullBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, - icon=None, + icon=IconOptions.NONE, ) diff --git a/pcdswidgets/motion/common/motor_classic_row.py b/pcdswidgets/motion/common/motor_classic_row.py index b0ed9d6..247fb81 100644 --- a/pcdswidgets/motion/common/motor_classic_row.py +++ b/pcdswidgets/motion/common/motor_classic_row.py @@ -5,6 +5,7 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.builder.icon_options import IconOptions from pcdswidgets.generated.motion.common.motor_classic_row_base import MotorClassicRowBase @@ -12,5 +13,5 @@ class MotorClassicRow(MotorClassicRowBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, - icon=None, + icon=IconOptions.NONE, ) diff --git a/pcdswidgets/motion/common/motor_tc_classic_row.py b/pcdswidgets/motion/common/motor_tc_classic_row.py index 81a8034..2279d6b 100644 --- a/pcdswidgets/motion/common/motor_tc_classic_row.py +++ b/pcdswidgets/motion/common/motor_tc_classic_row.py @@ -5,6 +5,7 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.builder.icon_options import IconOptions from pcdswidgets.generated.motion.common.motor_tc_classic_row_base import MotorTcClassicRowBase @@ -12,5 +13,5 @@ class MotorTcClassicRow(MotorTcClassicRowBase): designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, - icon=None, + icon=IconOptions.NONE, ) diff --git a/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py index 28d2a70..3ba628f 100644 --- a/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py +++ b/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py @@ -5,6 +5,7 @@ """ from pcdswidgets.builder.designer_options import DesignerOptions +from pcdswidgets.builder.icon_options import IconOptions from pcdswidgets.generated.motion.smaract.smaract_open_loop_classic_row_base import SmaractOpenLoopClassicRowBase @@ -12,5 +13,5 @@ class SmaractOpenLoopClassicRow(SmaractOpenLoopClassicRowBase): designer_options = DesignerOptions( group="ECS Motion Smaract", is_container=False, - icon=None, + icon=IconOptions.NONE, ) From 28532fb1314074d7bece5a789c5620a2ada02785 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Mon, 13 Apr 2026 18:21:36 -0700 Subject: [PATCH 095/104] ENH: add a quick gui that launches and can show all the available icon options --- pcdswidgets/builder/get_icon_options.py | 57 ++++++++++++++++++++++++- show_icon_options.sh | 15 +++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100755 show_icon_options.sh diff --git a/pcdswidgets/builder/get_icon_options.py b/pcdswidgets/builder/get_icon_options.py index dca76d4..9966407 100644 --- a/pcdswidgets/builder/get_icon_options.py +++ b/pcdswidgets/builder/get_icon_options.py @@ -4,11 +4,58 @@ This helps us figure out what options exist for designer icons as provided by pydm. """ +import argparse import json from pathlib import Path from jinja2 import Environment, PackageLoader from pydm.utilities import iconfont +from qtpy.QtCore import QSize +from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QMainWindow, QScrollArea, QVBoxLayout, QWidget + + +def show_icon_options(): + app = QApplication([]) + main_window = QMainWindow() + scroll_area = QScrollArea() + main_widget = QWidget() + main_layout = QGridLayout() + + main_window.setCentralWidget(scroll_area) + scroll_area.setWidgetResizable(True) + scroll_area.setWidget(main_widget) + main_widget.setLayout(main_layout) + + cols = 5 + curr = 0 + row = 0 + + ifont = iconfont.IconFont() + + for icon_name in get_icon_options(): + icon = ifont.icon(icon_name) + if icon is None: + continue + + icon_widget = QWidget() + icon_layout = QVBoxLayout() + icon_image = QLabel() + icon_text = QLabel(icon_name) + + icon_widget.setLayout(icon_layout) + icon_image.setPixmap(icon.pixmap(QSize(32, 32))) + icon_layout.addWidget(icon_image) + icon_layout.addWidget(icon_text) + + main_layout.addWidget(icon_widget, row, curr) + curr += 1 + if curr >= cols: + curr = 0 + row += 1 + + main_window.resize(800, 600) + main_window.show() + app.exec_() def generate_icon_options(): @@ -31,4 +78,12 @@ def get_icon_options() -> list[str]: if __name__ == "__main__": - generate_icon_options() + parser = argparse.ArgumentParser() + parser.add_argument("mode", choices=("show", "build"), default="show") + args = parser.parse_args() + if args.mode == "show": + show_icon_options() + elif args.mode == "build": + generate_icon_options() + else: + raise RuntimeError(f"Invalid option {args.mode}") diff --git a/show_icon_options.sh b/show_icon_options.sh new file mode 100755 index 0000000..e757a06 --- /dev/null +++ b/show_icon_options.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# shellcheck disable=SC1091 +# +# Open a window with all built-in designer icon options. +# +set -e + +THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +cd "${THIS_DIR}" + +unset PYTHONPATH +source base_env_vars.sh +source .venv/bin/activate + +python -m pcdswidgets.builder.get_icon_options show From d4166841e659c1f3200e19c3845435db2813f37f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 14 Apr 2026 10:52:33 -0700 Subject: [PATCH 096/104] FIX: expose only valid icons that render cleanly --- pcdswidgets/builder/get_icon_options.py | 14 +- pcdswidgets/builder/icon_options.j2 | 2 +- pcdswidgets/builder/icon_options.py | 457 ----------------------- pcdswidgets/builder/valid_glyph_map.json | 2 + 4 files changed, 15 insertions(+), 460 deletions(-) create mode 100644 pcdswidgets/builder/valid_glyph_map.json diff --git a/pcdswidgets/builder/get_icon_options.py b/pcdswidgets/builder/get_icon_options.py index 9966407..b8f1f54 100644 --- a/pcdswidgets/builder/get_icon_options.py +++ b/pcdswidgets/builder/get_icon_options.py @@ -53,7 +53,7 @@ def show_icon_options(): curr = 0 row += 1 - main_window.resize(800, 600) + main_window.resize(1200, 600) main_window.show() app.exec_() @@ -72,9 +72,19 @@ def generate_icon_options(): def get_icon_options() -> list[str]: + # The charmap file is everything that pydm recognizes as an icon, including things it has no image data for with open(Path(iconfont.__file__).parent / "fontawesome-charmap.json", "r") as fd: charmap: dict[str, str] = json.load(fd) - return list(charmap) + + # The glyph map is everything actually present in the otf file pydm uses + # I got this map by uploading the otf file to fontdrop.info and copying the glyphIndexMap in the cmap verbatim + # There are probably other ways to get this info, but this one avoided adding a dependency here + with open(Path(__file__).parent / "valid_glyph_map.json", "r") as fd: + glyph_map: dict[str, int] = json.load(fd) + valid_hex = {hex(int(key))[2:] for key in glyph_map} + + # If something is present in both, it's a valid icon! + return [name for name, hexval in charmap.items() if hexval in valid_hex] if __name__ == "__main__": diff --git a/pcdswidgets/builder/icon_options.j2 b/pcdswidgets/builder/icon_options.j2 index ec4e1b2..0999e1b 100644 --- a/pcdswidgets/builder/icon_options.j2 +++ b/pcdswidgets/builder/icon_options.j2 @@ -10,5 +10,5 @@ from enum import StrEnum class IconOptions(StrEnum): NONE = "" {% for opt in options %} - {{ opt.replace("-", "_").replace("5", "a5") }} = "{{ opt }}" + {{ opt.replace("-", "_") }} = "{{ opt }}" {% endfor %} diff --git a/pcdswidgets/builder/icon_options.py b/pcdswidgets/builder/icon_options.py index 48384ab..336b8fc 100644 --- a/pcdswidgets/builder/icon_options.py +++ b/pcdswidgets/builder/icon_options.py @@ -9,34 +9,19 @@ class IconOptions(StrEnum): NONE = "" - a500px = "500px" - accessible_icon = "accessible-icon" - accusoft = "accusoft" - acquisitions_incorporated = "acquisitions-incorporated" ad = "ad" address_book = "address-book" address_card = "address-card" adjust = "adjust" - adn = "adn" - adversal = "adversal" - affiliatetheme = "affiliatetheme" air_freshener = "air-freshener" - airbnb = "airbnb" - algolia = "algolia" align_center = "align-center" align_justify = "align-justify" align_left = "align-left" align_right = "align-right" - alipay = "alipay" allergies = "allergies" - amazon = "amazon" - amazon_pay = "amazon-pay" ambulance = "ambulance" american_sign_language_interpreting = "american-sign-language-interpreting" - amilia = "amilia" anchor = "anchor" - android = "android" - angellist = "angellist" angle_double_down = "angle-double-down" angle_double_left = "angle-double-left" angle_double_right = "angle-double-right" @@ -46,15 +31,8 @@ class IconOptions(StrEnum): angle_right = "angle-right" angle_up = "angle-up" angry = "angry" - angrycreative = "angrycreative" - angular = "angular" ankh = "ankh" - app_store = "app-store" - app_store_ios = "app-store-ios" - apper = "apper" - apple = "apple" apple_alt = "apple-alt" - apple_pay = "apple-pay" archive = "archive" archway = "archway" arrow_alt_circle_down = "arrow-alt-circle-down" @@ -72,21 +50,13 @@ class IconOptions(StrEnum): arrows_alt = "arrows-alt" arrows_alt_h = "arrows-alt-h" arrows_alt_v = "arrows-alt-v" - artstation = "artstation" assistive_listening_systems = "assistive-listening-systems" asterisk = "asterisk" - asymmetrik = "asymmetrik" at = "at" atlas = "atlas" - atlassian = "atlassian" atom = "atom" - audible = "audible" audio_description = "audio-description" - autoprefixer = "autoprefixer" - avianex = "avianex" - aviato = "aviato" award = "award" - aws = "aws" baby = "baby" baby_carriage = "baby-carriage" backspace = "backspace" @@ -100,7 +70,6 @@ class IconOptions(StrEnum): balance_scale_right = "balance-scale-right" ban = "ban" band_aid = "band-aid" - bandcamp = "bandcamp" barcode = "barcode" bars = "bars" baseball_ball = "baseball-ball" @@ -111,34 +80,21 @@ class IconOptions(StrEnum): battery_half = "battery-half" battery_quarter = "battery-quarter" battery_three_quarters = "battery-three-quarters" - battle_net = "battle-net" bed = "bed" beer = "beer" - behance = "behance" - behance_square = "behance-square" bell = "bell" bell_slash = "bell-slash" bezier_curve = "bezier-curve" bible = "bible" bicycle = "bicycle" biking = "biking" - bimobject = "bimobject" binoculars = "binoculars" biohazard = "biohazard" birthday_cake = "birthday-cake" - bitbucket = "bitbucket" - bitcoin = "bitcoin" - bity = "bity" - black_tie = "black-tie" - blackberry = "blackberry" blender = "blender" blender_phone = "blender-phone" blind = "blind" blog = "blog" - blogger = "blogger" - blogger_b = "blogger-b" - bluetooth = "bluetooth" - bluetooth_b = "bluetooth-b" bold = "bold" bolt = "bolt" bomb = "bomb" @@ -150,7 +106,6 @@ class IconOptions(StrEnum): book_open = "book-open" book_reader = "book-reader" bookmark = "bookmark" - bootstrap = "bootstrap" border_all = "border-all" border_none = "border-none" border_style = "border-style" @@ -167,19 +122,14 @@ class IconOptions(StrEnum): broadcast_tower = "broadcast-tower" broom = "broom" brush = "brush" - btc = "btc" - buffer = "buffer" bug = "bug" building = "building" bullhorn = "bullhorn" bullseye = "bullseye" burn = "burn" - buromobelexperte = "buromobelexperte" bus = "bus" bus_alt = "bus-alt" business_time = "business-time" - buy_n_large = "buy-n-large" - buysellads = "buysellads" calculator = "calculator" calendar = "calendar" calendar_alt = "calendar-alt" @@ -192,7 +142,6 @@ class IconOptions(StrEnum): camera = "camera" camera_retro = "camera-retro" campground = "campground" - canadian_maple_leaf = "canadian-maple-leaf" candy_cane = "candy-cane" cannabis = "cannabis" capsules = "capsules" @@ -215,18 +164,6 @@ class IconOptions(StrEnum): cart_plus = "cart-plus" cash_register = "cash-register" cat = "cat" - cc_amazon_pay = "cc-amazon-pay" - cc_amex = "cc-amex" - cc_apple_pay = "cc-apple-pay" - cc_diners_club = "cc-diners-club" - cc_discover = "cc-discover" - cc_jcb = "cc-jcb" - cc_mastercard = "cc-mastercard" - cc_paypal = "cc-paypal" - cc_stripe = "cc-stripe" - cc_visa = "cc-visa" - centercode = "centercode" - centos = "centos" certificate = "certificate" chair = "chair" chalkboard = "chalkboard" @@ -258,8 +195,6 @@ class IconOptions(StrEnum): chevron_right = "chevron-right" chevron_up = "chevron-up" child = "child" - chrome = "chrome" - chromecast = "chromecast" church = "church" circle = "circle" circle_notch = "circle-notch" @@ -281,15 +216,9 @@ class IconOptions(StrEnum): cloud_sun = "cloud-sun" cloud_sun_rain = "cloud-sun-rain" cloud_upload_alt = "cloud-upload-alt" - cloudflare = "cloudflare" - cloudscale = "cloudscale" - cloudsmith = "cloudsmith" - cloudversify = "cloudversify" cocktail = "cocktail" code = "code" code_branch = "code-branch" - codepen = "codepen" - codiepie = "codiepie" coffee = "coffee" cog = "cog" cogs = "cogs" @@ -309,32 +238,12 @@ class IconOptions(StrEnum): compress_alt = "compress-alt" compress_arrows_alt = "compress-arrows-alt" concierge_bell = "concierge-bell" - confluence = "confluence" - connectdevelop = "connectdevelop" - contao = "contao" cookie = "cookie" cookie_bite = "cookie-bite" copy = "copy" copyright = "copyright" - cotton_bureau = "cotton-bureau" couch = "couch" - cpanel = "cpanel" - creative_commons = "creative-commons" - creative_commons_by = "creative-commons-by" - creative_commons_nc = "creative-commons-nc" - creative_commons_nc_eu = "creative-commons-nc-eu" - creative_commons_nc_jp = "creative-commons-nc-jp" - creative_commons_nd = "creative-commons-nd" - creative_commons_pd = "creative-commons-pd" - creative_commons_pd_alt = "creative-commons-pd-alt" - creative_commons_remix = "creative-commons-remix" - creative_commons_sa = "creative-commons-sa" - creative_commons_sampling = "creative-commons-sampling" - creative_commons_sampling_plus = "creative-commons-sampling-plus" - creative_commons_share = "creative-commons-share" - creative_commons_zero = "creative-commons-zero" credit_card = "credit-card" - critical_role = "critical-role" crop = "crop" crop_alt = "crop-alt" cross = "cross" @@ -342,30 +251,15 @@ class IconOptions(StrEnum): crow = "crow" crown = "crown" crutch = "crutch" - css3 = "css3" - css3_alt = "css3-alt" cube = "cube" cubes = "cubes" cut = "cut" - cuttlefish = "cuttlefish" - d_and_d = "d-and-d" - d_and_d_beyond = "d-and-d-beyond" - dailymotion = "dailymotion" - dashcube = "dashcube" database = "database" deaf = "deaf" - deezer = "deezer" - delicious = "delicious" democrat = "democrat" - deploydog = "deploydog" - deskpro = "deskpro" desktop = "desktop" - dev = "dev" - deviantart = "deviantart" dharmachakra = "dharmachakra" - dhl = "dhl" diagnoses = "diagnoses" - diaspora = "diaspora" dice = "dice" dice_d20 = "dice-d20" dice_d6 = "dice-d6" @@ -375,18 +269,12 @@ class IconOptions(StrEnum): dice_six = "dice-six" dice_three = "dice-three" dice_two = "dice-two" - digg = "digg" - digital_ocean = "digital-ocean" digital_tachograph = "digital-tachograph" directions = "directions" - discord = "discord" - discourse = "discourse" disease = "disease" divide = "divide" dizzy = "dizzy" dna = "dna" - dochub = "dochub" - docker = "docker" dog = "dog" dollar_sign = "dollar-sign" dolly = "dolly" @@ -397,48 +285,29 @@ class IconOptions(StrEnum): dot_circle = "dot-circle" dove = "dove" download = "download" - draft2digital = "draft2digital" drafting_compass = "drafting-compass" dragon = "dragon" draw_polygon = "draw-polygon" - dribbble = "dribbble" - dribbble_square = "dribbble-square" - dropbox = "dropbox" drum = "drum" drum_steelpan = "drum-steelpan" drumstick_bite = "drumstick-bite" - drupal = "drupal" dumbbell = "dumbbell" dumpster = "dumpster" dumpster_fire = "dumpster-fire" dungeon = "dungeon" - dyalog = "dyalog" - earlybirds = "earlybirds" - ebay = "ebay" - edge = "edge" - edge_legacy = "edge-legacy" edit = "edit" egg = "egg" eject = "eject" - elementor = "elementor" ellipsis_h = "ellipsis-h" ellipsis_v = "ellipsis-v" - ello = "ello" - ember = "ember" - empire = "empire" envelope = "envelope" envelope_open = "envelope-open" envelope_open_text = "envelope-open-text" envelope_square = "envelope-square" - envira = "envira" equals = "equals" eraser = "eraser" - erlang = "erlang" - ethereum = "ethereum" ethernet = "ethernet" - etsy = "etsy" euro_sign = "euro-sign" - evernote = "evernote" exchange_alt = "exchange-alt" exclamation = "exclamation" exclamation_circle = "exclamation-circle" @@ -446,29 +315,20 @@ class IconOptions(StrEnum): expand = "expand" expand_alt = "expand-alt" expand_arrows_alt = "expand-arrows-alt" - expeditedssl = "expeditedssl" external_link_alt = "external-link-alt" external_link_square_alt = "external-link-square-alt" eye = "eye" eye_dropper = "eye-dropper" eye_slash = "eye-slash" - facebook = "facebook" - facebook_f = "facebook-f" - facebook_messenger = "facebook-messenger" - facebook_square = "facebook-square" fan = "fan" - fantasy_flight_games = "fantasy-flight-games" fast_backward = "fast-backward" fast_forward = "fast-forward" faucet = "faucet" fax = "fax" feather = "feather" feather_alt = "feather-alt" - fedex = "fedex" - fedora = "fedora" female = "female" fighter_jet = "fighter-jet" - figma = "figma" file = "file" file_alt = "file-alt" file_archive = "file-archive" @@ -500,97 +360,48 @@ class IconOptions(StrEnum): fire = "fire" fire_alt = "fire-alt" fire_extinguisher = "fire-extinguisher" - firefox = "firefox" - firefox_browser = "firefox-browser" first_aid = "first-aid" - first_order = "first-order" - first_order_alt = "first-order-alt" - firstdraft = "firstdraft" fish = "fish" fist_raised = "fist-raised" flag = "flag" flag_checkered = "flag-checkered" flag_usa = "flag-usa" flask = "flask" - flickr = "flickr" - flipboard = "flipboard" flushed = "flushed" - fly = "fly" folder = "folder" folder_minus = "folder-minus" folder_open = "folder-open" folder_plus = "folder-plus" font = "font" - font_awesome = "font-awesome" - font_awesome_alt = "font-awesome-alt" - font_awesome_flag = "font-awesome-flag" font_awesome_logo_full = "font-awesome-logo-full" - fonticons = "fonticons" - fonticons_fi = "fonticons-fi" football_ball = "football-ball" - fort_awesome = "fort-awesome" - fort_awesome_alt = "fort-awesome-alt" - forumbee = "forumbee" forward = "forward" - foursquare = "foursquare" - free_code_camp = "free-code-camp" - freebsd = "freebsd" frog = "frog" frown = "frown" frown_open = "frown-open" - fulcrum = "fulcrum" funnel_dollar = "funnel-dollar" futbol = "futbol" - galactic_republic = "galactic-republic" - galactic_senate = "galactic-senate" gamepad = "gamepad" gas_pump = "gas-pump" gavel = "gavel" gem = "gem" genderless = "genderless" - get_pocket = "get-pocket" - gg = "gg" - gg_circle = "gg-circle" ghost = "ghost" gift = "gift" gifts = "gifts" - git = "git" - git_alt = "git-alt" - git_square = "git-square" - github = "github" - github_alt = "github-alt" - github_square = "github-square" - gitkraken = "gitkraken" - gitlab = "gitlab" - gitter = "gitter" glass_cheers = "glass-cheers" glass_martini = "glass-martini" glass_martini_alt = "glass-martini-alt" glass_whiskey = "glass-whiskey" glasses = "glasses" - glide = "glide" - glide_g = "glide-g" globe = "globe" globe_africa = "globe-africa" globe_americas = "globe-americas" globe_asia = "globe-asia" globe_europe = "globe-europe" - gofore = "gofore" golf_ball = "golf-ball" - goodreads = "goodreads" - goodreads_g = "goodreads-g" - google = "google" - google_drive = "google-drive" - google_pay = "google-pay" - google_play = "google-play" - google_plus = "google-plus" - google_plus_g = "google-plus-g" - google_plus_square = "google-plus-square" - google_wallet = "google-wallet" gopuram = "gopuram" graduation_cap = "graduation-cap" - gratipay = "gratipay" - grav = "grav" greater_than = "greater-than" greater_than_equal = "greater-than-equal" grimace = "grimace" @@ -611,15 +422,8 @@ class IconOptions(StrEnum): grip_lines = "grip-lines" grip_lines_vertical = "grip-lines-vertical" grip_vertical = "grip-vertical" - gripfire = "gripfire" - grunt = "grunt" - guilded = "guilded" guitar = "guitar" - gulp = "gulp" h_square = "h-square" - hacker_news = "hacker-news" - hacker_news_square = "hacker-news-square" - hackerrank = "hackerrank" hamburger = "hamburger" hammer = "hammer" hamsa = "hamsa" @@ -669,15 +473,10 @@ class IconOptions(StrEnum): highlighter = "highlighter" hiking = "hiking" hippo = "hippo" - hips = "hips" - hire_a_helper = "hire-a-helper" history = "history" - hive = "hive" hockey_puck = "hockey-puck" holly_berry = "holly-berry" home = "home" - hooli = "hooli" - hornbill = "hornbill" horse = "horse" horse_head = "horse-head" hospital = "hospital" @@ -687,17 +486,13 @@ class IconOptions(StrEnum): hot_tub = "hot-tub" hotdog = "hotdog" hotel = "hotel" - hotjar = "hotjar" hourglass = "hourglass" hourglass_end = "hourglass-end" hourglass_half = "hourglass-half" hourglass_start = "hourglass-start" house_damage = "house-damage" house_user = "house-user" - houzz = "houzz" hryvnia = "hryvnia" - htmla5 = "html5" - hubspot = "hubspot" i_cursor = "i-cursor" ice_cream = "ice-cream" icicles = "icicles" @@ -705,85 +500,47 @@ class IconOptions(StrEnum): id_badge = "id-badge" id_card = "id-card" id_card_alt = "id-card-alt" - ideal = "ideal" igloo = "igloo" image = "image" images = "images" - imdb = "imdb" inbox = "inbox" indent = "indent" industry = "industry" infinity = "infinity" info = "info" info_circle = "info-circle" - innosoft = "innosoft" - instagram = "instagram" - instagram_square = "instagram-square" - instalod = "instalod" - intercom = "intercom" - internet_explorer = "internet-explorer" - invision = "invision" - ioxhost = "ioxhost" italic = "italic" - itch_io = "itch-io" - itunes = "itunes" - itunes_note = "itunes-note" - java = "java" jedi = "jedi" - jedi_order = "jedi-order" - jenkins = "jenkins" - jira = "jira" - joget = "joget" joint = "joint" - joomla = "joomla" journal_whills = "journal-whills" - js = "js" - js_square = "js-square" - jsfiddle = "jsfiddle" kaaba = "kaaba" - kaggle = "kaggle" key = "key" - keybase = "keybase" keyboard = "keyboard" - keycdn = "keycdn" khanda = "khanda" - kickstarter = "kickstarter" - kickstarter_k = "kickstarter-k" kiss = "kiss" kiss_beam = "kiss-beam" kiss_wink_heart = "kiss-wink-heart" kiwi_bird = "kiwi-bird" - korvue = "korvue" landmark = "landmark" language = "language" laptop = "laptop" laptop_code = "laptop-code" laptop_house = "laptop-house" laptop_medical = "laptop-medical" - laravel = "laravel" - lastfm = "lastfm" - lastfm_square = "lastfm-square" laugh = "laugh" laugh_beam = "laugh-beam" laugh_squint = "laugh-squint" laugh_wink = "laugh-wink" layer_group = "layer-group" leaf = "leaf" - leanpub = "leanpub" lemon = "lemon" - less = "less" less_than = "less-than" less_than_equal = "less-than-equal" level_down_alt = "level-down-alt" level_up_alt = "level-up-alt" life_ring = "life-ring" lightbulb = "lightbulb" - line = "line" link = "link" - linkedin = "linkedin" - linkedin_in = "linkedin-in" - linode = "linode" - linux = "linux" lira_sign = "lira-sign" list = "list" list_alt = "list-alt" @@ -800,14 +557,10 @@ class IconOptions(StrEnum): luggage_cart = "luggage-cart" lungs = "lungs" lungs_virus = "lungs-virus" - lyft = "lyft" - magento = "magento" magic = "magic" magnet = "magnet" mail_bulk = "mail-bulk" - mailchimp = "mailchimp" male = "male" - mandalorian = "mandalorian" map = "map" map_marked = "map-marked" map_marked_alt = "map-marked-alt" @@ -815,7 +568,6 @@ class IconOptions(StrEnum): map_marker_alt = "map-marker-alt" map_pin = "map-pin" map_signs = "map-signs" - markdown = "markdown" marker = "marker" mars = "mars" mars_double = "mars-double" @@ -823,45 +575,27 @@ class IconOptions(StrEnum): mars_stroke_h = "mars-stroke-h" mars_stroke_v = "mars-stroke-v" mask = "mask" - mastodon = "mastodon" - maxcdn = "maxcdn" - mdb = "mdb" medal = "medal" - medapps = "medapps" - medium = "medium" - medium_m = "medium-m" medkit = "medkit" - medrt = "medrt" - meetup = "meetup" - megaport = "megaport" meh = "meh" meh_blank = "meh-blank" meh_rolling_eyes = "meh-rolling-eyes" memory = "memory" - mendeley = "mendeley" menorah = "menorah" mercury = "mercury" meteor = "meteor" - microblog = "microblog" microchip = "microchip" microphone = "microphone" microphone_alt = "microphone-alt" microphone_alt_slash = "microphone-alt-slash" microphone_slash = "microphone-slash" microscope = "microscope" - microsoft = "microsoft" minus = "minus" minus_circle = "minus-circle" minus_square = "minus-square" mitten = "mitten" - mix = "mix" - mixcloud = "mixcloud" - mixer = "mixer" - mizuni = "mizuni" mobile = "mobile" mobile_alt = "mobile-alt" - modx = "modx" - monero = "monero" money_bill = "money-bill" money_bill_alt = "money-bill-alt" money_bill_wave = "money-bill-wave" @@ -878,42 +612,21 @@ class IconOptions(StrEnum): mouse_pointer = "mouse-pointer" mug_hot = "mug-hot" music = "music" - napster = "napster" - neos = "neos" network_wired = "network-wired" neuter = "neuter" newspaper = "newspaper" - nimblr = "nimblr" - node = "node" - node_js = "node-js" not_equal = "not-equal" notes_medical = "notes-medical" - npm = "npm" - ns8 = "ns8" - nutritionix = "nutritionix" object_group = "object-group" object_ungroup = "object-ungroup" - octopus_deploy = "octopus-deploy" - odnoklassniki = "odnoklassniki" - odnoklassniki_square = "odnoklassniki-square" oil_can = "oil-can" - old_republic = "old-republic" om = "om" - opencart = "opencart" - openid = "openid" - opera = "opera" - optin_monster = "optin-monster" - orcid = "orcid" - osi = "osi" otter = "otter" outdent = "outdent" - page4 = "page4" - pagelines = "pagelines" pager = "pager" paint_brush = "paint-brush" paint_roller = "paint-roller" palette = "palette" - palfed = "palfed" pallet = "pallet" paper_plane = "paper-plane" paperclip = "paperclip" @@ -923,11 +636,9 @@ class IconOptions(StrEnum): passport = "passport" pastafarianism = "pastafarianism" paste = "paste" - patreon = "patreon" pause = "pause" pause_circle = "pause-circle" paw = "paw" - paypal = "paypal" peace = "peace" pen = "pen" pen_alt = "pen-alt" @@ -936,18 +647,12 @@ class IconOptions(StrEnum): pen_square = "pen-square" pencil_alt = "pencil-alt" pencil_ruler = "pencil-ruler" - penny_arcade = "penny-arcade" people_arrows = "people-arrows" people_carry = "people-carry" pepper_hot = "pepper-hot" - perbyte = "perbyte" percent = "percent" percentage = "percentage" - periscope = "periscope" person_booth = "person-booth" - phabricator = "phabricator" - phoenix_framework = "phoenix-framework" - phoenix_squadron = "phoenix-squadron" phone = "phone" phone_alt = "phone-alt" phone_slash = "phone-slash" @@ -955,17 +660,8 @@ class IconOptions(StrEnum): phone_square_alt = "phone-square-alt" phone_volume = "phone-volume" photo_video = "photo-video" - php = "php" - pied_piper = "pied-piper" - pied_piper_alt = "pied-piper-alt" - pied_piper_hat = "pied-piper-hat" - pied_piper_pp = "pied-piper-pp" - pied_piper_square = "pied-piper-square" piggy_bank = "piggy-bank" pills = "pills" - pinterest = "pinterest" - pinterest_p = "pinterest-p" - pinterest_square = "pinterest-square" pizza_slice = "pizza-slice" place_of_worship = "place-of-worship" plane = "plane" @@ -974,7 +670,6 @@ class IconOptions(StrEnum): plane_slash = "plane-slash" play = "play" play_circle = "play-circle" - playstation = "playstation" plug = "plug" plus = "plus" plus_circle = "plus-circle" @@ -995,63 +690,38 @@ class IconOptions(StrEnum): prescription_bottle_alt = "prescription-bottle-alt" print = "print" procedures = "procedures" - product_hunt = "product-hunt" project_diagram = "project-diagram" pump_medical = "pump-medical" pump_soap = "pump-soap" - pushed = "pushed" puzzle_piece = "puzzle-piece" - python = "python" - qq = "qq" qrcode = "qrcode" question = "question" question_circle = "question-circle" quidditch = "quidditch" - quinscape = "quinscape" - quora = "quora" quote_left = "quote-left" quote_right = "quote-right" quran = "quran" - r_project = "r-project" radiation = "radiation" radiation_alt = "radiation-alt" rainbow = "rainbow" random = "random" - raspberry_pi = "raspberry-pi" - ravelry = "ravelry" - react = "react" - reacteurope = "reacteurope" - readme = "readme" - rebel = "rebel" receipt = "receipt" record_vinyl = "record-vinyl" recycle = "recycle" - red_river = "red-river" - reddit = "reddit" - reddit_alien = "reddit-alien" - reddit_square = "reddit-square" - redhat = "redhat" redo = "redo" redo_alt = "redo-alt" registered = "registered" remove_format = "remove-format" - renren = "renren" reply = "reply" reply_all = "reply-all" - replyd = "replyd" republican = "republican" - researchgate = "researchgate" - resolving = "resolving" restroom = "restroom" retweet = "retweet" - rev = "rev" ribbon = "ribbon" ring = "ring" road = "road" robot = "robot" rocket = "rocket" - rocketchat = "rocketchat" - rockrms = "rockrms" route = "route" rss = "rss" rss_square = "rss-square" @@ -1062,19 +732,13 @@ class IconOptions(StrEnum): ruler_vertical = "ruler-vertical" running = "running" rupee_sign = "rupee-sign" - rust = "rust" sad_cry = "sad-cry" sad_tear = "sad-tear" - safari = "safari" - salesforce = "salesforce" - sass = "sass" satellite = "satellite" satellite_dish = "satellite-dish" save = "save" - schlix = "schlix" school = "school" screwdriver = "screwdriver" - scribd = "scribd" scroll = "scroll" sd_card = "sd-card" search = "search" @@ -1082,12 +746,8 @@ class IconOptions(StrEnum): search_location = "search-location" search_minus = "search-minus" search_plus = "search-plus" - searchengin = "searchengin" seedling = "seedling" - sellcast = "sellcast" - sellsy = "sellsy" server = "server" - servicestack = "servicestack" shapes = "shapes" share = "share" share_alt = "share-alt" @@ -1098,13 +758,10 @@ class IconOptions(StrEnum): shield_virus = "shield-virus" ship = "ship" shipping_fast = "shipping-fast" - shirtsinbulk = "shirtsinbulk" shoe_prints = "shoe-prints" - shopify = "shopify" shopping_bag = "shopping-bag" shopping_basket = "shopping-basket" shopping_cart = "shopping-cart" - shopware = "shopware" shower = "shower" shuttle_van = "shuttle-van" sign = "sign" @@ -1114,25 +771,16 @@ class IconOptions(StrEnum): signal = "signal" signature = "signature" sim_card = "sim-card" - simplybuilt = "simplybuilt" sink = "sink" - sistrix = "sistrix" sitemap = "sitemap" - sith = "sith" skating = "skating" - sketch = "sketch" skiing = "skiing" skiing_nordic = "skiing-nordic" skull = "skull" skull_crossbones = "skull-crossbones" - skyatlas = "skyatlas" - skype = "skype" - slack = "slack" - slack_hash = "slack-hash" slash = "slash" sleigh = "sleigh" sliders_h = "sliders-h" - slideshare = "slideshare" smile = "smile" smile_beam = "smile-beam" smile_wink = "smile-wink" @@ -1140,9 +788,6 @@ class IconOptions(StrEnum): smoking = "smoking" smoking_ban = "smoking-ban" sms = "sms" - snapchat = "snapchat" - snapchat_ghost = "snapchat-ghost" - snapchat_square = "snapchat-square" snowboarding = "snowboarding" snowflake = "snowflake" snowman = "snowman" @@ -1165,25 +810,16 @@ class IconOptions(StrEnum): sort_numeric_up = "sort-numeric-up" sort_numeric_up_alt = "sort-numeric-up-alt" sort_up = "sort-up" - soundcloud = "soundcloud" - sourcetree = "sourcetree" spa = "spa" space_shuttle = "space-shuttle" - speakap = "speakap" - speaker_deck = "speaker-deck" spell_check = "spell-check" spider = "spider" spinner = "spinner" splotch = "splotch" - spotify = "spotify" spray_can = "spray-can" square = "square" square_full = "square-full" square_root_alt = "square-root-alt" - squarespace = "squarespace" - stack_exchange = "stack-exchange" - stack_overflow = "stack-overflow" - stackpath = "stackpath" stamp = "stamp" star = "star" star_and_crescent = "star-and-crescent" @@ -1191,14 +827,9 @@ class IconOptions(StrEnum): star_half_alt = "star-half-alt" star_of_david = "star-of-david" star_of_life = "star-of-life" - staylinked = "staylinked" - steam = "steam" - steam_square = "steam-square" - steam_symbol = "steam-symbol" step_backward = "step-backward" step_forward = "step-forward" stethoscope = "stethoscope" - sticker_mule = "sticker-mule" sticky_note = "sticky-note" stop = "stop" stop_circle = "stop-circle" @@ -1208,31 +839,20 @@ class IconOptions(StrEnum): store_alt = "store-alt" store_alt_slash = "store-alt-slash" store_slash = "store-slash" - strava = "strava" stream = "stream" street_view = "street-view" strikethrough = "strikethrough" - stripe = "stripe" - stripe_s = "stripe-s" stroopwafel = "stroopwafel" - studiovinari = "studiovinari" - stumbleupon = "stumbleupon" - stumbleupon_circle = "stumbleupon-circle" subscript = "subscript" subway = "subway" suitcase = "suitcase" suitcase_rolling = "suitcase-rolling" sun = "sun" - superpowers = "superpowers" superscript = "superscript" - supple = "supple" surprise = "surprise" - suse = "suse" swatchbook = "swatchbook" - swift = "swift" swimmer = "swimmer" swimming_pool = "swimming-pool" - symfony = "symfony" synagogue = "synagogue" sync = "sync" sync_alt = "sync-alt" @@ -1248,14 +868,10 @@ class IconOptions(StrEnum): tape = "tape" tasks = "tasks" taxi = "taxi" - teamspeak = "teamspeak" teeth = "teeth" teeth_open = "teeth-open" - telegram = "telegram" - telegram_plane = "telegram-plane" temperature_high = "temperature-high" temperature_low = "temperature-low" - tencent_weibo = "tencent-weibo" tenge = "tenge" terminal = "terminal" text_height = "text-height" @@ -1263,22 +879,17 @@ class IconOptions(StrEnum): th = "th" th_large = "th-large" th_list = "th-list" - the_red_yeti = "the-red-yeti" theater_masks = "theater-masks" - themeco = "themeco" - themeisle = "themeisle" thermometer = "thermometer" thermometer_empty = "thermometer-empty" thermometer_full = "thermometer-full" thermometer_half = "thermometer-half" thermometer_quarter = "thermometer-quarter" thermometer_three_quarters = "thermometer-three-quarters" - think_peaks = "think-peaks" thumbs_down = "thumbs-down" thumbs_up = "thumbs-up" thumbtack = "thumbtack" ticket_alt = "ticket-alt" - tiktok = "tiktok" times = "times" times_circle = "times-circle" tint = "tint" @@ -1295,7 +906,6 @@ class IconOptions(StrEnum): torah = "torah" torii_gate = "torii-gate" tractor = "tractor" - trade_federation = "trade-federation" trademark = "trademark" traffic_light = "traffic-light" trailer = "trailer" @@ -1308,8 +918,6 @@ class IconOptions(StrEnum): trash_restore = "trash-restore" trash_restore_alt = "trash-restore-alt" tree = "tree" - trello = "trello" - tripadvisor = "tripadvisor" trophy = "trophy" truck = "truck" truck_loading = "truck-loading" @@ -1318,35 +926,18 @@ class IconOptions(StrEnum): truck_pickup = "truck-pickup" tshirt = "tshirt" tty = "tty" - tumblr = "tumblr" - tumblr_square = "tumblr-square" tv = "tv" - twitch = "twitch" - twitter = "twitter" - twitter_square = "twitter-square" - typo3 = "typo3" - uber = "uber" - ubuntu = "ubuntu" - uikit = "uikit" - umbraco = "umbraco" umbrella = "umbrella" umbrella_beach = "umbrella-beach" - uncharted = "uncharted" underline = "underline" undo = "undo" undo_alt = "undo-alt" - uniregistry = "uniregistry" - unity = "unity" universal_access = "universal-access" university = "university" unlink = "unlink" unlock = "unlock" unlock_alt = "unlock-alt" - unsplash = "unsplash" - untappd = "untappd" upload = "upload" - ups = "ups" - usb = "usb" user = "user" user_alt = "user-alt" user_alt_slash = "user-alt-slash" @@ -1374,35 +965,22 @@ class IconOptions(StrEnum): users = "users" users_cog = "users-cog" users_slash = "users-slash" - usps = "usps" - ussunnah = "ussunnah" utensil_spoon = "utensil-spoon" utensils = "utensils" - vaadin = "vaadin" vector_square = "vector-square" venus = "venus" venus_double = "venus-double" venus_mars = "venus-mars" vest = "vest" vest_patches = "vest-patches" - viacoin = "viacoin" - viadeo = "viadeo" - viadeo_square = "viadeo-square" vial = "vial" vials = "vials" - viber = "viber" video = "video" video_slash = "video-slash" vihara = "vihara" - vimeo = "vimeo" - vimeo_square = "vimeo-square" - vimeo_v = "vimeo-v" - vine = "vine" virus = "virus" virus_slash = "virus-slash" viruses = "viruses" - vk = "vk" - vnv = "vnv" voicemail = "voicemail" volleyball_ball = "volleyball-ball" volume_down = "volume-down" @@ -1411,60 +989,25 @@ class IconOptions(StrEnum): volume_up = "volume-up" vote_yea = "vote-yea" vr_cardboard = "vr-cardboard" - vuejs = "vuejs" walking = "walking" wallet = "wallet" warehouse = "warehouse" - watchman_monitoring = "watchman-monitoring" water = "water" wave_square = "wave-square" - waze = "waze" - weebly = "weebly" - weibo = "weibo" weight = "weight" weight_hanging = "weight-hanging" - weixin = "weixin" - whatsapp = "whatsapp" - whatsapp_square = "whatsapp-square" wheelchair = "wheelchair" - whmcs = "whmcs" wifi = "wifi" - wikipedia_w = "wikipedia-w" wind = "wind" window_close = "window-close" window_maximize = "window-maximize" window_minimize = "window-minimize" window_restore = "window-restore" - windows = "windows" wine_bottle = "wine-bottle" wine_glass = "wine-glass" wine_glass_alt = "wine-glass-alt" - wix = "wix" - wizards_of_the_coast = "wizards-of-the-coast" - wodu = "wodu" - wolf_pack_battalion = "wolf-pack-battalion" won_sign = "won-sign" - wordpress = "wordpress" - wordpress_simple = "wordpress-simple" - wpbeginner = "wpbeginner" - wpexplorer = "wpexplorer" - wpforms = "wpforms" - wpressr = "wpressr" wrench = "wrench" x_ray = "x-ray" - xbox = "xbox" - xing = "xing" - xing_square = "xing-square" - y_combinator = "y-combinator" - yahoo = "yahoo" - yammer = "yammer" - yandex = "yandex" - yandex_international = "yandex-international" - yarn = "yarn" - yelp = "yelp" yen_sign = "yen-sign" yin_yang = "yin-yang" - yoast = "yoast" - youtube = "youtube" - youtube_square = "youtube-square" - zhihu = "zhihu" diff --git a/pcdswidgets/builder/valid_glyph_map.json b/pcdswidgets/builder/valid_glyph_map.json new file mode 100644 index 0000000..a4053fc --- /dev/null +++ b/pcdswidgets/builder/valid_glyph_map.json @@ -0,0 +1,2 @@ + +{"32":1,"45":2,"46":3,"48":4,"49":5,"50":6,"51":7,"52":8,"53":9,"54":10,"55":11,"56":12,"57":13,"65":14,"66":15,"67":16,"68":17,"69":18,"70":19,"71":20,"72":21,"73":22,"74":23,"75":24,"76":25,"77":26,"78":27,"79":28,"80":29,"81":30,"82":31,"83":32,"84":33,"85":34,"86":35,"87":36,"88":37,"89":38,"90":39,"97":40,"98":41,"99":42,"100":43,"101":44,"102":45,"103":46,"104":47,"105":48,"106":49,"107":50,"108":51,"109":52,"110":53,"111":54,"112":55,"113":56,"114":57,"115":58,"116":59,"117":60,"118":61,"119":62,"120":63,"121":64,"122":65,"57349":66,"57409":67,"57433":68,"57434":69,"57435":70,"57436":71,"57437":72,"57438":73,"57439":74,"57440":75,"57441":76,"57442":77,"57443":78,"57444":79,"57445":80,"57446":81,"57447":82,"57448":83,"57449":84,"57450":85,"57451":86,"57452":87,"57453":88,"57454":89,"57455":90,"57456":91,"57457":92,"57458":93,"57459":94,"57460":95,"57461":96,"57462":97,"57477":98,"57478":99,"61440":100,"61441":101,"61442":102,"61444":103,"61445":104,"61447":105,"61448":106,"61449":107,"61450":108,"61451":109,"61452":110,"61453":111,"61454":112,"61456":113,"61457":114,"61458":115,"61459":116,"61461":117,"61463":118,"61464":119,"61465":120,"61468":121,"61470":122,"61473":123,"61474":124,"61475":125,"61476":126,"61477":127,"61478":128,"61479":129,"61480":130,"61481":131,"61482":132,"61483":133,"61484":134,"61485":135,"61486":136,"61487":137,"61488":138,"61489":139,"61490":140,"61491":141,"61492":142,"61493":143,"61494":144,"61495":145,"61496":146,"61497":147,"61498":148,"61499":149,"61500":150,"61501":151,"61502":152,"61505":153,"61506":154,"61507":155,"61508":156,"61512":157,"61513":158,"61514":159,"61515":160,"61516":161,"61517":162,"61518":163,"61520":164,"61521":165,"61522":166,"61523":167,"61524":168,"61525":169,"61526":170,"61527":171,"61528":172,"61529":173,"61530":174,"61531":175,"61534":176,"61536":177,"61537":178,"61538":179,"61539":180,"61540":181,"61541":182,"61542":183,"61543":184,"61544":185,"61545":186,"61546":187,"61547":188,"61548":189,"61549":190,"61550":191,"61552":192,"61553":193,"61554":194,"61555":195,"61556":196,"61557":197,"61558":198,"61559":199,"61560":200,"61561":201,"61562":202,"61563":203,"61564":204,"61568":205,"61571":206,"61572":207,"61573":208,"61574":209,"61577":210,"61581":211,"61585":212,"61587":213,"61588":214,"61589":215,"61592":216,"61596":217,"61597":218,"61598":219,"61600":220,"61601":221,"61603":222,"61604":223,"61605":224,"61606":225,"61607":226,"61608":227,"61609":228,"61610":229,"61611":230,"61612":231,"61613":232,"61614":233,"61616":234,"61617":235,"61618":236,"61632":237,"61633":238,"61634":239,"61635":240,"61636":241,"61637":242,"61638":243,"61639":244,"61640":245,"61641":246,"61642":247,"61643":248,"61644":249,"61645":250,"61646":251,"61648":252,"61649":253,"61654":254,"61655":255,"61656":256,"61657":257,"61658":258,"61659":259,"61660":260,"61661":261,"61662":262,"61664":263,"61666":264,"61667":265,"61671":266,"61672":267,"61673":268,"61674":269,"61675":270,"61680":271,"61681":272,"61682":273,"61683":274,"61684":275,"61688":276,"61689":277,"61690":278,"61691":279,"61692":280,"61693":281,"61694":282,"61696":283,"61697":284,"61698":285,"61699":286,"61700":287,"61701":288,"61702":289,"61703":290,"61704":291,"61705":292,"61706":293,"61707":294,"61709":295,"61710":296,"61712":297,"61713":298,"61720":299,"61721":300,"61722":301,"61723":302,"61724":303,"61726":304,"61728":305,"61729":306,"61730":307,"61732":308,"61733":309,"61734":310,"61735":311,"61736":312,"61737":313,"61738":314,"61739":315,"61740":316,"61741":317,"61742":318,"61744":319,"61745":320,"61747":321,"61748":322,"61749":323,"61751":324,"61752":325,"61753":326,"61754":327,"61757":328,"61758":329,"61760":330,"61761":331,"61762":332,"61763":333,"61764":334,"61766":335,"61770":336,"61771":337,"61773":338,"61774":339,"61776":340,"61777":341,"61778":342,"61779":343,"61780":344,"61781":345,"61782":346,"61783":347,"61784":348,"61785":349,"61787":350,"61788":351,"61789":352,"61790":353,"61792":354,"61793":355,"61794":356,"61795":357,"61796":358,"61797":359,"61826":360,"61827":361,"61829":362,"61830":363,"61831":364,"61832":365,"61841":366,"61842":367,"61843":368,"61845":369,"61847":370,"61849":371,"61852":372,"61853":373,"61867":374,"61868":375,"61869":376,"61870":377,"61872":378,"61874":379,"61875":380,"61880":381,"61881":382,"61882":383,"61883":384,"61888":385,"61889":386,"61890":387,"61891":388,"61892":389,"61893":390,"61894":391,"61895":392,"61896":393,"61897":394,"61901":395,"61902":396,"61912":397,"61914":398,"61916":399,"61917":400,"61918":401,"61920":402,"61921":403,"61922":404,"61923":405,"61924":406,"61925":407,"61926":408,"61930":409,"61931":410,"61932":411,"61942":412,"61944":413,"61945":414,"61946":415,"61947":416,"61948":417,"61949":418,"61950":419,"61952":420,"61953":421,"61956":422,"61957":423,"61958":424,"61959":425,"61962":426,"61963":427,"61975":428,"61976":429,"61978":430,"61979":431,"61980":432,"61981":433,"61982":434,"61985":435,"61986":436,"61987":437,"61988":438,"61989":439,"61990":440,"61991":441,"61992":442,"61993":443,"61994":444,"61995":445,"61996":446,"61997":447,"62003":448,"62004":449,"62005":450,"62006":451,"62008":452,"62009":453,"62016":454,"62017":455,"62018":456,"62019":457,"62020":458,"62021":459,"62022":460,"62023":461,"62024":462,"62025":463,"62029":464,"62030":465,"62033":466,"62034":467,"62035":468,"62036":469,"62037":470,"62038":471,"62039":472,"62040":473,"62041":474,"62042":475,"62043":476,"62044":477,"62045":478,"62060":479,"62065":480,"62066":481,"62067":482,"62068":483,"62069":484,"62070":485,"62071":486,"62073":487,"62074":488,"62091":489,"62093":490,"62096":491,"62097":492,"62098":493,"62101":494,"62106":495,"62109":496,"62110":497,"62112":498,"62113":499,"62114":500,"62115":501,"62116":502,"62119":503,"62120":504,"62133":505,"62134":506,"62137":507,"62139":508,"62141":509,"62145":510,"62146":511,"62151":512,"62152":513,"62153":514,"62154":515,"62155":516,"62156":517,"62157":518,"62158":519,"62160":520,"62161":521,"62162":522,"62171":523,"62172":524,"62181":525,"62183":526,"62186":527,"62189":528,"62193":529,"62194":530,"62197":531,"62198":532,"62201":533,"62206":534,"62210":535,"62211":536,"62212":537,"62213":538,"62217":539,"62218":540,"62219":541,"62220":542,"62238":543,"62248":544,"62263":545,"62264":546,"62296":547,"62297":548,"62298":549,"62299":550,"62301":551,"62304":552,"62306":553,"62337":554,"62338":555,"62373":556,"62398":557,"62399":558,"62401":559,"62405":560,"62409":561,"62413":562,"62417":563,"62429":564,"62432":565,"62437":566,"62445":567,"62458":568,"62461":569,"62463":570,"62470":571,"62480":572,"62498":573,"62500":574,"62515":575,"62516":576,"62518":577,"62521":578,"62522":579,"62524":580,"62527":581,"62529":582,"62531":583,"62533":584,"62535":585,"62539":586,"62542":587,"62544":588,"62547":589,"62552":590,"62556":591,"62557":592,"62559":593,"62561":594,"62562":595,"62566":596,"62568":597,"62569":598,"62570":599,"62571":600,"62572":601,"62573":602,"62576":603,"62577":604,"62578":605,"62580":606,"62583":607,"62584":608,"62585":609,"62589":610,"62590":611,"62591":612,"62593":613,"62594":614,"62596":615,"62597":616,"62598":617,"62599":618,"62603":619,"62605":620,"62606":621,"62608":622,"62609":623,"62610":624,"62611":625,"62612":626,"62614":627,"62615":628,"62622":629,"62637":630,"62643":631,"62648":632,"62649":633,"62650":634,"62653":635,"62654":636,"62656":637,"62657":638,"62658":639,"62660":640,"62669":641,"62670":642,"62675":643,"62678":644,"62679":645,"62680":646,"62681":647,"62682":648,"62683":649,"62686":650,"62687":651,"62690":652,"62691":653,"62694":654,"62714":655,"62715":656,"62716":657,"62717":658,"62718":659,"62719":660,"62720":661,"62721":662,"62722":663,"62723":664,"62724":665,"62725":666,"62726":667,"62727":668,"62728":669,"62729":670,"62741":671,"62742":672,"62743":673,"62744":674,"62745":675,"62746":676,"62747":677,"62748":678,"62749":679,"62750":680,"62751":681,"62752":682,"62753":683,"62754":684,"62755":685,"62756":686,"62757":687,"62758":688,"62759":689,"62760":690,"62761":691,"62762":692,"62763":693,"62764":694,"62765":695,"62766":696,"62767":697,"62768":698,"62769":699,"62770":700,"62771":701,"62772":702,"62773":703,"62774":704,"62775":705,"62776":706,"62777":707,"62778":708,"62779":709,"62780":710,"62781":711,"62782":712,"62783":713,"62784":714,"62785":715,"62786":716,"62787":717,"62788":718,"62789":719,"62790":720,"62791":721,"62792":722,"62793":723,"62794":724,"62795":725,"62796":726,"62797":727,"62798":728,"62799":729,"62800":730,"62801":731,"62802":732,"62803":733,"62804":734,"62805":735,"62806":736,"62807":737,"62808":738,"62809":739,"62810":740,"62811":741,"62812":742,"62813":743,"62814":744,"62815":745,"62816":746,"62817":747,"62818":748,"62819":749,"62820":750,"62821":751,"62822":752,"62823":753,"62824":754,"62825":755,"62826":756,"62827":757,"62828":758,"62829":759,"62830":760,"62831":761,"62832":762,"62833":763,"62834":764,"62835":765,"62836":766,"62837":767,"62838":768,"62839":769,"62840":770,"62841":771,"62842":772,"62843":773,"62844":774,"62845":775,"62846":776,"62847":777,"62848":778,"62849":779,"62850":780,"62851":781,"62852":782,"62853":783,"62854":784,"62855":785,"62856":786,"62857":787,"62858":788,"62859":789,"62860":790,"62861":791,"62862":792,"62863":793,"62864":794,"62865":795,"62867":796,"62868":797,"62869":798,"62870":799,"62871":800,"62872":801,"62873":802,"62874":803,"62875":804,"62876":805,"62877":806,"62879":807,"62880":808,"62881":809,"62882":810,"62884":811,"62885":812,"62886":813,"62887":814,"62890":815,"62891":816,"62892":817,"62893":818,"62894":819,"62895":820,"62896":821,"62897":822,"62899":823,"62900":824,"62902":825,"62903":826,"62904":827,"62906":828,"62907":829,"62908":830,"62909":831,"62911":832,"62912":833,"62913":834,"62914":835,"62915":836,"62916":837,"62917":838,"62919":839,"62920":840,"62921":841,"62922":842,"62923":843,"62925":844,"62926":845,"62928":846,"62929":847,"62930":848,"62935":849,"62938":850,"62940":851,"62942":852,"62943":853,"62945":854,"62948":855,"62951":856,"62955":857,"62958":858,"62972":859,"62973":860,"62980":861,"62992":862,"62995":863,"63001":864,"63007":865,"63009":866,"63022":867,"63023":868,"63024":869,"63031":870,"63035":871,"63036":872,"63041":873,"63044":874,"63047":875,"63050":876,"63055":877,"63057":878,"63059":879,"63060":880,"63061":881,"63064":882,"63069":883,"63070":884,"63074":885,"63076":886,"63077":887,"63078":888,"63081":889,"63082":890,"63083":891,"63085":892,"63087":893,"63092":894,"63094":895,"63096":896,"63097":897,"63099":898,"63100":899,"63103":900,"63105":901,"63106":902,"63107":903,"63108":904,"63111":905,"63112":906,"63113":907,"63126":908,"63128":909,"63129":910,"63130":911,"63131":912,"63136":913,"63137":914,"63143":915,"63145":916,"63149":917,"63158":918,"63159":919,"63163":920,"63166":921,"63168":922,"63171":923,"63172":924,"63183":925,"63185":926,"63187":927,"63189":928,"63191":929,"63193":930,"63197":931,"63198":932,"63202":933,"63203":934,"63206":935,"63208":936,"63212":937,"63213":938,"63216":939,"63217":940,"63218":941,"63226":942,"63228":943,"63231":944,"63232":945,"63243":946,"63244":947,"63246":948,"63252":949,"63253":950,"63255":951,"63262":952,"63266":953,"63272":954,"63273":955,"63278":956,"63279":957,"63291":958,"63292":959,"63293":960,"63296":961,"63299":962,"63303":963,"63309":964,"63315":965,"63318":966,"63322":967,"63323":968,"63326":969,"63327":970,"63337":971,"63339":972,"63346":973,"63347":974,"63356":975,"63357":976,"63360":977,"63361":978,"63363":979,"63364":980,"63366":981,"63367":982,"63368":983,"63372":984,"63379":985,"63380":986,"63382":987,"63388":988,"63391":989,"63392":990,"63394":991,"63396":992,"63397":993,"63398":994,"63401":995,"63402":996,"63403":997,"63405":998,"63406":999,"63413":1000,"63414":1001,"63417":1002,"63418":1003,"63421":1004,"63423":1005,"63424":1006,"63426":1007,"63428":1008,"63429":1009,"63433":1010,"63434":1011,"63436":1012,"63437":1013,"63438":1014,"63440":1015,"63442":1016,"63447":1017,"63448":1018,"63449":1019,"63450":1020,"63460":1021,"63461":1022,"63462":1023,"63468":1024,"63471":1025,"63474":1026,"63477":1027,"63479":1028,"63482":1029,"63483":1030,"63493":1031,"63494":1032,"63495":1033,"63501":1034,"63503":1035,"63504":1036,"63506":1037,"63509":1038,"63510":1039,"63512":1040,"63529":1041,"63530":1042,"63535":1043,"63550":1044,"63562":1045,"63564":1046,"63568":1047,"63571":1048,"63587":1049,"63597":1050,"63609":1051,"63611":1052,"63612":1053,"63613":1054,"63617":1055,"63618":1056,"63620":1057,"63621":1058,"63622":1059,"63623":1060,"63633":1061,"63639":1062,"63680":1063,"63681":1064,"63692":1065,"63705":1066,"63743":1067} From 1697982f8746f8d4a00b71e9d3e2c89c9110b64b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 14 Apr 2026 10:54:30 -0700 Subject: [PATCH 097/104] DOC: add documentation to the icon options getter --- pcdswidgets/builder/get_icon_options.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pcdswidgets/builder/get_icon_options.py b/pcdswidgets/builder/get_icon_options.py index b8f1f54..87ef06b 100644 --- a/pcdswidgets/builder/get_icon_options.py +++ b/pcdswidgets/builder/get_icon_options.py @@ -15,6 +15,11 @@ def show_icon_options(): + """ + Show a simple qt window with a grid of valid rendered icons alongside their names. + + This is the full set of usable icons from pydm's iconfont. + """ app = QApplication([]) main_window = QMainWindow() scroll_area = QScrollArea() @@ -59,6 +64,9 @@ def show_icon_options(): def generate_icon_options(): + """ + Generate icon_options.py, which contains a large enum with icon options. + """ jinja_template = "icon_options.j2" env = Environment(trim_blocks=True, loader=PackageLoader("pcdswidgets", "builder")) template = env.get_template(jinja_template) @@ -72,6 +80,9 @@ def generate_icon_options(): def get_icon_options() -> list[str]: + """ + Returns the names of all the icons present in pydm's iconfont with valid rendering. + """ # The charmap file is everything that pydm recognizes as an icon, including things it has no image data for with open(Path(iconfont.__file__).parent / "fontawesome-charmap.json", "r") as fd: charmap: dict[str, str] = json.load(fd) From 104f0940e046882d51544fc51fa8ebf9e4a90fe2 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 14 Apr 2026 11:02:45 -0700 Subject: [PATCH 098/104] MNT: avoid updating init filestamps if they already exist, allows us to skip install in some cases --- pcdswidgets/builder/inits.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pcdswidgets/builder/inits.py b/pcdswidgets/builder/inits.py index ee9caff..38e89c7 100644 --- a/pcdswidgets/builder/inits.py +++ b/pcdswidgets/builder/inits.py @@ -42,7 +42,8 @@ def build_inits(base_dir: Path): if "__pycache__" not in path.parts: candidates.add(path.with_name("__init__.py")) for cand_path in candidates: - cand_path.touch() + if not cand_path.exists(): + cand_path.touch() if __name__ == "__main__": From dfc2cf42b30f1390de354d47b1f1bc40904b30e3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 14 Apr 2026 11:15:57 -0700 Subject: [PATCH 099/104] DOC: readme content tweaks and additions --- README.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fbaddc6..8be920c 100644 --- a/README.md +++ b/README.md @@ -99,22 +99,22 @@ Other guidelines: ### Adding a Symbol-based Vacuum Widget This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. -This will require at least some familiarity with Python and with the structure of this module. +This will require at least some familiarity with `Python`, `Qt`, `PyQt`, `pydm`, and with the structure of this module. Largely: refer back to the existing widgets. The steps are: -1. Create a new subclass of BaseSymbolIcon in the icons subfolder +1. Create a new subclass of `BaseSymbolIcon` in the icons subfolder - Define a path - Implement draw_icon -2. Create a new subclass of PCDSSymbolBase +2. Create a new subclass of `PCDSSymbolBase` - Include your icon as self.icon - Add relevant properties as needed, or inherit them from the existing mixins - - include the _qt_designer_ class attribute -3. make, to update pyproject.toml and the venv with new widget locations + - include the `_qt_designer_` class attribute +3. `make`, to update `pyproject.toml` and the venv with new widget locations -If the widget has been added and is included in the pyproject.toml file, it will appear in designer after installing pcdswidgets. +If the widget has been added and is included in the `pyproject.toml` file, it will appear in designer after installing pcdswidgets. ### Adding a Composite Widget @@ -125,9 +125,15 @@ Note that we can currently only run designer with custom widgets on our Rocky9 O This is not required, but you would do this to make your widget globally available, trivially discoverable, and easier to add to screens. The alternative is to pass your widget around via filepath in `PyDMEmbeddedDisplay`, which works but doesn't have the above advantages. -This requires only basic Python knowledge. +This requires basic `python` knowledge as well as familiarity with making `pydm` displays. -The steps are: +Here are some supplemental pages from the official `pydm` docs that you should understand before adding a widget: + +- [PyDM Macro Substitution](https://slaclab.github.io/pydm/tutorials/intro/macros.html) +- [Creating a small (widget) ui file with macros](https://slaclab.github.io/pydm/tutorials/action/designer_inline.html) +- [Creating a screen that uses embedded displays](https://slaclab.github.io/pydm/tutorials/action/designer_main.html) + +The steps for creating a new widget are: 1. Create a widget as a PyDM screen - Use qt `designer` to define the layout (saves a .ui file) @@ -136,7 +142,7 @@ The steps are: - Use `PyDMEmbeddedDisplay` to include your widget in other screens - Iterate, update the widget until you like it. 3. Bring it here - - Create a directory under ui, if needed: the form must be `pcdswidgets/ui/$subsystem/$type` + - Create a directory under ui, if needed, or use a suitable existing directory: the form must be `pcdswidgets/ui/$subsystem/$type` - Examples of subsystem: motion, vacuum - Examples of type: common, smaract, beckhoff - Pick a name for the ui file following the widget naming rules above @@ -167,26 +173,31 @@ class MyClassFull(MyClassFullBase): designer_options = DesignerOptions( group="ECS Subsystem Type", is_container=False, - icon=None, + icon=IconOptions.NONE, ) ``` If you like, you can extend these classes to add additional python code to use at runtime. #### Icons -If you want to set a non-default icon for the designer widget list, you can include a QIcon or a string -in the "icon" attribute of the `DesignerOptions` dataclass: +If you want to set a non-default icon for the designer widget list, +there are a bunch of standard icons distributed by pydm that are accessible using the IconOptions enum. +Set this in the `icon` option of `DesignerOptions`. + ``` designer_options = DesignerOptions( group="ECS Subsystem Type", is_container=False, - icon="expand-arrows-alt", + icon=IconOptions.expand_arrows_alt, ) ``` -If this is a string, we'll convert it to a `QIcon` using `Pydm`'s `IconFont`. -This uses a portable version of `fontawesome`, try running `qta-browser` -and look through everything with the `fa5s` prefix to browse options. +We'll convert these enums to a `QIcon` using `Pydm`'s `IconFont`. +This uses a portable version of `fontawesome`, try running `show_icon_options.sh` +to see all of the icons rendered in a grid. + +If you want to make your own icon, you can create a custom `QIcon` using any method you wish +and include it as the `icon` option here. #### Limitations @@ -194,3 +205,4 @@ and look through everything with the `fa5s` prefix to browse options. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. - Only direct QString and QStringList properties are supported. We still need to implement support for item-based QString widgets such as QListWidget. - The ordering of the designer widget categories is chaotic. This will require an update to PyDM to resolve. +- In pydm, you can edit a ui file by hand and add a macro anywhere. This is not supported for composite widgets. From 40025b8a26a6bbc78d64fcf754925a95d1b6b1d3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 14 Apr 2026 15:49:40 -0700 Subject: [PATCH 100/104] DOC: rework widget instructions in tutorial form --- README.md | 339 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 229 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 8be920c..02af677 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,46 @@ # pcdswidgets -LCLS PyDM Widget Library +## Usage +This is a widget library that uses the `pydm` framework to add additional widgets to the `pydm` ecosystem. + +When `pcdswidgets` is installed in a `python` environment, it will provide: + +- Additional widgets in designer via `pydm`'s widget entrypoint. +- The same additional widgets at runtime for use in `pydm` and `pyqt` displays. + +At `lcls`, this is currently distributed as part of the `pcds_conda` environments: + +``` +source pcds_conda +designer +``` + +Note that for the designer integration to work properly, the python designer plugin must be built and installed correctly. + ## Installation -### Prod +### Production Environments +`pcdswidgets` is packaged using standard tools and can be installed with standard tools. We maintain both `pypi` and `conda-forge` builds. + Pick your favorite: -- pip install pcdswidgets -- conda install pcdswidgets +- `pip install pcdswidgets` +- `conda install pcdswidgets` + +You can also install install `pcdswidgets` using other standard tools (such as `uv`) or directly from source in `github`. -### Dev -A helper script is included here: `build_local_venv.sh` (or, `make venv`). -This will create a `.venv` virtual environment that will be ready to go +### Development Environments +A helper script is included here: `build_local_venv.sh` (or, `make venv`) (or, just `make`). + +This will create a virtual environment under the `.venv` folder that will be ready to go to help you run designer and test your custom widgets. To work, this requires a suitable base environment to already exist on your system: one with pyqt and designer python plugin support, -which I've found to be tricky to set up in a scripted way in recent years. +which is tricky to set up properly. These base environments are stored centrally at LCLS and are specified in `base_env_vars.sh`. +If you are not at LCLS, you will need to edit this file to use these scripts. You can run the `build_local_venv.sh` again (or, `make venv`) to update the environment with any new widgets you've added since the last run. @@ -31,178 +53,275 @@ your new widgets. You can alternatively build your own environment: -- pip install -e . +- `pip install -e .` or -- uv sync +- `uv sync` -Or whatever your favorite method is. +or whatever your favorite method is. Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS. -This is due to complications in build process. +This is due to complications in the build process. + + +## Adding Widgets Tutorial +There are two kinds of widgets in `pcdswidgets`: +composite widgets, which start their lifecycles as standard `pydm` screens, +and symbol-based widgets, which start their lifecycles here in `pcdswidgets`. + +This tutorial will first go through how to add composite widgets, and then how to add symbol-based widgets. It is expected that most contributors will be adding composite widgets. + +Along the way, we'll introduce concepts like naming and sizing rules as they become relevant. + + +### Why would I add a widget? +Before starting, consider why you might add a widget to `pcdswidgets`. +Some good reasons may be: + +- Making a particularly useful or ubiquitous widget globally available and discoverable +- Making a high-usage widget easier to add to screens and control the settings of + +The alternative is to pass your widget around via filepath and macros using `PyDMEmbeddedDisplay`, +which works great and may be sufficient for many use cases. + + +### Provisioning a Composite Widget +Before cloning this repo, you should create your widget as a `pydm` screen and try it out. +It will be simpler and faster to iterate on your design this way +and you can get immediate feedback without doing any library work. + +If you don't know how to do this, refer to the `pydm` documentation: + +- [PyDM Macro Substitution](https://slaclab.github.io/pydm/tutorials/intro/macros.html) +- [Creating a small (widget) ui file with macros](https://slaclab.github.io/pydm/tutorials/action/designer_inline.html) +- [Creating a screen that uses embedded displays](https://slaclab.github.io/pydm/tutorials/action/designer_main.html) + +Before getting too deep, however, please consider widget sizing. -## Adding Widgets ### Widget Sizing +We have some strict guidelines on widget sizing. These are established to give us some consistency in application of widgets, as well as to make it simpler to avoid resizing a widget between library releases. + Device control widgets should fall into exactly one of three size classes. +(Note: we can add more size classes if necessary). Widgets can be smaller than the maximum of their size class by up to 20% before being flagged by CI. To ensure sizing consistency, set the minimum and maximum sizes to values that look good throughout the range and are permissible sizes as recorded below. -It's recommended to used fixed sizing when possible because dynamic sizing is hard to do right. +It's recommended to used fixed sizing when possible because dynamic sizing is hard to implement correctly. Widgets should always be maintained to work at the original designed size, because changing this can break existing screens. -#### Full Size -- Width: 400px -- Height: 125px +| Size Class | Width | Height | +| ---------- | ----- | -------| +| Full | 400 px | 125 px | +| Compact | 100 px | 75 px | +| Row | 800 px | 50 px | -#### Compact Size -- Width: 100px -- Height: 75px +Rows are also allowed to be double-height, e.g. 100px height. -#### Row Size -- Width: 800px -- Height: 50px +Note: widgets that aren't control widgets (containers, etc.) should not have a maximum or a minimum size. These widgets should instead be usable at any size. There is a list in the test suite to add test exceptions for these. -Rows are also allowed to be double-height, e.g. 100px height. -#### Widgets that aren't control widgets (containers, etc.) -These should not have a maximum or a minimum size- they should be usable at any size. +### Environment Setup +If you've gotten this far, with a provisioned widget of a good size class, it's time to set up your dev environment. +Before we begin, please clone the source code and make sure you can establish a working `designer` build +using the commands below. + +Note that at LCLS this only works on rocky9 machines! + +``` +make +./try_in_designer.sh +``` + +If the `make` completes successfully, you will have a working `python` environment +and `try_in_designer.sh` will open a designer window with the existing `pcdswidgets` widgets in the sidebar. + -#### Widgets created before 2026 -These may have a variety of sizes because we had no standards, and will not be checked in CI. +### Adding Your Composite Widget: Part 1 +1. Decide on your widget category: this is the subsystem and the type of the widget. + - Example subsystems include "motion" and "vacuum". + - Example types include "common", "smaract", and "beckhoff" + +2. Copy your `.ui` file into `pcdswidgets` in the correct folder: `pcdswidgets/ui/${subsystem}/${type}` + - Example: `pcdswidgets/ui/motion/beckhoff` + - If this folder does not exist, consider if an existing folder is appropriate. + - If no existing folder is appropriate, feel free to create a new folder. + +3. Rename your `.ui` file to match the widget naming convention below. + - It's important to be intentional about widget naming because renaming a widget can break existing screens. ### Widget Naming -Widget names should contain three parts: +Widget names and ui filenames should have one to one correspondance and contain three parts: - Type of device controlled - Descriptor word to differentiate this widget from other possible widgets with the same device type and size - Size class signifier +For casing: +- ui filenames should be lowercase_with_underscores for ease of working with filenames +- class names should use CamelCase to match qt and python naming conventions + - The class name will be generated automatically from the ui filename. + +Examples: +- `motor_classic_full.ui` (`MotorClassicFull`) + - Controls generic EPICS motor record + - Is inspired by the classic EDM style + - Is sized to be the "full" size +- `motor_tc_classic_row.ui` (`MotorTcClassicRow`) + - Controls generic EPICS motor record with a thermocouple added + - Is inspired by the classic EDM style + - Is sized to be the "row" size + Other guidelines: - The name should not be unnecessarily long, but avoid abbreviations. -- The name should use CamelCase to match qt and python class naming conventions. - - - For example, the first widget added in 2026 was `MotorClassicFull`, because it controls generic epics record motors, is inspired by the classic EDM style, and has the full size class. - - If multiple devices are controlled, include them in order of importance, e.g. `MotorTcClassicRow`. -- There is no need to end a widget name with "Widget". Please avoid this. -- Widgets with ui files, such as the composite widgets, should have parity between the ui file name and the widget name, for example `motor_classic_full.ui` for `MotorClassicFull`. +- There is no need to end a widget name or filename with "Widget". Please avoid this. - Widgets should never be renamed between tags, this will break existing screens. - Widgets named before 2026 may break some of these rules because we don't want to rename them. -### Adding a Symbol-based Vacuum Widget -This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. - -This will require at least some familiarity with `Python`, `Qt`, `PyQt`, `pydm`, and with the structure of this module. - -Largely: refer back to the existing widgets. - -The steps are: +### Adding Your Composite Widget: Part 2 +4. Run `make` to generate the code and update the project metadata. + - This will generate at least three `.py` files and add a row to `pyproject.toml` + - Do not edit the files in `generated`. +5. Try it out! + - Run `./try_in_designer.sh` and make a test screen. (Which, reminder: only works on rocky9 at LCLS) + - After you've made a test screen, then do `./try_in_pydm.sh my_screen.ui` for further testing. + - Make sure to take screenshots to include in your pull request. -1. Create a new subclass of `BaseSymbolIcon` in the icons subfolder - - Define a path - - Implement draw_icon -2. Create a new subclass of `PCDSSymbolBase` - - Include your icon as self.icon - - Add relevant properties as needed, or inherit them from the existing mixins - - include the `_qt_designer_` class attribute -3. `make`, to update `pyproject.toml` and the venv with new widget locations +At this point, if you like what you see, you're actually done. +You can commit, push, and make a pull request if you'd like. +The next few sections are optional. -If the widget has been added and is included in the `pyproject.toml` file, it will appear in designer after installing pcdswidgets. +Some notes: +- If you edit the ui file, you should `make` again, or your changes will not take effect. +- If you change your mind about which subsystem and type directory you'd like to use, you must manually delete the generated files from the old location. -### Adding a Composite Widget -This is how you would convert a .ui file with macro substitution that is normally used with `PyDMEmbeddedDisplay` into a designer widget served from here. -Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS! +### Optional: Edit the Designer Settings +One of the built files is not in `generated`, instead it is in `pcdswidgets/ui/${subsystem}/${type}`. -This is not required, but you would do this to make your widget globally available, trivially discoverable, and easier to add to screens. -The alternative is to pass your widget around via filepath in `PyDMEmbeddedDisplay`, which works but doesn't have the above advantages. +This file is free to edit and, among other thing, contains a `DesignerOptions` specification for the widget. -This requires basic `python` knowledge as well as familiarity with making `pydm` displays. +This looks something like: -Here are some supplemental pages from the official `pydm` docs that you should understand before adding a widget: +``` +class MyClassFull(MyClassFullBase): + designer_options = DesignerOptions( + group="ECS Subsystem Type", + is_container=False, + icon=IconOptions.NONE, + ) +``` -- [PyDM Macro Substitution](https://slaclab.github.io/pydm/tutorials/intro/macros.html) -- [Creating a small (widget) ui file with macros](https://slaclab.github.io/pydm/tutorials/action/designer_inline.html) -- [Creating a screen that uses embedded displays](https://slaclab.github.io/pydm/tutorials/action/designer_main.html) +The editable options are: +- `group`, which determines which category the widget sorts into in the designer sidebar. +- `is_container`, which tells designer if we should be able to drag other widgets into this one in designer. +- `icon`, which tells designer which icon to use in the designer sidebar (see next section) -The steps for creating a new widget are: - -1. Create a widget as a PyDM screen - - Use qt `designer` to define the layout (saves a .ui file) - - Use `PyDM` macros to define user inputs -2. Try it! - - Use `PyDMEmbeddedDisplay` to include your widget in other screens - - Iterate, update the widget until you like it. -3. Bring it here - - Create a directory under ui, if needed, or use a suitable existing directory: the form must be `pcdswidgets/ui/$subsystem/$type` - - Examples of subsystem: motion, vacuum - - Examples of type: common, smaract, beckhoff - - Pick a name for the ui file following the widget naming rules above - - Copy in your .ui file to the correct folder with the new name -4. `make` - - This should have created two python files in `generated`, which are not to be edited by hand. - - It also creates a python file in `pcdswidgets/$subsystem/$type` which can be edited by hand if you'd like to. - - It will also create some number of `__init__.py` files to make the generated filetrees valid Python modules. -5. Try it out - - Run `./try_in_designer.sh` and make a test screen. (Which, reminder: only works on rocky9 at LCLS) - - After you've made a test screen, then do `./try_in_pydm.sh my_screen.ui` for further testing. -6. Pick an icon (optional) - - You can select an icon for your widget to use in designer. See the sections below about designer settings and icons. -7. Make a PR! - - Commit - - Take some screenshots (in designer, and in pydm) -Some notes: +### Optional: Choose a Designer Icon +The designer icon is the symbol that appears to the left of the widget name in the left-hand widget box. +The default designer icon is simply the `Qt` logo. If you'd like to change it, you have a few options. -- If you edit the ui file, you should `make` again, or your changes will not take effect. -- If you change your mind about which subsystem and type directory you'd like to use, you must manually delete the generated files from the old location. +1. Use `IconOptions` (recommended) + - `pydm` provides the free subset of fontawesome as icons. + - You can select one of these by changing `IconOptions.NONE` to any of the other enum options. + - If you're using an IDE, the options should autocomplete. + - To see all of the options, run `show_icon_options.sh`. This will open up a grid with all of the options and names. +Here's an example: -#### Widget Classes -The widget classes look something like: ``` class MyClassFull(MyClassFullBase): designer_options = DesignerOptions( group="ECS Subsystem Type", is_container=False, - icon=IconOptions.NONE, + icon=IconOptions.expand_arrows_alt, ) ``` -If you like, you can extend these classes to add additional python code to use at runtime. +2. Create your own `QIcon` + - You can use the qt APIs to create your own icon object + - For example: from a `.png` + - Please refer to the qt/pyqt docs for how to do this + - You can set `icon=your_qicon_object` in your `DesignerOptions` to include your custom icon + + +### Optional: Add Logic to a Composite Widget +The widget class here that includes the `designer_options` object is exactly the class that will be used +when your widget is included in a screen. +This means you can add code to the widget to override and extend any built-in behavior. -#### Icons -If you want to set a non-default icon for the designer widget list, -there are a bunch of standard icons distributed by pydm that are accessible using the IconOptions enum. -Set this in the `icon` option of `DesignerOptions`. +There are a few things to keep in mind when you do this: +1. If you override `__init__`, you must call `super().__init__(parent)` before doing any other `qt`-related operations. +2. There is no way to pass custom arguments to `__init__` in `designer`. + Any parameterization should be done via qt properties, which will show up in the sidebar. + - If you do this, do not assume that the properties will be set in any particular order. + Make your code work regardless of which order the properties are set in. +3. Be wary of backwards compatibility. + - Removing preoperties from a widget will break existing screens. + +Here is an example where we add a single configuration parameter that does nothing. ``` +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + + +class MyClassFull(MyClassFullBase): designer_options = DesignerOptions( group="ECS Subsystem Type", is_container=False, - icon=IconOptions.expand_arrows_alt, + icon=IconOptions.NONE, ) -``` -We'll convert these enums to a `QIcon` using `Pydm`'s `IconFont`. -This uses a portable version of `fontawesome`, try running `show_icon_options.sh` -to see all of the icons rendered in a grid. + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self._my_value = 0 -If you want to make your own icon, you can create a custom `QIcon` using any method you wish -and include it as the `icon` option here. + def get_my_value(self) -> int: + return self._my_value + + def set_my_value(self, value: int) -> None: + self._my_value = value + + my_value = pyqtProperty(int, get_motor, set_motor) +``` -#### Limitations +### Composite Widget Limitations - Widgets that contain `PyDMEmbeddedWidget` are not supported: bootstrap these by turning the contents into widgets themselves. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. -- Only direct QString and QStringList properties are supported. We still need to implement support for item-based QString widgets such as QListWidget. -- The ordering of the designer widget categories is chaotic. This will require an update to PyDM to resolve. -- In pydm, you can edit a ui file by hand and add a macro anywhere. This is not supported for composite widgets. +- Only direct `QString` and `QStringList` properties are supported. We still need to implement support for item-based `QString` widgets such as `QListWidget`. +- The ordering of the designer widget categories is chaotic. This will require an update to `pydm` to resolve. +- In `pydm`, you can edit a ui file by hand and add a macro anywhere. This is not supported for composite widgets. + + +### Adding a Symbol-based Vacuum Widget +This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. + +This will require at least some familiarity with `Python`, `Qt`, `PyQt`, `pydm`, and with the structure of this module. + +Largely: refer back to the existing widgets. + +The steps are: + +1. Create a new subclass of `BaseSymbolIcon` in the icons subfolder + - Define a path + - Implement draw_icon +2. Create a new subclass of `PCDSSymbolBase` + - Include your icon as self.icon + - Add relevant properties as needed, or inherit them from the existing mixins + - include the `_qt_designer_` class attribute +3. `make`, to update `pyproject.toml` and the venv with new widget locations + +If the widget has been added and is included in the `pyproject.toml` file, it will appear in designer after installing pcdswidgets. From d75dcb58faeb6b45d30648ee1f97d255e0d117dd Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 15 Apr 2026 17:44:33 -0700 Subject: [PATCH 101/104] DOC: manual nitpicks and response to review --- README.md | 71 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 02af677..dffc702 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,9 @@ This is due to complications in the build process. ## Adding Widgets Tutorial -There are two kinds of widgets in `pcdswidgets`: -composite widgets, which start their lifecycles as standard `pydm` screens, -and symbol-based widgets, which start their lifecycles here in `pcdswidgets`. +There are two kinds of widgets in `pcdswidgets` +1. Composite widgets, which start their lifecycles as standard `pydm` screens and are composed entirely of smaller standard widgets. +2. Symbol-based widgets, which start their lifecycles here in `pcdswidgets` and feature fully custom symbol components. This tutorial will first go through how to add composite widgets, and then how to add symbol-based widgets. It is expected that most contributors will be adding composite widgets. @@ -97,7 +97,7 @@ If you don't know how to do this, refer to the `pydm` documentation: - [Creating a small (widget) ui file with macros](https://slaclab.github.io/pydm/tutorials/action/designer_inline.html) - [Creating a screen that uses embedded displays](https://slaclab.github.io/pydm/tutorials/action/designer_main.html) -Before getting too deep, however, please consider widget sizing. +Before getting too deep, however, please consider widget sizing: ### Widget Sizing @@ -105,7 +105,6 @@ We have some strict guidelines on widget sizing. These are established to give u Device control widgets should fall into exactly one of three size classes. (Note: we can add more size classes if necessary). -Widgets can be smaller than the maximum of their size class by up to 20% before being flagged by CI. To ensure sizing consistency, set the minimum and maximum sizes to values that look good throughout the range and are permissible sizes as recorded below. @@ -119,9 +118,10 @@ Widgets should always be maintained to work at the original designed size, becau | Compact | 100 px | 75 px | | Row | 800 px | 50 px | -Rows are also allowed to be double-height, e.g. 100px height. - -Note: widgets that aren't control widgets (containers, etc.) should not have a maximum or a minimum size. These widgets should instead be usable at any size. There is a list in the test suite to add test exceptions for these. +Note: +- All widgets area allowed to be smaller than the mazimum of their size class by up to 20%. +- Rows are also allowed to be double-height, e.g. 100px height. +- Widgets that aren't control widgets (containers, etc.) should not have a maximum or a minimum size. These widgets should instead be usable at any size. There is a list in the test suite to add test exceptions for these. ### Environment Setup @@ -143,9 +143,9 @@ and `try_in_designer.sh` will open a designer window with the existing `pcdswidg ### Adding Your Composite Widget: Part 1 1. Decide on your widget category: this is the subsystem and the type of the widget. - Example subsystems include "motion" and "vacuum". - - Example types include "common", "smaract", and "beckhoff" + - Example types include "common", "smaract", and "beckhoff". -2. Copy your `.ui` file into `pcdswidgets` in the correct folder: `pcdswidgets/ui/${subsystem}/${type}` +2. Copy your `.ui` file into `pcdswidgets` in the folder corresponding with your choices in step 1: `pcdswidgets/ui/${subsystem}/${type}` - Example: `pcdswidgets/ui/motion/beckhoff` - If this folder does not exist, consider if an existing folder is appropriate. - If no existing folder is appropriate, feel free to create a new folder. @@ -162,8 +162,8 @@ Widget names and ui filenames should have one to one correspondance and contain - Size class signifier For casing: -- ui filenames should be lowercase_with_underscores for ease of working with filenames -- class names should use CamelCase to match qt and python naming conventions +- `.ui` filenames should be lowercase_with_underscores for ease of working with filenames. +- Class names should use CamelCase to match qt and python naming conventions. - The class name will be generated automatically from the ui filename. Examples: @@ -187,10 +187,10 @@ Other guidelines: ### Adding Your Composite Widget: Part 2 4. Run `make` to generate the code and update the project metadata. - - This will generate at least three `.py` files and add a row to `pyproject.toml` + - This will generate at least three `.py` files and add a row to `pyproject.toml`. - Do not edit the files in `generated`. 5. Try it out! - - Run `./try_in_designer.sh` and make a test screen. (Which, reminder: only works on rocky9 at LCLS) + - Run `./try_in_designer.sh` and make a test screen. (Which, reminder: only works on rocky9 at LCLS). - After you've made a test screen, then do `./try_in_pydm.sh my_screen.ui` for further testing. - Make sure to take screenshots to include in your pull request. @@ -201,13 +201,14 @@ The next few sections are optional. Some notes: - If you edit the ui file, you should `make` again, or your changes will not take effect. -- If you change your mind about which subsystem and type directory you'd like to use, you must manually delete the generated files from the old location. +- If you change your mind about which subsystem and type directory you'd like to use, you must manually delete the generated files from the old locations. ### Optional: Edit the Designer Settings -One of the built files is not in `generated`, instead it is in `pcdswidgets/ui/${subsystem}/${type}`. +One of the built files is in `pcdswidgets/ui/${subsystem}/${type}`. -This file is free to edit and, among other thing, contains a `DesignerOptions` specification for the widget. +Unlike the files in `generated`, this file is free to edit, +and, among other thing, contains a `DesignerOptions` specification for the widget. This looks something like: @@ -223,7 +224,7 @@ class MyClassFull(MyClassFullBase): The editable options are: - `group`, which determines which category the widget sorts into in the designer sidebar. - `is_container`, which tells designer if we should be able to drag other widgets into this one in designer. -- `icon`, which tells designer which icon to use in the designer sidebar (see next section) +- `icon`, which tells designer which icon to use in the designer sidebar (see next section). ### Optional: Choose a Designer Icon @@ -248,10 +249,10 @@ class MyClassFull(MyClassFullBase): ``` 2. Create your own `QIcon` - - You can use the qt APIs to create your own icon object - - For example: from a `.png` - - Please refer to the qt/pyqt docs for how to do this - - You can set `icon=your_qicon_object` in your `DesignerOptions` to include your custom icon + - You can use the `Qt` APIs to create your own icon object. + - For example: you can create an icon from a `.png`. + - Please refer to the `Qt`/`PyQt` docs for how to do this. + - You can set `icon=your_qicon_object` in your `DesignerOptions` to include your custom icon. ### Optional: Add Logic to a Composite Widget @@ -260,15 +261,15 @@ when your widget is included in a screen. This means you can add code to the widget to override and extend any built-in behavior. There are a few things to keep in mind when you do this: -1. If you override `__init__`, you must call `super().__init__(parent)` before doing any other `qt`-related operations. +1. If you override `__init__`, you must call `super().__init__(parent)` before doing any other `Qt`-related operations. 2. There is no way to pass custom arguments to `__init__` in `designer`. - Any parameterization should be done via qt properties, which will show up in the sidebar. + - Any parameterization should be done via `Qt` properties, which will show up in the sidebar. - If you do this, do not assume that the properties will be set in any particular order. - Make your code work regardless of which order the properties are set in. + - Make your code work regardless of which order the properties are set in. 3. Be wary of backwards compatibility. - - Removing preoperties from a widget will break existing screens. + - Removing properties from a widget will break existing screens. -Here is an example where we add a single configuration parameter that does nothing. +Here is an example where we add a single configuration parameter that does nothing. In practice, you would also change something meaningful about the widget during the setter. ``` try: @@ -306,7 +307,7 @@ class MyClassFull(MyClassFullBase): - In `pydm`, you can edit a ui file by hand and add a macro anywhere. This is not supported for composite widgets. -### Adding a Symbol-based Vacuum Widget +### Adding a Symbol-based Widget This is how you would add e.g. a pump or valve widget with a custom drawing symbol and some color awareness. This will require at least some familiarity with `Python`, `Qt`, `PyQt`, `pydm`, and with the structure of this module. @@ -315,13 +316,19 @@ Largely: refer back to the existing widgets. The steps are: -1. Create a new subclass of `BaseSymbolIcon` in the icons subfolder +1. Create a new subclass of `BaseSymbolIcon` in the icons subfolder. - Define a path - Implement draw_icon -2. Create a new subclass of `PCDSSymbolBase` +2. Create a new subclass of `PCDSSymbolBase`. - Include your icon as self.icon - Add relevant properties as needed, or inherit them from the existing mixins - include the `_qt_designer_` class attribute -3. `make`, to update `pyproject.toml` and the venv with new widget locations +3. `make`, to update `pyproject.toml` and the venv with new widget locations. + +If the widget has been added and is included in the `pyproject.toml` file, it will appear in designer after installing `pcdswidgets`. -If the widget has been added and is included in the `pyproject.toml` file, it will appear in designer after installing pcdswidgets. +Note: +- At time of writing, all symbol-based widgets are vacuum widgets, and as such all the symbol-related code is in the vacuum folder. + - If you would like to make a non-vacuum widget in this style, you should first refactor to pull out the base icon and symbol code, then edit the readme here to remove this note. +- The colors of all the existing vacuum symbol widgets is based on stylesheet rules. We keep the latest version of the stylesheet in use at LCLS in another module: see [lcls-twincat-vacuum](https://github.com/pcdshub/lcls-pydm-vacuum/blob/master/styleSheet/masterStyleSheet.qss). + - You are not required to continue the stylesheet pattern if you add new symbol widgets. From 939a5fff0e72051d48042261077a0975252bf5d9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 15 Apr 2026 17:47:08 -0700 Subject: [PATCH 102/104] DOC: apply spelling and consistency fixes from LLM --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dffc702..ae0e2ba 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ This is a widget library that uses the `pydm` framework to add additional widget When `pcdswidgets` is installed in a `python` environment, it will provide: - Additional widgets in designer via `pydm`'s widget entrypoint. -- The same additional widgets at runtime for use in `pydm` and `pyqt` displays. +- The same additional widgets at runtime for use in `pydm` and `PyQt` displays. -At `lcls`, this is currently distributed as part of the `pcds_conda` environments: +At `LCLS`, this is currently distributed as part of the `pcds_conda` environments: ``` source pcds_conda @@ -19,14 +19,14 @@ Note that for the designer integration to work properly, the python designer plu ## Installation ### Production Environments -`pcdswidgets` is packaged using standard tools and can be installed with standard tools. We maintain both `pypi` and `conda-forge` builds. +`pcdswidgets` is packaged using standard tools and can be installed with standard tools. We maintain both `PyPI` and `conda-forge` builds. Pick your favorite: - `pip install pcdswidgets` - `conda install pcdswidgets` -You can also install install `pcdswidgets` using other standard tools (such as `uv`) or directly from source in `github`. +You can also install `pcdswidgets` using other standard tools (such as `uv`) or directly from source in `GitHub`. ### Development Environments @@ -35,7 +35,7 @@ A helper script is included here: `build_local_venv.sh` (or, `make venv`) (or, j This will create a virtual environment under the `.venv` folder that will be ready to go to help you run designer and test your custom widgets. To work, this requires a suitable base environment to already exist on -your system: one with pyqt and designer python plugin support, +your system: one with PyQt and designer python plugin support, which is tricky to set up properly. These base environments are stored centrally at LCLS and are @@ -61,7 +61,7 @@ or or whatever your favorite method is. -Note that we can currently only run designer with custom widgets on our Rocky9 OS machines at LCLS. +Note that we can currently only run designer with custom widgets on our Rocky 9 OS machines at LCLS. This is due to complications in the build process. @@ -108,7 +108,7 @@ Device control widgets should fall into exactly one of three size classes. To ensure sizing consistency, set the minimum and maximum sizes to values that look good throughout the range and are permissible sizes as recorded below. -It's recommended to used fixed sizing when possible because dynamic sizing is hard to implement correctly. +It's recommended to use fixed sizing when possible because dynamic sizing is hard to implement correctly. Widgets should always be maintained to work at the original designed size, because changing this can break existing screens. @@ -119,7 +119,7 @@ Widgets should always be maintained to work at the original designed size, becau | Row | 800 px | 50 px | Note: -- All widgets area allowed to be smaller than the mazimum of their size class by up to 20%. +- All widgets are allowed to be smaller than the maximum of their size class by up to 20%. - Rows are also allowed to be double-height, e.g. 100px height. - Widgets that aren't control widgets (containers, etc.) should not have a maximum or a minimum size. These widgets should instead be usable at any size. There is a list in the test suite to add test exceptions for these. @@ -129,7 +129,7 @@ If you've gotten this far, with a provisioned widget of a good size class, it's Before we begin, please clone the source code and make sure you can establish a working `designer` build using the commands below. -Note that at LCLS this only works on rocky9 machines! +Note that at LCLS this only works on Rocky 9 machines! ``` make @@ -155,7 +155,7 @@ and `try_in_designer.sh` will open a designer window with the existing `pcdswidg ### Widget Naming -Widget names and ui filenames should have one to one correspondance and contain three parts: +Widget names and ui filenames should have one to one correspondence and contain three parts: - Type of device controlled - Descriptor word to differentiate this widget from other possible widgets with the same device type and size @@ -208,7 +208,7 @@ Some notes: One of the built files is in `pcdswidgets/ui/${subsystem}/${type}`. Unlike the files in `generated`, this file is free to edit, -and, among other thing, contains a `DesignerOptions` specification for the widget. +and, among other things, contains a `DesignerOptions` specification for the widget. This looks something like: @@ -295,12 +295,12 @@ class MyClassFull(MyClassFullBase): def set_my_value(self, value: int) -> None: self._my_value = value - my_value = pyqtProperty(int, get_motor, set_motor) + my_value = pyqtProperty(int, get_my_value, set_my_value) ``` ### Composite Widget Limitations -- Widgets that contain `PyDMEmbeddedWidget` are not supported: bootstrap these by turning the contents into widgets themselves. +- Widgets that contain `PyDMEmbeddedDisplay` are not supported: bootstrap these by turning the contents into widgets themselves. - The automatic type hinting runs into issues when the qt object names are the same as the classnames. If you want to extend the composite widget class in python, giving your child widgets more unique names will result in more useful type hints, automatically. - Only direct `QString` and `QStringList` properties are supported. We still need to implement support for item-based `QString` widgets such as `QListWidget`. - The ordering of the designer widget categories is chaotic. This will require an update to `pydm` to resolve. From 89ed66cd2f70f8cf4c16bd63aedf4085ffe49aae Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 15 Apr 2026 17:47:29 -0700 Subject: [PATCH 103/104] DOC: missed a spot for literal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae0e2ba..784fbab 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ A helper script is included here: `build_local_venv.sh` (or, `make venv`) (or, j This will create a virtual environment under the `.venv` folder that will be ready to go to help you run designer and test your custom widgets. To work, this requires a suitable base environment to already exist on -your system: one with PyQt and designer python plugin support, +your system: one with `PyQt` and designer python plugin support, which is tricky to set up properly. These base environments are stored centrally at LCLS and are From 31160bc12e1883bb27222e8d13a53d5ca87e04d7 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 16 Apr 2026 11:56:34 -0700 Subject: [PATCH 104/104] FIX: remove redundant and incorrectly-timed init gen --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 60d0693..c4d5217 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ all: $(MAKE) pyproject.toml $(MAKE) venv -build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) inits +build: $(PY_FORM) $(PY_BASE) $(PY_MAIN) # Need to re-run form and base if the ui file is updated $(PY_FORM): pcdswidgets/generated/%_form.py: pcdswidgets/ui/%.ui