From eb17afd09c45494f4e3df8276049c6680503bbad Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 5 Jul 2024 11:40:19 +0000 Subject: [PATCH 1/3] Adopt copier 2.1.0 --- .copier-answers.yml | 16 ++ .devcontainer/devcontainer.json | 54 ++-- .github/CONTRIBUTING.md | 27 ++ .github/CONTRIBUTING.rst | 35 --- .../actions/install_requirements/action.yml | 65 ++--- .github/dependabot.yml | 8 + .github/pages/index.html | 2 +- .github/pages/make_switcher.py | 29 +-- .github/workflows/_check.yml | 27 ++ .github/workflows/_dist.yml | 36 +++ .github/workflows/{docs.yml => _docs.yml} | 27 +- .github/workflows/_pypi.yml | 17 ++ .github/workflows/_release.yml | 32 +++ .github/workflows/_test.yml | 62 +++++ .github/workflows/_tox.yml | 22 ++ .github/workflows/ci.yml | 59 +++++ .github/workflows/code.yml | 231 ------------------ .github/workflows/docs_clean.yml | 43 ---- .github/workflows/linkcheck.yml | 27 -- .github/workflows/periodic.yml | 13 + .gitignore | 1 - .pre-commit-config.yaml | 18 +- .vscode/extensions.json | 7 +- .vscode/launch.json | 24 +- .vscode/settings.json | 22 +- .vscode/tasks.json | 2 +- Dockerfile | 40 +-- README.md | 33 +++ README.rst | 54 ---- catalog-info.yaml | 10 + docs/conf.py | 2 +- docs/developer/explanations/decisions.rst | 17 -- .../0001-record-architecture-decisions.rst | 26 -- .../0002-switched-to-pip-skeleton.rst | 35 --- docs/developer/how-to/build-docs.rst | 38 --- docs/developer/how-to/contribute.rst | 1 - docs/developer/how-to/lint.rst | 39 --- docs/developer/how-to/make-release.rst | 16 -- docs/developer/how-to/pin-requirements.rst | 74 ------ docs/developer/how-to/run-tests.rst | 12 - docs/developer/how-to/static-analysis.rst | 8 - docs/developer/how-to/test-container.rst | 25 -- docs/developer/how-to/update-tools.rst | 16 -- docs/developer/index.rst | 66 ----- docs/developer/reference/standards.rst | 63 ----- docs/developer/tutorials/dev-install.rst | 68 ------ docs/explanations.md | 10 + docs/explanations/decisions.md | 12 + .../0001-record-architecture-decisions.md | 18 ++ ...0002-switched-to-python-copier-template.md | 28 +++ docs/explanations/decisions/COPYME | 19 ++ docs/genindex.md | 3 + docs/genindex.rst | 5 - docs/how-to.md | 10 + docs/how-to/contribute.md | 2 + docs/images/dls-favicon.ico | Bin 99678 -> 0 bytes docs/index.md | 56 +++++ docs/index.rst | 29 --- docs/reference.md | 12 + .../reference/api.rst => reference/api.md} | 11 +- docs/tutorials.md | 10 + docs/tutorials/installation.md | 42 ++++ docs/user/explanations/docs-structure.rst | 18 -- docs/user/how-to/run-container.rst | 15 -- docs/user/index.rst | 57 ----- docs/user/tutorials/installation.rst | 38 --- pyproject.toml | 34 +-- tests/conftest.py | 5 +- 68 files changed, 720 insertions(+), 1263 deletions(-) create mode 100644 .copier-answers.yml create mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/CONTRIBUTING.rst create mode 100644 .github/workflows/_check.yml create mode 100644 .github/workflows/_dist.yml rename .github/workflows/{docs.yml => _docs.yml} (74%) create mode 100644 .github/workflows/_pypi.yml create mode 100644 .github/workflows/_release.yml create mode 100644 .github/workflows/_test.yml create mode 100644 .github/workflows/_tox.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/code.yml delete mode 100644 .github/workflows/docs_clean.yml delete mode 100644 .github/workflows/linkcheck.yml create mode 100644 .github/workflows/periodic.yml create mode 100644 README.md delete mode 100644 README.rst create mode 100644 catalog-info.yaml delete mode 100644 docs/developer/explanations/decisions.rst delete mode 100644 docs/developer/explanations/decisions/0001-record-architecture-decisions.rst delete mode 100644 docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst delete mode 100644 docs/developer/how-to/build-docs.rst delete mode 100644 docs/developer/how-to/contribute.rst delete mode 100644 docs/developer/how-to/lint.rst delete mode 100644 docs/developer/how-to/make-release.rst delete mode 100644 docs/developer/how-to/pin-requirements.rst delete mode 100644 docs/developer/how-to/run-tests.rst delete mode 100644 docs/developer/how-to/static-analysis.rst delete mode 100644 docs/developer/how-to/test-container.rst delete mode 100644 docs/developer/how-to/update-tools.rst delete mode 100644 docs/developer/index.rst delete mode 100644 docs/developer/reference/standards.rst delete mode 100644 docs/developer/tutorials/dev-install.rst create mode 100644 docs/explanations.md create mode 100644 docs/explanations/decisions.md create mode 100644 docs/explanations/decisions/0001-record-architecture-decisions.md create mode 100644 docs/explanations/decisions/0002-switched-to-python-copier-template.md create mode 100644 docs/explanations/decisions/COPYME create mode 100644 docs/genindex.md delete mode 100644 docs/genindex.rst create mode 100644 docs/how-to.md create mode 100644 docs/how-to/contribute.md delete mode 100644 docs/images/dls-favicon.ico create mode 100644 docs/index.md delete mode 100644 docs/index.rst create mode 100644 docs/reference.md rename docs/{user/reference/api.rst => reference/api.md} (66%) create mode 100644 docs/tutorials.md create mode 100644 docs/tutorials/installation.md delete mode 100644 docs/user/explanations/docs-structure.rst delete mode 100644 docs/user/how-to/run-container.rst delete mode 100644 docs/user/index.rst delete mode 100644 docs/user/tutorials/installation.rst diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 00000000..13471899 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,16 @@ +# Changes here will be overwritten by Copier +_commit: 2.1.0 +_src_path: gh:DiamondLightSource/python-copier-template +author_email: gary.yendell@diamond.ac.uk +author_name: Gary Yendell +component_owner: mef65357 +description: EPICS PV Interface described in YAML +distribution_name: pvi +docker: false +docs_type: sphinx +git_platform: github.com +github_org: epics-containers +package_name: pvi +pypi: true +repo_name: pvi +type_checker: mypy diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 44de8d36..79b85ff4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,52 +3,44 @@ "name": "Python 3 Developer Container", "build": { "dockerfile": "../Dockerfile", - "target": "build", - // Only upgrade pip, we will install the project below - "args": { - "PIP_OPTIONS": "--upgrade pip" - } + "target": "developer" }, "remoteEnv": { + // Allow X11 apps to run inside the container "DISPLAY": "${localEnv:DISPLAY}" }, - // Add the URLs of features you want added when the container is built. - "features": { - "ghcr.io/devcontainers/features/common-utils:1": { - "username": "none", - "upgradePackages": false - } - }, - // Set *default* container specific settings.json values on container create. - "settings": { - "python.defaultInterpreterPath": "/venv/bin/python" - }, "customizations": { "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/venv/bin/python" + }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", + "github.vscode-github-actions", "tamasfe.even-better-toml", "redhat.vscode-yaml", - "ryanluker.vscode-coverage-gutters" + "ryanluker.vscode-coverage-gutters", + "charliermarsh.ruff", + "ms-azuretools.vscode-docker" ] } }, - // Make sure the files we are mapping into the container exist on the host - "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", + "features": { + // Some default things like git config + "ghcr.io/devcontainers/features/common-utils:2": { + "upgradePackages": false + } + }, "runArgs": [ + // Allow the container to access the host X11 display and EPICS CA "--net=host", - "--security-opt=label=type:container_runtime_t" - ], - "mounts": [ - "source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind", - "source=${localEnv:HOME}/.inputrc,target=/root/.inputrc,type=bind", - // map in home directory - not strictly necessary but useful - "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" + // Make sure SELinux does not disable with access to host filesystems like tmp + "--security-opt=label=disable" ], - // make the workspace folder the same inside and outside of the container - "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", - "workspaceFolder": "${localWorkspaceFolder}", + // Mount the parent as /workspaces so we can pip install peers as editable + "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind", // After the container is created, install the python project in editable form - "postCreateCommand": "pip install -e '.[dev]'" -} + "postCreateCommand": "pip install $([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e '.[dev]' && pre-commit install" +} \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..fdb377e2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contribute to the project + +Contributions and issues are most welcome! All issues and pull requests are +handled through [GitHub](https://github.com/epics-containers/pvi/issues). Also, please check for any existing issues before +filing a new one. If you have a great idea but it involves big changes, please +file a ticket before making a pull request! We want to make sure you don't spend +your time coding something that might not fit the scope of the project. + +## Issue or Discussion? + +Github also offers [discussions](https://github.com/epics-containers/pvi/discussions) as a place to ask questions and share ideas. If +your issue is open ended and it is not obvious when it can be "closed", please +raise it as a discussion instead. + +## Code Coverage + +While 100% code coverage does not make a library bug-free, it significantly +reduces the number of easily caught bugs! Please make sure coverage remains the +same or is improved by a pull request! + +## Developer Information + +It is recommended that developers use a [vscode devcontainer](https://code.visualstudio.com/docs/devcontainers/containers). This repository contains configuration to set up a containerized development environment that suits its own needs. + +This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects. + +For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.1.0/how-to.html). diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst deleted file mode 100644 index b31080a4..00000000 --- a/.github/CONTRIBUTING.rst +++ /dev/null @@ -1,35 +0,0 @@ -Contributing to the project -=========================== - -Contributions and issues are most welcome! All issues and pull requests are -handled through GitHub_. Also, please check for any existing issues before -filing a new one. If you have a great idea but it involves big changes, please -file a ticket before making a pull request! We want to make sure you don't spend -your time coding something that might not fit the scope of the project. - -.. _GitHub: https://github.com/epics-containers/pvi/issues - -Issue or Discussion? --------------------- - -Github also offers discussions_ as a place to ask questions and share ideas. If -your issue is open ended and it is not obvious when it can be "closed", please -raise it as a discussion instead. - -.. _discussions: https://github.com/epics-containers/pvi/discussions - -Code coverage -------------- - -While 100% code coverage does not make a library bug-free, it significantly -reduces the number of easily caught bugs! Please make sure coverage remains the -same or is improved by a pull request! - -Developer guide ---------------- - -The `Developer Guide`_ contains information on setting up a development -environment, running the tests and what standards the code and documentation -should follow. - -.. _Developer Guide: https://epics-containers.github.io/pvi/main/developer/how-to/contribute.html diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml index 20d7a3ad..d33e0805 100644 --- a/.github/actions/install_requirements/action.yml +++ b/.github/actions/install_requirements/action.yml @@ -1,57 +1,34 @@ name: Install requirements -description: Run pip install with requirements and upload resulting requirements +description: Install a version of python then call pip install and report what was installed inputs: - requirements_file: - description: Name of requirements file to use and upload - required: true - install_options: + python-version: + description: Python version to install, default is from Dockerfile + default: "dev" + pip-install: description: Parameters to pass to pip install - required: true - python_version: - description: Python version to install - default: "3.x" + default: "$([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e .[dev]" runs: using: composite - steps: - - name: Setup python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python_version }} - - - name: Pip install - run: | - touch ${{ inputs.requirements_file }} - # -c uses requirements.txt as constraints, see 'Validate requirements file' - pip install -c ${{ inputs.requirements_file }} ${{ inputs.install_options }} - shell: bash - - - name: Create lockfile + - name: Get version of python run: | - mkdir -p lockfiles - pip freeze --exclude-editable > lockfiles/${{ inputs.requirements_file }} - # delete the self referencing line and make sure it isn't blank - sed -i'' -e '/file:/d' lockfiles/${{ inputs.requirements_file }} + PYTHON_VERSION="${{ inputs.python-version }}" + if [ $PYTHON_VERSION == "dev" ]; then + PYTHON_VERSION=$(sed -n "s/ARG PYTHON_VERSION=//p" Dockerfile) + fi + echo "PYTHON_VERSION=$PYTHON_VERSION" >> "$GITHUB_ENV" shell: bash - - name: Upload lockfiles - uses: actions/upload-artifact@v3 + - name: Setup python + uses: actions/setup-python@v5 with: - name: lockfiles - path: lockfiles + python-version: ${{ env.PYTHON_VERSION }} - # This eliminates the class of problems where the requirements being given no - # longer match what the packages themselves dictate. E.g. In the rare instance - # where I install some-package which used to depend on vulnerable-dependency - # but now uses good-dependency (despite being nominally the same version) - # pip will install both if given a requirements file with -r - - name: If requirements file exists, check it matches pip installed packages - run: | - if [ -s ${{ inputs.requirements_file }} ]; then - if ! diff -u ${{ inputs.requirements_file }} lockfiles/${{ inputs.requirements_file }}; then - echo "Error: ${{ inputs.requirements_file }} need the above changes to be exhaustive" - exit 1 - fi - fi + - name: Install packages + run: pip install ${{ inputs.pip-install }} + shell: bash + + - name: Report what was installed + run: pip freeze shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fb7c6ee6..184ba363 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,8 +9,16 @@ updates: directory: "/" schedule: interval: "weekly" + groups: + actions: + patterns: + - "*" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" + groups: + dev-dependencies: + patterns: + - "*" diff --git a/.github/pages/index.html b/.github/pages/index.html index c495f39f..80f0a009 100644 --- a/.github/pages/index.html +++ b/.github/pages/index.html @@ -8,4 +8,4 @@ - + \ No newline at end of file diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py index ae227ab7..29f646c3 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -3,28 +3,27 @@ from argparse import ArgumentParser from pathlib import Path from subprocess import CalledProcessError, check_output -from typing import List, Optional -def report_output(stdout: bytes, label: str) -> List[str]: +def report_output(stdout: bytes, label: str) -> list[str]: ret = stdout.decode().strip().split("\n") print(f"{label}: {ret}") return ret -def get_branch_contents(ref: str) -> List[str]: +def get_branch_contents(ref: str) -> list[str]: """Get the list of directories in a branch.""" stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref]) return report_output(stdout, "Branch contents") -def get_sorted_tags_list() -> List[str]: +def get_sorted_tags_list() -> list[str]: """Get a list of sorted tags in descending order from the repository.""" stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"]) return report_output(stdout, "Tags list") -def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[str]: +def get_versions(ref: str, add: str | None) -> list[str]: """Generate the file containing the list of all GitHub Pages builds.""" # Get the directories (i.e. builds) from the GitHub Pages branch try: @@ -36,15 +35,12 @@ def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[st # Add and remove from the list of builds if add: builds.add(add) - if remove: - assert remove in builds, f"Build '{remove}' not in {sorted(builds)}" - builds.remove(remove) # Get a sorted list of tags tags = get_sorted_tags_list() # Make the sorted versions list from main branches and tags - versions: List[str] = [] + versions: list[str] = [] for version in ["master", "main"] + tags: if version in builds: versions.append(version) @@ -58,9 +54,12 @@ def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[st def write_json(path: Path, repository: str, versions: str): org, repo_name = repository.split("/") + pages_url = f"https://{org}.github.io" + if repo_name != f"{org}.github.io": + # Only add the repo name if it isn't the source for the org pages site + pages_url += f"/{repo_name}" struct = [ - {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"} - for version in versions + {"version": version, "url": f"{pages_url}/{version}/"} for version in versions ] text = json.dumps(struct, indent=2) print(f"JSON switcher:\n{text}") @@ -69,16 +68,12 @@ def write_json(path: Path, repository: str, versions: str): def main(args=None): parser = ArgumentParser( - description="Make a versions.txt file from gh-pages directories" + description="Make a versions.json file from gh-pages directories" ) parser.add_argument( "--add", help="Add this directory to the list of existing directories", ) - parser.add_argument( - "--remove", - help="Remove this directory from the list of existing directories", - ) parser.add_argument( "repository", help="The GitHub org and repository name: ORG/REPO", @@ -91,7 +86,7 @@ def main(args=None): args = parser.parse_args(args) # Write the versions file - versions = get_versions("origin/gh-pages", args.add, args.remove) + versions = get_versions("origin/gh-pages", args.add) write_json(args.output, args.repository, versions) diff --git a/.github/workflows/_check.yml b/.github/workflows/_check.yml new file mode 100644 index 00000000..a6139c19 --- /dev/null +++ b/.github/workflows/_check.yml @@ -0,0 +1,27 @@ +on: + workflow_call: + outputs: + branch-pr: + description: The PR number if the branch is in one + value: ${{ jobs.pr.outputs.branch-pr }} + +jobs: + pr: + runs-on: "ubuntu-latest" + outputs: + branch-pr: ${{ steps.script.outputs.result }} + steps: + - uses: actions/github-script@v7 + id: script + if: github.event_name == 'push' + with: + script: | + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: context.repo.owner + ':${{ github.ref_name }}' + }) + if (prs.data.length) { + console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`) + return prs.data[0]["number"] + } diff --git a/.github/workflows/_dist.yml b/.github/workflows/_dist.yml new file mode 100644 index 00000000..b1c4c93c --- /dev/null +++ b/.github/workflows/_dist.yml @@ -0,0 +1,36 @@ +on: + workflow_call: + +jobs: + build: + runs-on: "ubuntu-latest" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Build sdist and wheel + run: > + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && + pipx run build + + - name: Upload sdist and wheel as artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + - name: Check for packaging errors + run: pipx run twine check --strict dist/* + + - name: Install produced wheel + uses: ./.github/actions/install_requirements + with: + pip-install: dist/*.whl + + - name: Test module --version works using the installed wheel + # If more than one module in src/ replace with module name to test + run: python -m $(ls --hide='*.egg-info' src | head -1) --version diff --git a/.github/workflows/docs.yml b/.github/workflows/_docs.yml similarity index 74% rename from .github/workflows/docs.yml rename to .github/workflows/_docs.yml index 1d8a687a..40446e33 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/_docs.yml @@ -1,17 +1,13 @@ -name: Docs CI - on: - push: - pull_request: + workflow_call: jobs: - docs: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + build: runs-on: ubuntu-latest steps: - name: Avoid git conflicts when tag and branch pushed at same time - if: startsWith(github.ref, 'refs/tags') + if: github.ref_type == 'tag' run: sleep 60 - name: Checkout @@ -21,18 +17,23 @@ jobs: fetch-depth: 0 - name: Install system packages - # Can delete this if you don't use graphviz in your docs run: sudo apt-get install graphviz - name: Install python packages uses: ./.github/actions/install_requirements - with: - requirements_file: requirements-dev-3.x.txt - install_options: -e .[dev] - name: Build docs run: tox -e docs + - name: Remove environment.pickle + run: rm build/html/.doctrees/environment.pickle + + - name: Upload built docs artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: build + - name: Sanitize ref name for docs version run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV @@ -43,11 +44,11 @@ jobs: run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json - name: Publish Docs to gh-pages - if: github.event_name == 'push' && github.actor != 'dependabot[bot]' + if: github.ref_type == 'tag' || github.ref_name == 'main' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages - keep_files: true + keep_files: true \ No newline at end of file diff --git a/.github/workflows/_pypi.yml b/.github/workflows/_pypi.yml new file mode 100644 index 00000000..0c5258db --- /dev/null +++ b/.github/workflows/_pypi.yml @@ -0,0 +1,17 @@ +on: + workflow_call: + +jobs: + upload: + runs-on: ubuntu-latest + environment: release + + steps: + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Publish to PyPI using trusted publishing + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml new file mode 100644 index 00000000..e55efdb3 --- /dev/null +++ b/.github/workflows/_release.yml @@ -0,0 +1,32 @@ +on: + workflow_call: + +jobs: + artifacts: + runs-on: ubuntu-latest + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Zip up docs + run: | + set -vxeuo pipefail + if [ -d html ]; then + mv html $GITHUB_REF_NAME + zip -r docs.zip $GITHUB_REF_NAME + rm -rf $GITHUB_REF_NAME + fi + + - name: Create GitHub Release + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.4 + with: + prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} + files: "*" + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml new file mode 100644 index 00000000..f652d414 --- /dev/null +++ b/.github/workflows/_test.yml @@ -0,0 +1,62 @@ +on: + workflow_call: + inputs: + python-version: + type: string + description: The version of python to install + required: true + runs-on: + type: string + description: The runner to run this job on + required: true + secrets: + CODECOV_TOKEN: + required: true + +env: + # https://github.com/pytest-dev/pytest/issues/2042 + PY_IGNORE_IMPORTMISMATCH: "1" + +jobs: + run: + runs-on: ${{ inputs.runs-on }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - if: inputs.python-version == 'dev' + name: Install dev versions of python packages + uses: ./.github/actions/install_requirements + + - if: inputs.python-version == 'dev' + name: Write the requirements as an artifact + run: pip freeze --exclude-editable > /tmp/dev-requirements.txt + + - if: inputs.python-version == 'dev' + name: Upload dev-requirements.txt + uses: actions/upload-artifact@v4 + with: + name: dev-requirements + path: /tmp/dev-requirements.txt + + - if: inputs.python-version != 'dev' + name: Install latest versions of python packages + uses: ./.github/actions/install_requirements + with: + python-version: ${{ inputs.python-version }} + pip-install: ".[dev]" + + - name: Run tests + run: tox -e tests + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + name: ${{ inputs.python-version }}/${{ inputs.runs-on }} + files: cov.xml + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/_tox.yml b/.github/workflows/_tox.yml new file mode 100644 index 00000000..a13536d3 --- /dev/null +++ b/.github/workflows/_tox.yml @@ -0,0 +1,22 @@ +on: + workflow_call: + inputs: + tox: + type: string + description: What to run under tox + required: true + + +jobs: + run: + runs-on: "ubuntu-latest" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install python packages + uses: ./.github/actions/install_requirements + + - name: Run tox + run: tox -e ${{ inputs.tox }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..544f25b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + push: + pull_request: + +jobs: + check: + uses: ./.github/workflows/_check.yml + + lint: + needs: check + if: needs.check.outputs.branch-pr == '' + uses: ./.github/workflows/_tox.yml + with: + tox: pre-commit,type-checking + + test: + needs: check + if: needs.check.outputs.branch-pr == '' + strategy: + matrix: + runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest + python-version: ["3.10", "3.11"] + include: + # Include one that runs in the dev environment + - runs-on: "ubuntu-latest" + python-version: "dev" + fail-fast: false + uses: ./.github/workflows/_test.yml + with: + runs-on: ${{ matrix.runs-on }} + python-version: ${{ matrix.python-version }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + docs: + needs: check + if: needs.check.outputs.branch-pr == '' + uses: ./.github/workflows/_docs.yml + + dist: + needs: check + if: needs.check.outputs.branch-pr == '' + uses: ./.github/workflows/_dist.yml + + pypi: + if: github.ref_type == 'tag' + needs: dist + uses: ./.github/workflows/_pypi.yml + permissions: + id-token: write + + release: + if: github.ref_type == 'tag' + needs: [dist, docs] + uses: ./.github/workflows/_release.yml + permissions: + contents: write diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml deleted file mode 100644 index e6665503..00000000 --- a/.github/workflows/code.yml +++ /dev/null @@ -1,231 +0,0 @@ -name: Code CI - -on: - push: - pull_request: -env: - # The target python version, which must match the Dockerfile version - CONTAINER_PYTHON: "3.11" - -jobs: - lint: - # pull requests are a duplicate of a branch push if within the same repo. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install python packages - uses: ./.github/actions/install_requirements - with: - requirements_file: requirements-dev-3.x.txt - install_options: -e .[dev] - - - name: Lint - run: tox -e pre-commit,mypy - - test: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository - strategy: - fail-fast: false - matrix: - os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.10", "3.11"] - install: ["-e .[dev]"] - # Make one version be non-editable to test both paths of version code - include: - - os: "ubuntu-latest" - python: "3.10" - install: ".[dev]" - - runs-on: ${{ matrix.os }} - env: - # https://github.com/pytest-dev/pytest/issues/2042 - PY_IGNORE_IMPORTMISMATCH: "1" - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # Need this to get version number from last tag - fetch-depth: 0 - - - name: Install python packages - uses: ./.github/actions/install_requirements - with: - python_version: ${{ matrix.python }} - requirements_file: requirements-test-${{ matrix.os }}-${{ matrix.python }}.txt - install_options: ${{ matrix.install }} - - - name: List dependency tree - run: pipdeptree - - - name: Run tests - run: tox -e pytest - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - name: ${{ matrix.python }}/${{ matrix.os }} - files: cov.xml - - dist: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: "ubuntu-latest" - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # Need this to get version number from last tag - fetch-depth: 0 - - - name: Build sdist and wheel - run: | - export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ - pipx run build - - - name: Upload sdist and wheel as artifacts - uses: actions/upload-artifact@v3 - with: - name: dist - path: dist - - - name: Check for packaging errors - run: pipx run twine check --strict dist/* - - - name: Install python packages - uses: ./.github/actions/install_requirements - with: - python_version: ${{env.CONTAINER_PYTHON}} - requirements_file: requirements.txt - install_options: dist/*.whl - - - name: Test module --version works using the installed wheel - # If more than one module in src/ replace with module name to test - run: python -m $(ls src | head -1) --version - - container: - needs: [lint, dist, test] - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - env: - TEST_TAG: "testing" - - steps: - - name: Checkout - uses: actions/checkout@v4 - - # image names must be all lower case - - name: Generate image repo name - run: echo IMAGE_REPOSITORY=ghcr.io/$(tr '[:upper:]' '[:lower:]' <<< "${{ github.repository }}") >> $GITHUB_ENV - - - name: Download wheel and lockfiles - uses: actions/download-artifact@v3 - with: - path: artifacts/ - - - name: Log in to GitHub Docker Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and export to Docker local cache - uses: docker/build-push-action@v5 - with: - # Note build-args, context, file, and target must all match between this - # step and the later build-push-action, otherwise the second build-push-action - # will attempt to build the image again - build-args: | - PIP_OPTIONS=-r lockfiles/requirements.txt dist/*.whl - context: artifacts/ - file: ./Dockerfile - target: runtime - load: true - tags: ${{ env.TEST_TAG }} - # If you have a long docker build (2+ minutes), uncomment the - # following to turn on caching. For short build times this - # makes it a little slower - #cache-from: type=gha - #cache-to: type=gha,mode=max - - - name: Test cli works in cached runtime image - run: docker run docker.io/library/${{ env.TEST_TAG }} --version - - - name: Create tags for publishing image - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.IMAGE_REPOSITORY }} - tags: | - type=ref,event=tag - type=raw,value=latest, enable=${{ github.ref_type == 'tag' }} - # type=edge,branch=main - # Add line above to generate image for every commit to given branch, - # and uncomment the end of if clause in next step - - - name: Push cached image to container registry - if: github.ref_type == 'tag' # || github.ref_name == 'main' - uses: docker/build-push-action@v5 - # This does not build the image again, it will find the image in the - # Docker cache and publish it - with: - # Note build-args, context, file, and target must all match between this - # step and the previous build-push-action, otherwise this step will - # attempt to build the image again - build-args: | - PIP_OPTIONS=-r lockfiles/requirements.txt dist/*.whl - context: artifacts/ - file: ./Dockerfile - target: runtime - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - release: - # upload to PyPI and make a release on every tag - needs: [lint, dist, test] - if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} - runs-on: ubuntu-latest - permissions: - # this permission is mandatory for trusted publishing To PyPI - id-token: write - contents: write - # Specify the GitHub Environment to publish to - environment: release - - steps: - - uses: actions/download-artifact@v3 - - - name: Fixup blank lockfiles - # Github release artifacts can't be blank - run: for f in lockfiles/*; do [ -s $f ] || echo '# No requirements' >> $f; done - - - name: Github Release - # We pin to the SHA, not the tag, for security reasons. - # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 - with: - prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} - files: | - dist/* - lockfiles/* - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml deleted file mode 100644 index e324640e..00000000 --- a/.github/workflows/docs_clean.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Docs Cleanup CI - -# delete branch documentation when a branch is deleted -# also allow manually deleting a documentation version -on: - delete: - workflow_dispatch: - inputs: - version: - description: "documentation version to DELETE" - required: true - type: string - -jobs: - remove: - if: github.event.ref_type == 'branch' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: gh-pages - - - name: removing documentation for branch ${{ github.event.ref }} - if: ${{ github.event_name != 'workflow_dispatch' }} - run: echo "REF_NAME=${{ github.event.ref }}" >> $GITHUB_ENV - - - name: manually removing documentation version ${{ github.event.inputs.version }} - if: ${{ github.event_name == 'workflow_dispatch' }} - run: echo "REF_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - - name: Sanitize ref name for docs version - run: echo "DOCS_VERSION=${REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV - - - name: update index and push changes - run: | - rm -r $DOCS_VERSION - python make_switcher.py --remove $DOCS_VERSION ${{ github.repository }} switcher.json - git config --global user.name 'GitHub Actions Docs Cleanup CI' - git config --global user.email 'GithubActionsCleanup@noreply.github.com' - git commit -am "Removing redundant docs version $DOCS_VERSION" - git push diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml deleted file mode 100644 index d2a80410..00000000 --- a/.github/workflows/linkcheck.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Link Check - -on: - workflow_dispatch: - schedule: - # Run weekly to check URL links still resolve - - cron: "0 8 * * WED" - -jobs: - docs: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install python packages - uses: ./.github/actions/install_requirements - with: - requirements_file: requirements-dev-3.x.txt - install_options: -e .[dev] - - - name: Check links - run: tox -e docs build -- -b linkcheck - - - name: Keepalive Workflow - uses: gautamkrishnar/keepalive-workflow@v1 diff --git a/.github/workflows/periodic.yml b/.github/workflows/periodic.yml new file mode 100644 index 00000000..e2a0fd1b --- /dev/null +++ b/.github/workflows/periodic.yml @@ -0,0 +1,13 @@ +name: Periodic + +on: + workflow_dispatch: + schedule: + # Run weekly to check URL links still resolve + - cron: "0 8 * * WED" + +jobs: + linkcheck: + uses: ./.github/workflows/_tox.yml + with: + tox: docs build -- -b linkcheck diff --git a/.gitignore b/.gitignore index a8e45be5..ab6ebeeb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ __pycache__/ # Distribution / packaging .Python env/ -.venv build/ develop-eggs/ dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bc9f001..5a4cbf7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-yaml @@ -8,16 +8,16 @@ repos: - repo: local hooks: - - id: black - name: Run black - stages: [commit] + - id: ruff + name: lint with ruff language: system - entry: black --check --diff + entry: ruff check --force-exclude types: [python] + require_serial: true - - id: ruff - name: Run ruff - stages: [commit] + - id: ruff-format + name: format with ruff language: system - entry: ruff + entry: ruff format --force-exclude types: [python] + require_serial: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a1227b34..66ad6324 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,5 @@ { "recommendations": [ "ms-vscode-remote.remote-containers", - "ms-python.python", - "tamasfe.even-better-toml", - "redhat.vscode-yaml", - "ryanluker.vscode-coverage-gutters", - "charliermarsh.Ruff" ] -} +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 9cfb5fee..c328de06 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "configurations": [ { "name": "Debug Unit Test", - "type": "python", + "type": "debugpy", "request": "launch", "justMyCode": false, "program": "${file}", @@ -15,10 +15,7 @@ ], "console": "integratedTerminal", "env": { - // The default config in pyproject.toml's "[tool.pytest.ini_options]" adds coverage. - // Cannot have coverage and debugging at the same time. - // https://github.com/microsoft/vscode-python/issues/693 - "PYTEST_ADDOPTS": "--no-cov", + // Enable break on exception when debugging tests (see: tests/conftest.py) "PYTEST_RAISE": "1", }, }, @@ -31,7 +28,7 @@ "args": [ "convert", "device", - "${workspaceFolder:ADCore}/pvi", + "/epics/pvi-defs/", "${workspaceFolder:ADCore}/ADApp/ADSrc/asynNDArrayDriver.h", "--template", "${workspaceFolder:ADCore}/ADApp/Db/NDArrayBase.template", @@ -49,7 +46,7 @@ "args": [ "convert", "device", - "${workspaceFolder:ADCore}/pvi", + "/epics/pvi-defs/", "${workspaceFolder:ADCore}/ADApp/ADSrc/ADDriver.h", "--template", "${workspaceFolder:ADCore}/ADApp/Db/ADBase.template", @@ -65,7 +62,7 @@ "args": [ "convert", "device", - "${workspaceFolder:ADCore}/pvi", + "/epics/pvi-defs/", "${workspaceFolder:ADCore}/ADApp/pluginSrc/NDPluginDriver.h", "--template", "${workspaceFolder:ADCore}/ADApp/Db/NDPluginBase.template", @@ -127,7 +124,7 @@ "args": [ "convert", "device", - "${workspaceFolder:ADSimDetector}/pvi", + "/epics/pvi-defs/", "${workspaceFolder:ADSimDetector}/simDetectorApp/src/simDetector.h", "--template", "${workspaceFolder:ADSimDetector}/simDetectorApp/Db/simDetector.template", @@ -156,7 +153,7 @@ "program": "${workspaceFolder:pvi}/src/pvi", "args": [ "regroup", - "${workspaceFolder:ADCore}/pvi/asynNDArrayDriver.pvi.device.yaml", + "/epics/pvi-defs/asynNDArrayDriver.pvi.device.yaml", "${workspaceFolder:ADCore}/ADApp/op/adl/ADSetup.adl", "${workspaceFolder:ADCore}/ADApp/op/adl/ADReadout.adl", "${workspaceFolder:ADCore}/ADApp/op/adl/ADShutter.adl", @@ -176,7 +173,7 @@ "program": "${workspaceFolder:pvi}/src/pvi", "args": [ "regroup", - "${workspaceFolder:ADCore}/pvi/ADDriver.pvi.device.yaml", + "/epics/pvi-defs/ADDriver.pvi.device.yaml", "${workspaceFolder:ADCore}/ADApp/op/adl/ADSetup.adl", "${workspaceFolder:ADCore}/ADApp/op/adl/ADReadout.adl", "${workspaceFolder:ADCore}/ADApp/op/adl/ADShutter.adl", @@ -193,7 +190,7 @@ "program": "${workspaceFolder:pvi}/src/pvi", "args": [ "regroup", - "${workspaceFolder:ADSimDetector}/pvi/simDetector.pvi.device.yaml", + "/epics/pvi-defs/simDetector.pvi.device.yaml", "${workspaceFolder:ADSimDetector}/simDetectorApp/op/adl/simDetector.adl", "${workspaceFolder:ADSimDetector}/simDetectorApp/op/adl/simDetectorSetup.adl", ], @@ -207,7 +204,7 @@ "program": "${workspaceFolder:pvi}/src/pvi", "args": [ "regroup", - "${workspaceFolder:ADCore}/pvi/NDPluginDriver.pvi.device.yaml", + "/epics/pvi-defs/NDPluginDriver.pvi.device.yaml", "${workspaceFolder:ADCore}/ADApp/op/adl/NDPluginBase.adl", "${workspaceFolder:ADCore}/ADApp/op/adl/NDPluginBaseFull.adl", ], @@ -419,3 +416,4 @@ ], }, ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 97ed91e8..c129d991 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,11 @@ { - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": false, - "python.linting.mypyEnabled": true, - "python.linting.enabled": true, - "python.testing.pytestArgs": [ - "--cov=pvi", - "--cov-report", - "xml:cov.xml" - ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.languageServer": "Pylance", "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, "[python]": { - "editor.codeActionsOnSave": { - "source.fixAll.ruff": "explicit", - "source.organizeImports.ruff": "explicit" - } - } -} + "editor.defaultFormatter": "charliermarsh.ruff", + }, +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c999e864..946e69d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,4 +13,4 @@ "problemMatcher": [], } ] -} +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 027e24cf..c4404eca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,13 @@ -# This file is for use as a devcontainer and a runtime container -# -# The devcontainer should use the build target and run as root with podman +# The devcontainer should use the developer target and run as root with podman # or docker with user namespaces. -# -FROM python:3.11 as build +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION} as developer -ARG PIP_OPTIONS=. +# Add any system dependencies for the developer/build environment here +RUN apt-get update && apt-get install -y --no-install-recommends \ + graphviz \ + && rm -rf /var/lib/apt/lists/* -# Add any system dependencies for the developer/build environment here e.g. -# RUN apt-get update && apt-get upgrade -y && \ -# apt-get install -y --no-install-recommends \ -# desired-packages \ -# && rm -rf /var/lib/apt/lists/* - -# set up a virtual environment and put it in PATH +# Set up a virtual environment and put it in PATH RUN python -m venv /venv ENV PATH=/venv/bin:$PATH - -# Copy any required context for the pip install over -COPY . /context -WORKDIR /context - -# install python package into /venv -RUN pip install ${PIP_OPTIONS} - -FROM python:3.11-slim as runtime - -# Add apt-get system dependecies for runtime here if needed - -# copy the virtual environment from the build stage and put it in PATH -COPY --from=build /venv/ /venv/ -ENV PATH=/venv/bin:$PATH - -# change this entrypoint if it is not the same as the repo -ENTRYPOINT ["pvi"] -CMD ["--version"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..18017c2c --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +[![CI](https://github.com/epics-containers/pvi/actions/workflows/ci.yml/badge.svg)](https://github.com/epics-containers/pvi/actions/workflows/ci.yml) +[![Coverage](https://codecov.io/gh/epics-containers/pvi/branch/main/graph/badge.svg)](https://codecov.io/gh/epics-containers/pvi) +[![PyPI](https://img.shields.io/pypi/v/pvi.svg)](https://pypi.org/project/pvi) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +# PVI + +PVI (PV Interface) is a framework for specifying the interface to an EPICS +driver in a single YAML file. The initial target is asyn port driver based +drivers, but it could be extended to streamDevice and other driver types at a +later date. + +It allows the asyn parameter interface to be specified in a single place, +and removes boilerplate code in the driver CPP, template files, documentation, +and low level opis. + +Source | +:---: | :---: +PyPI | `pip install pvi` +Documentation | +Releases | + +--- + +Note: This module is currently a proposal only, so all details are subject to +change at any point. The documentation is written in the present tense, but only +prototype code is written. + +--- + + + +See https://epics-containers.github.io/pvi for more detailed documentation. diff --git a/README.rst b/README.rst deleted file mode 100644 index da170ccb..00000000 --- a/README.rst +++ /dev/null @@ -1,54 +0,0 @@ -PVI -=== - -|code_ci| |docs_ci| |coverage| |pypi_version| |license| - -PVI (PV Interface) is a framework for specifying the interface to an EPICS -driver in a single YAML file. The initial target is asyn port driver based -drivers, but it could be extended to streamDevice and other driver types at a -later date. - -It allows the asyn parameter interface to be specified in a single place, -and removes boilerplate code in the driver CPP, template files, documentation, -and low level opis. - -============== ============================================================== -PyPI ``pip install pvi`` -Source code https://github.com/epics-containers/pvi -Documentation https://epics-containers.github.io/pvi -Releases https://github.com/epics-containers/pvi/releases -============== ============================================================== - -**** - -Note: This module is currently a proposal only, so all details are subject to -change at any point. The documentation is written in the present tense, but only -prototype code is written. - -**** - -.. |code_ci| image:: https://github.com/epics-containers/pvi/actions/workflows/code.yml/badge.svg?branch=main - :target: https://github.com/epics-containers/pvi/actions/workflows/code.yml - :alt: Code CI - -.. |docs_ci| image:: https://github.com/epics-containers/pvi/actions/workflows/docs.yml/badge.svg?branch=main - :target: https://github.com/epics-containers/pvi/actions/workflows/docs.yml - :alt: Docs CI - -.. |coverage| image:: https://codecov.io/gh/epics-containers/pvi/branch/main/graph/badge.svg - :target: https://codecov.io/gh/epics-containers/pvi - :alt: Test Coverage - -.. |pypi_version| image:: https://img.shields.io/pypi/v/pvi.svg - :target: https://pypi.org/project/pvi - :alt: Latest PyPI version - -.. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg - :target: https://opensource.org/licenses/Apache-2.0 - :alt: Apache License - -.. - Anything below this line is used when viewing README.rst and will be replaced - when included in index.rst - -See https://epics-containers.github.io/pvi for more detailed documentation. diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 00000000..43505a06 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: pvi + title: pvi + description: EPICS PV Interface described in YAML +spec: + type: documentation + lifecycle: experimental + owner: mef65357 \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 75061190..f305b432 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -126,7 +126,7 @@ # a list of builtin themes. # html_theme = "pydata_sphinx_theme" -github_repo = project +github_repo = "pvi" github_user = "epics-containers" switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" switcher_exists = requests.get(switcher_json).ok diff --git a/docs/developer/explanations/decisions.rst b/docs/developer/explanations/decisions.rst deleted file mode 100644 index 5841e6ea..00000000 --- a/docs/developer/explanations/decisions.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. This Source Code Form is subject to the terms of the Mozilla Public -.. License, v. 2.0. If a copy of the MPL was not distributed with this -.. file, You can obtain one at http://mozilla.org/MPL/2.0/. - -Architectural Decision Records -============================== - -We record major architectural decisions in Architecture Decision Records (ADRs), -as `described by Michael Nygard -`_. -Below is the list of our current ADRs. - -.. toctree:: - :maxdepth: 1 - :glob: - - decisions/* \ No newline at end of file diff --git a/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst b/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst deleted file mode 100644 index b2d3d0fe..00000000 --- a/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst +++ /dev/null @@ -1,26 +0,0 @@ -1. Record architecture decisions -================================ - -Date: 2022-02-18 - -Status ------- - -Accepted - -Context -------- - -We need to record the architectural decisions made on this project. - -Decision --------- - -We will use Architecture Decision Records, as `described by Michael Nygard -`_. - -Consequences ------------- - -See Michael Nygard's article, linked above. To create new ADRs we will copy and -paste from existing ones. diff --git a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst deleted file mode 100644 index 41d90fd4..00000000 --- a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +++ /dev/null @@ -1,35 +0,0 @@ -2. Adopt python3-pip-skeleton for project structure -=================================================== - -Date: 2022-02-18 - -Status ------- - -Accepted - -Context -------- - -We should use the following `pip-skeleton `_. -The skeleton will ensure consistency in developer -environments and package management. - -Decision --------- - -We have switched to using the skeleton. - -Consequences ------------- - -This module will use a fixed set of tools as developed in python3-pip-skeleton -and can pull from this skeleton to update the packaging to the latest techniques. - -As such, the developer environment may have changed, the following could be -different: - -- linting -- formatting -- pip venv setup -- CI/CD diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst deleted file mode 100644 index 11a5e638..00000000 --- a/docs/developer/how-to/build-docs.rst +++ /dev/null @@ -1,38 +0,0 @@ -Build the docs using sphinx -=========================== - -You can build the `sphinx`_ based docs from the project directory by running:: - - $ tox -e docs - -This will build the static docs on the ``docs`` directory, which includes API -docs that pull in docstrings from the code. - -.. seealso:: - - `documentation_standards` - -The docs will be built into the ``build/html`` directory, and can be opened -locally with a web browser:: - - $ firefox build/html/index.html - -Autobuild ---------- - -You can also run an autobuild process, which will watch your ``docs`` -directory for changes and rebuild whenever it sees changes, reloading any -browsers watching the pages:: - - $ tox -e docs autobuild - -You can view the pages at localhost:: - - $ firefox http://localhost:8000 - -If you are making changes to source code too, you can tell it to watch -changes in this directory too:: - - $ tox -e docs autobuild -- --watch src - -.. _sphinx: https://www.sphinx-doc.org/ diff --git a/docs/developer/how-to/contribute.rst b/docs/developer/how-to/contribute.rst deleted file mode 100644 index 65b992f0..00000000 --- a/docs/developer/how-to/contribute.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../../.github/CONTRIBUTING.rst diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst deleted file mode 100644 index 2df258d8..00000000 --- a/docs/developer/how-to/lint.rst +++ /dev/null @@ -1,39 +0,0 @@ -Run linting using pre-commit -============================ - -Code linting is handled by black_ and ruff_ run under pre-commit_. - -Running pre-commit ------------------- - -You can run the above checks on all files with this command:: - - $ tox -e pre-commit - -Or you can install a pre-commit hook that will run each time you do a ``git -commit`` on just the files that have changed:: - - $ pre-commit install - -It is also possible to `automatically enable pre-commit on cloned repositories `_. -This will result in pre-commits being enabled on every repo your user clones from now on. - -Fixing issues -------------- - -If black reports an issue you can tell it to reformat all the files in the -repository:: - - $ black . - -Likewise with ruff:: - - $ ruff --fix . - -Ruff may not be able to automatically fix all issues; in this case, you will have to fix those manually. - -VSCode support --------------- - -The ``.vscode/settings.json`` will run black formatting as well as -ruff checking on save. Issues will be highlighted in the editor window. diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst deleted file mode 100644 index a7d1688d..00000000 --- a/docs/developer/how-to/make-release.rst +++ /dev/null @@ -1,16 +0,0 @@ -Make a release -============== - -To make a new release, please follow this checklist: - -- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) -- Go to the GitHub release_ page -- Choose ``Draft New Release`` -- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) -- Click ``Generate release notes``, review and edit these notes -- Choose a title and click ``Publish Release`` - -Note that tagging and pushing to the main branch has the same effect except that -you will not get the option to edit the release notes. - -.. _release: https://github.com/epics-containers/pvi/releases diff --git a/docs/developer/how-to/pin-requirements.rst b/docs/developer/how-to/pin-requirements.rst deleted file mode 100644 index 91ed5535..00000000 --- a/docs/developer/how-to/pin-requirements.rst +++ /dev/null @@ -1,74 +0,0 @@ -Pinning Requirements -==================== - -Introduction ------------- - -By design this project only defines dependencies in one place, i.e. in -the ``requires`` table in ``pyproject.toml``. - -In the ``requires`` table it is possible to pin versions of some dependencies -as needed. For library projects it is best to leave pinning to a minimum so -that your library can be used by the widest range of applications. - -When CI builds the project it will use the latest compatible set of -dependencies available (after applying your pins and any dependencies' pins). - -This approach means that there is a possibility that a future build may -break because an updated release of a dependency has made a breaking change. - -The correct way to fix such an issue is to work out the minimum pinning in -``requires`` that will resolve the problem. However this can be quite hard to -do and may be time consuming when simply trying to release a minor update. - -For this reason we provide a mechanism for locking all dependencies to -the same version as a previous successful release. This is a quick fix that -should guarantee a successful CI build. - -Finding the lock files ----------------------- - -Every release of the project will have a set of requirements files published -as release assets. - -For example take a look at the release page for python3-pip-skeleton-cli here: -https://github.com/DiamondLightSource/python3-pip-skeleton-cli/releases/tag/3.3.0 - -There is a list of requirements*.txt files showing as assets on the release. - -There is one file for each time the CI installed the project into a virtual -environment. There are multiple of these as the CI creates a number of -different environments. - -The files are created using ``pip freeze`` and will contain a full list -of the dependencies and sub-dependencies with pinned versions. - -You can download any of these files by clicking on them. It is best to use -the one that ran with the lowest Python version as this is more likely to -be compatible with all the versions of Python in the test matrix. -i.e. ``requirements-test-ubuntu-latest-3.10.txt`` in this example. - -Applying the lock file ----------------------- - -To apply a lockfile: - -- copy the requirements file you have downloaded to the root of your - repository -- rename it to requirements.txt -- commit it into the repo -- push the changes - -The CI looks for a requirements.txt in the root and will pass it to pip -when installing each of the test environments. pip will then install exactly -the same set of packages as the previous release. - -Removing dependency locking from CI ------------------------------------ - -Once the reasons for locking the build have been resolved it is a good idea -to go back to an unlocked build. This is because you get an early indication -of any incoming problems. - -To restore unlocked builds in CI simply remove requirements.txt from the root -of the repo and push. diff --git a/docs/developer/how-to/run-tests.rst b/docs/developer/how-to/run-tests.rst deleted file mode 100644 index d2e03644..00000000 --- a/docs/developer/how-to/run-tests.rst +++ /dev/null @@ -1,12 +0,0 @@ -Run the tests using pytest -========================== - -Testing is done with pytest_. It will find functions in the project that `look -like tests`_, and run them to check for errors. You can run it with:: - - $ tox -e pytest - -It will also report coverage to the commandline and to ``cov.xml``. - -.. _pytest: https://pytest.org/ -.. _look like tests: https://docs.pytest.org/explanation/goodpractices.html#test-discovery diff --git a/docs/developer/how-to/static-analysis.rst b/docs/developer/how-to/static-analysis.rst deleted file mode 100644 index 065920e1..00000000 --- a/docs/developer/how-to/static-analysis.rst +++ /dev/null @@ -1,8 +0,0 @@ -Run static analysis using mypy -============================== - -Static type analysis is done with mypy_. It checks type definition in source -files without running them, and highlights potential issues where types do not -match. You can run it with:: - - $ tox -e mypy diff --git a/docs/developer/how-to/test-container.rst b/docs/developer/how-to/test-container.rst deleted file mode 100644 index a4a43a6f..00000000 --- a/docs/developer/how-to/test-container.rst +++ /dev/null @@ -1,25 +0,0 @@ -Container Local Build and Test -============================== - -CI builds a runtime container for the project. The local tests -checks available via ``tox -p`` do not verify this because not -all developers will have docker installed locally. - -If CI is failing to build the container, then it is best to fix and -test the problem locally. This would require that you have docker -or podman installed on your local workstation. - -In the following examples the command ``docker`` is interchangeable with -``podman`` depending on which container cli you have installed. - -To build the container and call it ``test``:: - - cd - docker build -t test . - -To verify that the container runs:: - - docker run -it test --help - -You can pass any other command line parameters to your application -instead of --help. diff --git a/docs/developer/how-to/update-tools.rst b/docs/developer/how-to/update-tools.rst deleted file mode 100644 index c1075ee8..00000000 --- a/docs/developer/how-to/update-tools.rst +++ /dev/null @@ -1,16 +0,0 @@ -Update the tools -================ - -This module is merged with the python3-pip-skeleton_. This is a generic -Python project structure which provides a means to keep tools and -techniques in sync between multiple Python projects. To update to the -latest version of the skeleton, run:: - - $ git pull --rebase=false https://github.com/DiamondLightSource/python3-pip-skeleton - -Any merge conflicts will indicate an area where something has changed that -conflicts with the setup of the current module. Check the `closed pull requests -`_ -of the skeleton module for more details. - -.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/docs/developer/index.rst b/docs/developer/index.rst deleted file mode 100644 index af52a292..00000000 --- a/docs/developer/index.rst +++ /dev/null @@ -1,66 +0,0 @@ -Developer Guide -=============== - -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. grid:: 2 - :gutter: 4 - - .. grid-item-card:: :material-regular:`directions_run;3em` - - .. toctree:: - :caption: Tutorials - :maxdepth: 1 - - tutorials/dev-install - - +++ - - Tutorials for getting up and running as a developer. - - .. grid-item-card:: :material-regular:`task;3em` - - .. toctree:: - :caption: How-to Guides - :maxdepth: 1 - - how-to/contribute - how-to/build-docs - how-to/run-tests - how-to/static-analysis - how-to/lint - how-to/update-tools - how-to/make-release - how-to/pin-requirements - how-to/test-container - how-to/write-a-formatter - - +++ - - Practical step-by-step guides for day-to-day dev tasks. - - .. grid-item-card:: :material-regular:`apartment;3em` - - .. toctree:: - :caption: Explanations - :maxdepth: 1 - - explanations/decisions - explanations/original-design - - +++ - - Explanations of how and why the architecture is why it is. - - .. grid-item-card:: :material-regular:`description;3em` - - .. toctree:: - :caption: Reference - :maxdepth: 1 - - reference/standards - - +++ - - Technical reference material on standards in use. diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst deleted file mode 100644 index 5a1fd478..00000000 --- a/docs/developer/reference/standards.rst +++ /dev/null @@ -1,63 +0,0 @@ -Standards -========= - -This document defines the code and documentation standards used in this -repository. - -Code Standards --------------- - -The code in this repository conforms to standards set by the following tools: - -- black_ for code formatting -- ruff_ for style checks -- mypy_ for static type checking - -.. seealso:: - - How-to guides `../how-to/lint` and `../how-to/static-analysis` - -.. _documentation_standards: - -Documentation Standards ------------------------ - -Docstrings are pre-processed using the Sphinx Napoleon extension. As such, -google-style_ is considered as standard for this repository. Please use type -hints in the function signature for types. For example: - -.. code:: python - - def func(arg1: str, arg2: int) -> bool: - """Summary line. - - Extended description of function. - - Args: - arg1: Description of arg1 - arg2: Description of arg2 - - Returns: - Description of return value - """ - return True - -.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy - -Documentation is contained in the ``docs`` directory and extracted from -docstrings of the API. - -Docs follow the underlining convention:: - - Headling 1 (page title) - ======================= - - Heading 2 - --------- - - Heading 3 - ~~~~~~~~~ - -.. seealso:: - - How-to guide `../how-to/build-docs` diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst deleted file mode 100644 index 06c80b5e..00000000 --- a/docs/developer/tutorials/dev-install.rst +++ /dev/null @@ -1,68 +0,0 @@ -Developer install -================= - -These instructions will take you through the minimal steps required to get a dev -environment setup, so you can run the tests locally. - -Clone the repository --------------------- - -First clone the repository locally using `Git -`_:: - - $ git clone git://github.com/epics-containers/pvi.git - -Install dependencies --------------------- - -You can choose to either develop on the host machine using a `venv` (which -requires python 3.10 or later) or to run in a container under `VSCode -`_ - -.. tab-set:: - - .. tab-item:: Local virtualenv - - .. code:: - - $ cd pvi - $ python3 -m venv venv - $ source venv/bin/activate - $ pip install -e '.[dev]' - - .. tab-item:: VSCode devcontainer - - .. code:: - - $ code pvi - # Click on 'Reopen in Container' when prompted - # Open a new terminal - - .. note:: - - See the epics-containers_ documentation for more complex - use cases, such as integration with podman. - -See what was installed ----------------------- - -To see a graph of the python package dependency tree type:: - - $ pipdeptree - -Build and test --------------- - -Now you have a development environment you can run the tests in a terminal:: - - $ tox -p - -This will run in parallel the following checks: - -- `../how-to/build-docs` -- `../how-to/run-tests` -- `../how-to/static-analysis` -- `../how-to/lint` - - -.. _epics-containers: https://epics-containers.github.io/main/user/tutorials/devcontainer.html diff --git a/docs/explanations.md b/docs/explanations.md new file mode 100644 index 00000000..73ab289b --- /dev/null +++ b/docs/explanations.md @@ -0,0 +1,10 @@ +# Explanations + +Explanations of how it works and why it works that way. + +```{toctree} +:maxdepth: 1 +:glob: + +explanations/* +``` diff --git a/docs/explanations/decisions.md b/docs/explanations/decisions.md new file mode 100644 index 00000000..0533b98d --- /dev/null +++ b/docs/explanations/decisions.md @@ -0,0 +1,12 @@ +# Architectural Decision Records + +Architectural decisions are made throughout a project's lifetime. As a way of keeping track of these decisions, we record these decisions in Architecture Decision Records (ADRs) listed below. + +```{toctree} +:glob: true +:maxdepth: 1 + +decisions/* +``` + +For more information on ADRs see this [blog by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.md b/docs/explanations/decisions/0001-record-architecture-decisions.md new file mode 100644 index 00000000..44d234ef --- /dev/null +++ b/docs/explanations/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,18 @@ +# 1. Record architecture decisions + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. To create new ADRs we will copy and +paste from existing ones. diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md new file mode 100644 index 00000000..66fe5d8b --- /dev/null +++ b/docs/explanations/decisions/0002-switched-to-python-copier-template.md @@ -0,0 +1,28 @@ +# 2. Adopt python-copier-template for project structure + +## Status + +Accepted + +## Context + +We should use the following [python-copier-template](https://github.com/DiamondLightSource/python-copier-template). +The template will ensure consistency in developer +environments and package management. + +## Decision + +We have switched to using the template. + +## Consequences + +This module will use a fixed set of tools as developed in `python-copier-template` +and can pull from this template to update the packaging to the latest techniques. + +As such, the developer environment may have changed, the following could be +different: + +- linting +- formatting +- pip venv setup +- CI/CD diff --git a/docs/explanations/decisions/COPYME b/docs/explanations/decisions/COPYME new file mode 100644 index 00000000..b466c792 --- /dev/null +++ b/docs/explanations/decisions/COPYME @@ -0,0 +1,19 @@ +# 3. Short descriptive title + +Date: Today's date + +## Status + +Accepted + +## Context + +Background to allow us to make the decision, to show how we arrived at our conclusions. + +## Decision + +What decision we made. + +## Consequences + +What we will do as a result of this decision. diff --git a/docs/genindex.md b/docs/genindex.md new file mode 100644 index 00000000..73f1191b --- /dev/null +++ b/docs/genindex.md @@ -0,0 +1,3 @@ +# Index + + diff --git a/docs/genindex.rst b/docs/genindex.rst deleted file mode 100644 index 93eb8b29..00000000 --- a/docs/genindex.rst +++ /dev/null @@ -1,5 +0,0 @@ -API Index -========= - -.. - https://stackoverflow.com/a/42310803 diff --git a/docs/how-to.md b/docs/how-to.md new file mode 100644 index 00000000..6b161417 --- /dev/null +++ b/docs/how-to.md @@ -0,0 +1,10 @@ +# How-to Guides + +Practical step-by-step guides for the more experienced user. + +```{toctree} +:maxdepth: 1 +:glob: + +how-to/* +``` diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md new file mode 100644 index 00000000..f9c4ca1d --- /dev/null +++ b/docs/how-to/contribute.md @@ -0,0 +1,2 @@ +```{include} ../../.github/CONTRIBUTING.md +``` \ No newline at end of file diff --git a/docs/images/dls-favicon.ico b/docs/images/dls-favicon.ico deleted file mode 100644 index 9a11f508ef8aed28f14c5ce0d8408e1ec8b614a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 ZYRRh@^bCDh5vs@!z69bV>$COq{{Z);QUw42 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..730b3fdc --- /dev/null +++ b/docs/index.md @@ -0,0 +1,56 @@ +--- +html_theme.sidebar_secondary.remove: true +--- + +```{include} ../README.md +:end-before: + +::::{grid} 2 +:gutter: 4 + +:::{grid-item-card} {material-regular}`directions_walk;2em` +```{toctree} +:maxdepth: 2 +tutorials +``` ++++ +Tutorials for installation and typical usage. New users start here. +::: + +:::{grid-item-card} {material-regular}`directions;2em` +```{toctree} +:maxdepth: 2 +how-to +``` ++++ +Practical step-by-step guides for the more experienced user. +::: + +:::{grid-item-card} {material-regular}`info;2em` +```{toctree} +:maxdepth: 2 +explanations +``` ++++ +Explanations of how it works and why it works that way. +::: + +:::{grid-item-card} {material-regular}`menu_book;2em` +```{toctree} +:maxdepth: 2 +reference +``` ++++ +Technical reference material including APIs and release notes. +::: + +:::: diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index daa9c73c..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,29 +0,0 @@ -:html_theme.sidebar_secondary.remove: - -.. include:: ../README.rst - :end-before: when included in index.rst - -How the documentation is structured ------------------------------------ - -The documentation is split into 2 sections: - -.. grid:: 2 - - .. grid-item-card:: :material-regular:`person;4em` - :link: user/index - :link-type: doc - - The User Guide contains documentation on how to install and use pvi. - - .. grid-item-card:: :material-regular:`code;4em` - :link: developer/index - :link-type: doc - - The Developer Guide contains documentation on how to develop and contribute changes back to pvi. - -.. toctree:: - :hidden: - - user/index - developer/index diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 00000000..feb18eb2 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,12 @@ +# Reference + +Technical reference material including APIs and release notes. + +```{toctree} +:maxdepth: 1 +:glob: + +reference/* +genindex +Release Notes +``` diff --git a/docs/user/reference/api.rst b/docs/reference/api.md similarity index 66% rename from docs/user/reference/api.rst rename to docs/reference/api.md index 3c8f7275..45658678 100644 --- a/docs/user/reference/api.rst +++ b/docs/reference/api.md @@ -1,14 +1,17 @@ -API -=== +# API +```{eval-rst} .. automodule:: pvi ``pvi`` - ----------------------------------- + ------- +``` This is the internal API reference for pvi -.. data:: pvi.__version__ :noindex: +```{eval-rst} +.. data:: pvi.__version__ :type: str Version number as calculated by https://github.com/pypa/setuptools_scm +``` diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 00000000..1fe66c54 --- /dev/null +++ b/docs/tutorials.md @@ -0,0 +1,10 @@ +# Tutorials + +Tutorials for installation and typical usage. New users start here. + +```{toctree} +:maxdepth: 1 +:glob: + +tutorials/* +``` diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md new file mode 100644 index 00000000..3951e8b8 --- /dev/null +++ b/docs/tutorials/installation.md @@ -0,0 +1,42 @@ +# Installation + +## Check your version of python + +You will need python 3.10 or later. You can check your version of python by +typing into a terminal: + +``` +$ python3 --version +``` + +## Create a virtual environment + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software: + +``` +$ python3 -m venv /path/to/venv +$ source /path/to/venv/bin/activate +``` + +## Installing the library + +You can now use `pip` to install the library and its dependencies: + +``` +$ python3 -m pip install pvi +``` + +If you require a feature that is not currently released you can also install +from github: + +``` +$ python3 -m pip install git+https://github.com/epics-containers/pvi.git +``` + +The library should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing: + +``` +$ pvi --version +``` diff --git a/docs/user/explanations/docs-structure.rst b/docs/user/explanations/docs-structure.rst deleted file mode 100644 index f25a09ba..00000000 --- a/docs/user/explanations/docs-structure.rst +++ /dev/null @@ -1,18 +0,0 @@ -About the documentation ------------------------ - - :material-regular:`format_quote;2em` - - The Grand Unified Theory of Documentation - - -- David Laing - -There is a secret that needs to be understood in order to write good software -documentation: there isn't one thing called *documentation*, there are four. - -They are: *tutorials*, *how-to guides*, *technical reference* and *explanation*. -They represent four different purposes or functions, and require four different -approaches to their creation. Understanding the implications of this will help -improve most documentation - often immensely. - -`More information on this topic. `_ diff --git a/docs/user/how-to/run-container.rst b/docs/user/how-to/run-container.rst deleted file mode 100644 index 53dbc8b6..00000000 --- a/docs/user/how-to/run-container.rst +++ /dev/null @@ -1,15 +0,0 @@ -Run in a container -================== - -Pre-built containers with pvi and its dependencies already -installed are available on `Github Container Registry -`_. - -Starting the container ----------------------- - -To pull the container from github container registry and run:: - - $ docker run ghcr.io/epics-containers/pvi:main --version - -To get a released version, use a numbered release instead of ``main``. diff --git a/docs/user/index.rst b/docs/user/index.rst deleted file mode 100644 index 2c94a0c0..00000000 --- a/docs/user/index.rst +++ /dev/null @@ -1,57 +0,0 @@ -User Guide -========== - -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. grid:: 2 - :gutter: 4 - - .. grid-item-card:: :material-regular:`directions_walk;3em` - - .. toctree:: - :caption: Tutorials - :maxdepth: 1 - - tutorials/installation - - +++ - - Tutorials for installation and typical usage. New users start here. - - .. grid-item-card:: :material-regular:`directions;3em` - - .. toctree:: - :caption: How-to Guides - :maxdepth: 1 - - how-to/run-container - - +++ - - Practical step-by-step guides for the more experienced user. - - .. grid-item-card:: :material-regular:`info;3em` - - .. toctree:: - :caption: Explanations - :maxdepth: 1 - - explanations/docs-structure - - +++ - - Explanations of how the library works and why it works that way. - - .. grid-item-card:: :material-regular:`menu_book;3em` - - .. toctree:: - :caption: Reference - :maxdepth: 1 - - reference/api - ../genindex - - +++ - - Technical reference material including APIs and release notes. diff --git a/docs/user/tutorials/installation.rst b/docs/user/tutorials/installation.rst deleted file mode 100644 index fcf3c3b9..00000000 --- a/docs/user/tutorials/installation.rst +++ /dev/null @@ -1,38 +0,0 @@ -Installation -============ - -Check your version of python ----------------------------- - -You will need python 3.10 or later. You can check your version of python by -typing into a terminal:: - - $ python3 --version - - -Create a virtual environment ----------------------------- - -It is recommended that you install into a “virtual environment” so this -installation will not interfere with any existing Python software:: - - $ python3 -m venv /path/to/venv - $ source /path/to/venv/bin/activate - - -Installing the library ----------------------- - -You can now use ``pip`` to install the library and its dependencies:: - - $ python3 -m pip install pvi - -If you require a feature that is not currently released you can also install -from github:: - - $ python3 -m pip install git+https://github.com/epics-containers/pvi.git - -The library should now be installed and the commandline interface on your path. -You can check the version that has been installed by typing:: - - $ pvi --version diff --git a/pyproject.toml b/pyproject.toml index 93143938..316c88f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] +requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [project] @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] -description = "One line description of your module" +description = "EPICS PV Interface described in YAML" dependencies = [ "pydantic>=2.7", "lxml", @@ -24,13 +24,14 @@ dependencies = [ ] dynamic = ["version"] license.file = "LICENSE" -readme = "README.rst" +readme = "README.md" requires-python = ">=3.10" [project.optional-dependencies] dev = [ - "black", + "copier", "mypy", + "myst-parser", "pipdeptree", "pre-commit", "pydata-sphinx-theme>=0.12", @@ -69,7 +70,7 @@ addopts = """ --tb=native -vv --doctest-modules --doctest-glob="*.rst" """ # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings -filterwarnings = ["error", "ignore::pytest_cov.plugin.CovDisabledWarning"] +filterwarnings = "error" # Doctest python code in docs, python code in src docstrings, test functions in tests testpaths = "docs src tests" @@ -87,7 +88,7 @@ legacy_tox_ini = """ [tox] skipsdist=True -[testenv:{pre-commit,mypy,pytest,docs}] +[testenv:{pre-commit,type-checking,tests,docs}] # Don't create a virtualenv for the command, requires tox-direct plugin direct = True passenv = * @@ -98,20 +99,21 @@ allowlist_externals = sphinx-build sphinx-autobuild commands = - pytest: pytest --cov=pvi --cov-report term --cov-report xml:cov.xml {posargs} - mypy: mypy src tests {posargs} - pre-commit: pre-commit run --all-files {posargs} + pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs} + type-checking: mypy src tests {posargs} + tests: pytest --cov=pvi --cov-report term --cov-report xml:cov.xml {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html """ - [tool.ruff] src = ["src", "tests"] line-length = 88 -select = [ - "C4", # flake8-comprehensions - https://beta.ruff.rs/docs/rules/#flake8-comprehensions-c4 - "E", # pycodestyle errors - https://beta.ruff.rs/docs/rules/#error-e - "F", # pyflakes rules - https://beta.ruff.rs/docs/rules/#pyflakes-f - "W", # pycodestyle warnings - https://beta.ruff.rs/docs/rules/#warning-w - "I001", # isort +lint.select = [ + "B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "C4", # flake8-comprehensions - https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e + "F", # pyflakes rules - https://docs.astral.sh/ruff/rules/#pyflakes-f + "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w + "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i + "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up ] diff --git a/tests/conftest.py b/tests/conftest.py index bbea28ba..cda99bc7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,9 @@ from typer import Typer from typer.testing import CliRunner -# See: https://github.com/pytest-dev/pytest/issues/7409 -if os.getenv("PYTEST_RAISE", "0") != "0": +# Prevent pytest from catching exceptions when debugging in vscode so that break on +# exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409) +if os.getenv("PYTEST_RAISE", "0") == "1": @pytest.hookimpl(tryfirst=True) def pytest_exception_interact(call): From ca2574fc6e6d23ce51d799ae7015ab3d630d88c6 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 5 Jul 2024 12:20:10 +0000 Subject: [PATCH 2/3] Convert docs to MyST --- docs/conf.py | 17 +- ...0002-switched-to-python-copier-template.md | 2 +- .../original-design.md} | 413 ++++++++---------- .../write-a-formatter.md} | 120 ++--- 4 files changed, 266 insertions(+), 286 deletions(-) rename docs/{developer/explanations/original-design.rst => explanations/original-design.md} (60%) rename docs/{developer/how-to/write-a-formatter.rst => how-to/write-a-formatter.md} (72%) diff --git a/docs/conf.py b/docs/conf.py index f305b432..0d72a592 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,8 +44,13 @@ "sphinx_copybutton", # For the card element "sphinx_design", + # So we can write markdown files + "myst_parser", ] +# So we can use the ::: syntax +myst_enable_extensions = ["colon_fence"] + # If true, Sphinx will warn about all references where the target cannot # be found. nitpicky = True @@ -82,9 +87,6 @@ # role, that is, for text marked up `like this` default_role = "any" -# The suffix of source filenames. -source_suffix = ".rst" - # The master toctree document. master_doc = "index" @@ -103,15 +105,6 @@ # A dictionary of graphviz graph attributes for inheritance diagrams. inheritance_graph_attrs = {"rankdir": "TB"} -# Common links that should be available on every page -rst_epilog = """ -.. _Diamond Light Source: http://www.diamond.ac.uk -.. _black: https://github.com/psf/black -.. _ruff: https://beta.ruff.rs/docs/ -.. _mypy: http://mypy-lang.org/ -.. _pre-commit: https://pre-commit.com/ -""" - # Ignore localhost links for periodic check that links in docs are valid linkcheck_ignore = [r"http://localhost:\d+/"] diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md index 66fe5d8b..d3d234bb 100644 --- a/docs/explanations/decisions/0002-switched-to-python-copier-template.md +++ b/docs/explanations/decisions/0002-switched-to-python-copier-template.md @@ -16,7 +16,7 @@ We have switched to using the template. ## Consequences -This module will use a fixed set of tools as developed in `python-copier-template` +This module will use a fixed set of tools as developed in ``python-copier-template`` and can pull from this template to update the packaging to the latest techniques. As such, the developer environment may have changed, the following could be diff --git a/docs/developer/explanations/original-design.rst b/docs/explanations/original-design.md similarity index 60% rename from docs/developer/explanations/original-design.rst rename to docs/explanations/original-design.md index 9f94b4fd..36c8f520 100644 --- a/docs/developer/explanations/original-design.rst +++ b/docs/explanations/original-design.md @@ -1,12 +1,13 @@ -Original Design -=============== +# Original Design -.. note:: - This page was the initial plan for what PVI would do. The produce features have now - been removed and only direct conversion to a Device representation and then - formatting of UIs is supported now. This page is kept now to record the design in - case full integration into areaDetector is revisited in the future. +:::{note} +This page was the initial plan for what PVI would do. The produce features have now +been removed and only direct conversion to a Device representation and then +formatting of UIs is supported now. This page is kept now to record the design in +case full integration into areaDetector is revisited in the future. +::: +```{eval-rst} .. digraph:: pvi_flowchart bgcolor=transparent @@ -37,7 +38,9 @@ Original Design "pilatus_parameters.adl" -> "pilatus.adl" [label="linked from"] "pilatus_parameters.edl" -> "pilatus.edl" [label="linked from"] "pilatus_parameters.opi" -> "pilatus.opi" [label="linked from"] +``` +```{eval-rst} .. list-table:: Aims of PVI :widths: 20, 80 :header-rows: 1 @@ -61,9 +64,9 @@ Original Design type, pv and widget), and lets the site specific template generate the screen according to local styles +``` -How it works ------------- +## How it works The YAML file contains information about each asyn parameter that will be exposed by the driver, it's name, type, description, initial value, which record @@ -73,11 +76,11 @@ Channel and AsynParam objects. These are passed to a site specific Formatter whi takes the tree of intermediate objects and writes a parameter CPP file, database template, and site specific screens to disk. -YAML file -~~~~~~~~~ +### YAML file The YAML file is formed of a number of sections: +```{eval-rst} .. list-table:: :widths: 20, 80 :header-rows: 1 @@ -95,12 +98,14 @@ The YAML file is formed of a number of sections: * - components - Tree of Components for each logical asyn parameter arranged in logical GUI groups +``` The Components are created from the YAML file with local overrides (also incorporating the base classes for screens). These are passed to the Producer which produces AsynParameters, Records and Channels. These are then passed to the Formatter which outputs them to file: +```{eval-rst} .. digraph:: pvi_products bgcolor=transparent @@ -111,16 +116,16 @@ outputs them to file: Products [label="Template\nScreens\nDriver Params\nDocumentation"] {rank=same; Components -> Producer -> Intermediate -> Formatter -> Products} +``` Here's a cut down pilatus.yaml file that might describe a parameter in a detector: -.. literalinclude:: ../../snippets/pilatusDetector.pvi.producer.yaml - :language: yaml +```{literalinclude} ../snippets/pilatusDetector.pvi.producer.yaml +:language: yaml +``` - -Screen files -~~~~~~~~~~~~ +### Screen files The intermediate objects are a number of Channel instances. These contain basic types (like Combo, TextInput, TextUpdate, LED, Group) and some creation hints @@ -136,26 +141,25 @@ little screens with one group per screen. Styling is also covered, so the blue/grey MEDM screens and green/grey EDM screens can be customized to fit the site style guide. -HTML Documentation -~~~~~~~~~~~~~~~~~~ +### HTML Documentation The Parameter and record sections of the existing documentation could be reproduced, in tabular form as a csv file that can be included in rst docs: +```{eval-rst} .. csv-table:: Pilatus Parameters - :file: ../../snippets/pilatusParameters.csv + :file: ../snippets/pilatusParameters.csv :widths: 15, 10, 8, 25, 25, 10, 60 :header-rows: 1 +``` -Questions ---------- +## Questions I am fairly happy with the scheme set out above, but there are a lot of implementation questions. Here are the most pressing: -One-time generation and checked into source control or generated by Makefile? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +### One-time generation and checked into source control or generated by Makefile? The process would probably be: @@ -166,63 +170,49 @@ The process would probably be: ADGenICam would be supported by building a GenICamProducer which took no components, just a path to a GenICam XML file - -Which screen tools to support? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +### Which screen tools to support? I suggest creating adl and edl files initially, following the example of makeAdl.py in ADGenICam, then expanding to support opi, bob and ui files natively. This would avoid needing screen converters installed - -.. _YAML: - https://en.wikipedia.org/wiki/YAML - - -Drivers -======== +# Drivers The generated header file contains the string parameters, and defines the parameters to make the interface. In this example we have a header file pilatusDetectorParamSet.h: -.. literalinclude:: ../../snippets/pilatusDetectorParamSet.h - :language: cpp - +```{literalinclude} ../snippets/pilatusDetectorParamSet.h +:language: cpp +``` The existing pilatus.cpp is then modified to remove these parameters definitions and use the param set API. - -Database Template File ----------------------- +## Database Template File According to the demand and readback properties of the component, the following records are created: -.. literalinclude:: ../../snippets/pilatusDetectorParameters.template - +```{literalinclude} ../snippets/pilatusDetectorParameters.template +``` The top level pilatus.template includes this file, as well as records that provide logic (for things like the arrayRate and EPICSShutter in areaDetector). - -UI --- +## UI Finally, UI elements can be generated for each component for multiple graphical applications. For example, the following EDM screen is generated: -.. image:: ../../images/pilatus_edl.png - :width: 50% - :align: center - +```{image} ../images/pilatus_edl.png +:align: center +:width: 50% +``` This can serve as a low level overview of the entire system, as well as a convenient pallette for constructing higher level, more structured screens. - -Ongoing Development -------------------- +## Ongoing Development Once a module is working with PVI (either an existing module after the YAML is created from the templates using the one time generation script, or a newly written module) it @@ -230,134 +220,126 @@ will then be necessary to update the YAML file in future development. Here is an example of necessary changes to add a new parameter, with and without PVI: -With PVI -~~~~~~~~ +### With PVI Update YAML file: -.. code-block:: YAML - - - type: AsynFloat64 - name: DelayTime - description: Delay in seconds between the external trigger and the start of image acquisition - role: Setting - initial: 0 - record_fields: - PREC: 3 - EGU: s +```YAML +- type: AsynFloat64 + name: DelayTime + description: Delay in seconds between the external trigger and the start of image acquisition + role: Setting + initial: 0 + record_fields: + PREC: 3 + EGU: s +``` And then run pvi (or possibly just make, if it is integrated into a Makefile). It can then be shared with other sites who can generate their own required files. - -Without PVI -~~~~~~~~~~~ +### Without PVI Update template: -.. code-block:: cpp - - # Delay time in External Trigger mode. - record(ao, "$(P)$(R)DelayTime") - { - field(PINI, "YES") - field(DTYP, "asynFloat64") - field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME") - field(EGU, "s") - field(VAL, "0") - field(PREC, "6") - } - - record(ai, "$(P)$(R)DelayTime_RBV") - { - field(DTYP, "asynFloat64") - field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME") - field(EGU, "s") - field(PREC, "6") - field(SCAN, "I/O Intr") - } - +```cpp +# Delay time in External Trigger mode. +record(ao, "$(P)$(R)DelayTime") +{ + field(PINI, "YES") + field(DTYP, "asynFloat64") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME") + field(EGU, "s") + field(VAL, "0") + field(PREC, "6") +} + +record(ai, "$(P)$(R)DelayTime_RBV") +{ + field(DTYP, "asynFloat64") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))DELAY_TIME") + field(EGU, "s") + field(PREC, "6") + field(SCAN, "I/O Intr") +} +``` Update header file: -.. code-block:: cpp - - ... - #define PilatusDelayTimeString "DELAY_TIME" - ... - createParam(PilatusDelayTimeString, asynParamFloat64, &PilatusDelayTime); - ... - int PilatusDelayTime; - ... - +```cpp +... +#define PilatusDelayTimeString "DELAY_TIME" +... +createParam(PilatusDelayTimeString, asynParamFloat64, &PilatusDelayTime); +... +int PilatusDelayTime; +... +``` Update docs: -.. code-block:: rst - - * - Delay in seconds between the external trigger and the start of image acquisition - - DELAY_TIME - - $(P)$(R)DelayTime - - ao - +```rst +* - Delay in seconds between the external trigger and the start of image acquisition + - DELAY_TIME + - $(P)$(R)DelayTime + - ao +``` Update screens (of course, this will actually involve editing with a graphical interface): -.. code-block:: javascript - - "text update" { - object { - x=604 - y=146 - width=80 - height=18 - } - monitor { - chan="$(P)$(R)DelayTime_RBV" - clr=54 - bclr=4 - } - align="horiz. centered" - limits { - } +```javascript +"text update" { + object { + x=604 + y=146 + width=80 + height=18 } - "text entry" { - object { - x=540 - y=145 - width=59 - height=20 - } - control { - chan="$(P)$(R)DelayTime" - clr=14 - bclr=51 - } - limits { - } + monitor { + chan="$(P)$(R)DelayTime_RBV" + clr=54 + bclr=4 } - text { - object { - x=435 - y=145 - width=100 - height=20 - } - "basic attribute" { - clr=14 - } - textix="Delay time" - align="horiz. right" + align="horiz. centered" + limits { } +} +"text entry" { + object { + x=540 + y=145 + width=59 + height=20 + } + control { + chan="$(P)$(R)DelayTime" + clr=14 + bclr=51 + } + limits { + } +} +text { + object { + x=435 + y=145 + width=100 + height=20 + } + "basic attribute" { + clr=14 + } + textix="Delay time" + align="horiz. right" +} +``` Then either add equivalent changes to other screen types or use autoconvert, if available, and add any site specific details to any of these files (such as autosave and archiver tags). - -Class Hierarchy ---------------- +## Class Hierarchy Drivers will access their parameters via a param set, either using inheritance or composition. The class hierarchy for param sets mirrors the drivers. Each of the 'base' @@ -381,67 +363,59 @@ the non-virtual base classes. This means the constructors are called in the corr order such that when the asynPortDriver constructor the ``asynParamSet`` ``parameterDefinitions`` is fully populated when ``createParams`` is called. -Change Summary -~~~~~~~~~~~~~~ - - * asyn - https://github.com/dls-controls/asyn/tree/pvi - * Created ``asynParamSet`` - * New overloaded asynPortDriver constructor that takes an ``asynParamSet*`` - and calls createParams() - - * ADCore - https://github.com/dls-controls/ADCore/tree/pvi - * ``asynNDArrayDriver`` parameters split into ``asynNDArrayDriverParamSet`` - Constructor updated to take an ``asynNDArrayDriverParamSet*``. - Updated to access parameters via ``paramSet->`` - * ``ADDriver`` parameters split into ``ADDriverParamSet`` - Constructor updated to take an ``ADDriverParamSet*``. - Updated to access parameters via ``paramSet->`` - * ``NDPluginDriver`` inherits ``asynNDArrayDriverParamSet`` in addition to ``asynNDArrayDriver`` - Updated to access parameters via ``paramSet->``. - Child classes work with no changes - * Some trivial updates to the tests - - * ADSimDetector - https://github.com/dls-controls/ADSimDetector/tree/pvi - * ``simDetector`` parameters split into ``simDetectorParamSet`` - * ``simDetector`` inherits from ``simDetectorParamSet`` in addition to - ``ADDriver`` - * Can access parameters as before - - * ADPilatus - https://github.com/dls-controls/ADPilatus/tree/pvi - * Equivalent to ADSimDetector changes - - * motor - https://github.com/dls-controls/motor/tree/pvi - * ``asynMotorController`` parameters split into ``asynMotorControllerParamSet`` - * Updated to access parameters via ``paramSet->`` - - * pmac - https://github.com/dls-controls/pmac/tree/pvi - * ``pmacController`` parameters split into ``pmacControllerParamSet`` - * ``pmacCSController`` same - * Each inherit from their own param set (which inherits - ``asynMotorControllerParamSet``) in addition to ``asynMotorController`` - * Can access parameters as before - -Caveats -~~~~~~~ +### Change Summary + +> - asyn - +> : - Created ``asynParamSet`` +> - New overloaded asynPortDriver constructor that takes an ``asynParamSet*`` +> and calls createParams() +> - ADCore - +> : - ``asynNDArrayDriver`` parameters split into ``asynNDArrayDriverParamSet`` +> : Constructor updated to take an ``asynNDArrayDriverParamSet*``. +> Updated to access parameters via ``paramSet->`` +> - ``ADDriver`` parameters split into ``ADDriverParamSet`` +> : Constructor updated to take an ``ADDriverParamSet*``. +> Updated to access parameters via ``paramSet->`` +> - ``NDPluginDriver`` inherits ``asynNDArrayDriverParamSet`` in addition to ``asynNDArrayDriver`` +> : Updated to access parameters via ``paramSet->``. +> Child classes work with no changes +> - Some trivial updates to the tests +> - ADSimDetector - +> : - ``simDetector`` parameters split into ``simDetectorParamSet`` +> - ``simDetector`` inherits from ``simDetectorParamSet`` in addition to +> ``ADDriver`` +> - Can access parameters as before +> - ADPilatus - +> : - Equivalent to ADSimDetector changes +> - motor - +> : - ``asynMotorController`` parameters split into ``asynMotorControllerParamSet`` +> - Updated to access parameters via ``paramSet->`` +> - pmac - +> : - ``pmacController`` parameters split into ``pmacControllerParamSet`` +> - ``pmacCSController`` same +> - Each inherit from their own param set (which inherits +> ``asynMotorControllerParamSet``) in addition to ``asynMotorController`` +> - Can access parameters as before + +### Caveats There are some changes that are unavoidable without inserting edge cases into the generation logic and making the YAML schema more complicated. Some examples are: - 1. The first param index used for calling base class methods is inconsistently - named, so we will have to agree a consistent way to generate them and make them - the same. - 2. Any readback parameters will have an _RBV suffix added. Some existing readbacks - do not have this, e.g. Armed in pilatusDetector. - 3. FIRST_DRIVER_PARAM needs to be defined in the main header file based on the - FIRST_DRIVER_PARAM_INDEX defined in the param set header file, appending - ``paramSet->`` or not depending on whether it inherits the param set or not. (This - could possibly be handled in a better way by adding more logic to the - ``asynParamSet`` - see `Possible Further Work`_) - 4. Asyn parameter names will be the same as the name of the index variable. The - value can be overridden to define drvInfo for drvUserCreate dynamic parameters. - -Next Steps -~~~~~~~~~~ +> 1. The first param index used for calling base class methods is inconsistently +> named, so we will have to agree a consistent way to generate them and make them +> the same. +> 2. Any readback parameters will have an \_RBV suffix added. Some existing readbacks +> do not have this, e.g. Armed in pilatusDetector. +> 3. FIRST_DRIVER_PARAM needs to be defined in the main header file based on the +> FIRST_DRIVER_PARAM_INDEX defined in the param set header file, appending +> ``paramSet->`` or not depending on whether it inherits the param set or not. (This +> could possibly be handled in a better way by adding more logic to the +> ``asynParamSet`` - see [Possible Further Work]) +> 4. Asyn parameter names will be the same as the name of the index variable. The +> value can be overridden to define drvInfo for drvUserCreate dynamic parameters. + +### Next Steps A script is in development to perform the generation of an initial YAML file from a template. The idea being that after this point, everything is generated from the YAML @@ -462,18 +436,19 @@ set, with no additional logic. This could then instantiate the param set and pas the driver constructor. Either solution would resolve caveat 3, because all classes will access FIRST_DRIVER_PARAM via ``paramSet->``. -Possible Further Work -~~~~~~~~~~~~~~~~~~~~~ +### Possible Further Work Adopting this framework could make it easier to make other improvements to the C++ code, such as: - * Move some asynPortDriver functionality into ``asynParamSet`` / ``asynParam`` (See ADEiger eigerParam) - * Reduce the size of ``asynPortDriver`` - * Possibility of typed subclasses of ``asynParam`` to reduce if statements for - handling the many ``asynParamType`` values in slightly different ways - * Simplify the sharing of file writing functionality between some drivers - * ADPilatus and NDPluginFile-derived classes - Parameters could be split out - of ``asynNDArrayDriver`` into a ``FileWriterParamSet``, which could then be - included via composition only where required, rather than every driver and - plugin under ``asynNDArrayDriver`` having these parameters. +> - Move some asynPortDriver functionality into ``asynParamSet`` / ``asynParam`` (See ADEiger eigerParam) +> : - Reduce the size of ``asynPortDriver`` +> - Possibility of typed subclasses of ``asynParam`` to reduce if statements for +> handling the many ``asynParamType`` values in slightly different ways +> - Simplify the sharing of file writing functionality between some drivers +> : - ADPilatus and NDPluginFile-derived classes - Parameters could be split out +> of ``asynNDArrayDriver`` into a ``FileWriterParamSet``, which could then be +> included via composition only where required, rather than every driver and +> plugin under ``asynNDArrayDriver`` having these parameters. + +[yaml]: https://en.wikipedia.org/wiki/YAML diff --git a/docs/developer/how-to/write-a-formatter.rst b/docs/how-to/write-a-formatter.md similarity index 72% rename from docs/developer/how-to/write-a-formatter.rst rename to docs/how-to/write-a-formatter.md index af40a49f..4de1ad58 100644 --- a/docs/developer/how-to/write-a-formatter.rst +++ b/docs/how-to/write-a-formatter.md @@ -1,40 +1,44 @@ -How to Write a Site Specific Formatter -====================================== +# How to Write a Site Specific Formatter + This guide explains how you can create a pvi formatter to generate screens for your own use cases. -Overview --------- +## Overview + The formatters role is to take a device.yaml file and turn this into a screen file that can be used by the display software. Inside of the device.yaml file is a list of components that specify its name, a widget type and any additional properties that can be assigned to that widget (such as a pv name). During formatting, the device.yaml file is deserialised into component objects, which are later translated into widgets: -.. literalinclude:: ../../../src/pvi/device.py - :pyobject: Component +```{literalinclude} ../../src/pvi/device.py +:pyobject: Component +``` -.. literalinclude:: ../../../src/pvi/device.py - :pyobject: SignalR +```{literalinclude} ../../src/pvi/device.py +:pyobject: SignalR +``` To make a screen from this, we need a template file. This contains a blank representation of each supported widget for each of the supported file formats (bob, edl etc...). Below is an example of a 'text entry' widget for a .bob file: -.. literalinclude:: ../../../src/pvi/_format/dls.bob - :lines: 57-73 +```{literalinclude} ../../src/pvi/_format/dls.bob +:lines: 57-73 +``` By extracting and altering the template widgets with the information provided by the components, we can create a screen file. -Create a formatter subclass ---------------------------- +## Create a formatter subclass + To start, we will need to create our own formatter class. These inherit from an abstract 'Formatter' class that is defined in base.py. Inside, we need to define one mandatory 'format' function, which will be used to create our screen file: -.. literalinclude:: ../../../src/pvi/_format/base.py - :pyobject: Formatter +```{literalinclude} ../../src/pvi/_format/base.py +:pyobject: Formatter +``` The format function takes in a device: a list of components obtained from our deserialised device.yaml file, A prefix: the pv prefix of the device, and a path: the @@ -43,14 +47,15 @@ output destination for the generated screen file. With a formatter defined, we now can start to populate this by defining the screen dependencies. -Define the Screen Layout Properties ------------------------------------ +## Define the Screen Layout Properties + Each screen requires a number of layout properties that allow you to customise the size and placement of widgets. These are stored within a 'ScrenLayout' dataclass that can be imported from utils.py. Within the dataclass are the following configurable parameters: -.. literalinclude:: ../../../src/pvi/_format/screen.py - :pyobject: ScreenLayout +```{literalinclude} ../../src/pvi/_format/screen.py +:pyobject: ScreenLayout +``` When defining these in our formatter, we have the option of deciding which properties should be configurable inside of the formatter.yaml. Properties defined as member @@ -58,9 +63,10 @@ variables of the formatter class (and then referenced by the layout properties i screen format function) will be available to adjust inside of the formatter.yaml. Anything else, should be considered as defaults for the formatter: -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: LP DOCS REF - :end-before: SW DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:end-before: SW DOCS REF +:start-after: LP DOCS REF +``` In the example above, everything has been made adjustable from the formatter.yaml except the properties relating to groups. This is becuase they are more dependant on the file @@ -70,43 +76,44 @@ For clarity, the example below shows how the formatter.yaml can be used to set t layout properties. Note that these are optional as each property is defined with a default value: -.. literalinclude:: ../../../formatters/dls.bob.pvi.formatter.yaml +```{literalinclude} ../../formatters/dls.bob.pvi.formatter.yaml +``` + +## Assign a Template File -Assign a Template File ----------------------- As previously stated, a template file provides the formatter with a base model of all of the supported widgets that it can then overwrite with component data. Currently, pvi supports templates for edl, adl and bob files, which can be referenced from the -_format directory with the filename 'dls' + the file formats suffix (eg. dls.bob). +\_format directory with the filename 'dls' + the file formats suffix (eg. dls.bob). Inside of the format function, we need to provide a reference to the template file that can then be used to identify what each widget should look like: -.. code-block:: python3 +```python3 +template = BobTemplate(str(Path(__file__).parent / "dls.bob")) +``` - template = BobTemplate(str(Path(__file__).parent / "dls.bob")) +% Documentation does not explain what the WidgetTemplate function does, +% nor its subclasses BobTemplate, EdlTemplate & AdlTemplate. -.. - Documentation does not explain what the WidgetTemplate function does, - nor its subclasses BobTemplate, EdlTemplate & AdlTemplate. +## Divide the Template into Widgets -Divide the Template into Widgets --------------------------------- With a template defined, we now need to assign each part of it to a supported widget. This is achieved using the ScreenWidgets dataclass (from utils.py). With this, we can assign each of the widget classes to a snippet of the template using the WidgetFactory.from_template method: -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: SW DOCS REF - :end-before: MAKE_WIDGETS DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:end-before: MAKE_WIDGETS DOCS REF +:start-after: SW DOCS REF +``` This function uses a unique search term to locate and extract a widget from the template. As such, the search term MUST be unique to avoid extracing multiple or irrelevant widgets from the template. -Define screen and group widget functions ----------------------------------------- +## Define screen and group widget functions + Two widgets that are not handled by ScreenWidgets are the screen title and group object. This is because the style of these widgets differ greatly for each file type. For instance, with edl and adl files, groups are represented by a rectangle and title placed @@ -119,18 +126,20 @@ We then need to define two functions that can be used to create multiple instanc these widgets. In this example, we provide two arguments: The 'bounds', to set the widgets size and position, and the 'title' to populate the label with. -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: MAKE_WIDGETS DOCS REF - :end-before: SCREEN_INI DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:end-before: SCREEN_INI DOCS REF +:start-after: MAKE_WIDGETS DOCS REF +``` + +## Construct a Screen Object -Construct a Screen Object -------------------------- Provided that you have defined the LayoutProperties, template, ScreenWidgets and the screen title and group object functions, we are now ready to define a screen object. -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: SCREEN_INI DOCS REF - :end-before: SCREEN_FORMAT DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:end-before: SCREEN_FORMAT DOCS REF +:start-after: SCREEN_INI DOCS REF +``` Note that screen_cls and group_cls are defined separately here as GroupFactories. This is because they take in the make_widgets function, which has the possibility of returning @@ -144,21 +153,24 @@ On the output of this, we can call a (screen.)format function that populates the with the extracted properties from the device.yaml, and converts them into the chosen file format: -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: SCREEN_FORMAT DOCS REF - :end-before: SCREEN_WRITE DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:end-before: SCREEN_WRITE DOCS REF +:start-after: SCREEN_FORMAT DOCS REF +``` + +## Generate the Screen file -Generate the Screen file ------------------------- After calling format on the screen object, you will be left with a list of strings that represent each widget in your chosen file format. The final step is to create a screen file by unpacking the list and writing each widget to the file: -.. literalinclude:: ../../../src/pvi/_format/dls.py - :start-after: SCREEN_WRITE DOCS REF +```{literalinclude} ../../src/pvi/_format/dls.py +:start-after: SCREEN_WRITE DOCS REF +``` And thats it. With this you can now create your own custom formatters. Below you can find a complete example formatter, supporting both edl and bob file formats for DLS: -.. literalinclude:: ../../../src/pvi/_format/dls.py - :pyobject: DLSFormatter +```{literalinclude} ../../src/pvi/_format/dls.py +:pyobject: DLSFormatter +``` From ec7af0dc9d4facf31a084f7e4aaf60be70851881 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 5 Jul 2024 13:01:02 +0000 Subject: [PATCH 3/3] Update code standards --- src/pvi/__main__.py | 77 +++++++++++++-------- src/pvi/_convert/_asyn_convert.py | 12 ++-- src/pvi/_convert/_parameters.py | 9 ++- src/pvi/_convert/_template_convert.py | 35 +++++----- src/pvi/_convert/utils.py | 17 +++-- src/pvi/_format/adl.py | 19 ++--- src/pvi/_format/aps.py | 5 +- src/pvi/_format/base.py | 2 +- src/pvi/_format/bob.py | 27 +++++--- src/pvi/_format/dls.py | 9 ++- src/pvi/_format/edl.py | 21 +++--- src/pvi/_format/screen.py | 47 ++++++------- src/pvi/_format/template.py | 5 +- src/pvi/_format/utils.py | 17 ++--- src/pvi/_format/widget.py | 99 +++++++++++++-------------- src/pvi/_pv_group.py | 15 ++-- src/pvi/_schema_utils.py | 8 +-- src/pvi/_yaml_utils.py | 10 +-- src/pvi/device.py | 32 ++++----- src/pvi/typed_model.py | 2 +- src/pvi/utils.py | 3 +- 21 files changed, 240 insertions(+), 231 deletions(-) diff --git a/src/pvi/__main__.py b/src/pvi/__main__.py index d489f0e5..cb2b4fc0 100644 --- a/src/pvi/__main__.py +++ b/src/pvi/__main__.py @@ -1,7 +1,7 @@ import json import os from pathlib import Path -from typing import List, Optional +from typing import Annotated, Optional import typer @@ -26,19 +26,25 @@ def version_callback(value: bool): @app.callback() def main( - version: Optional[bool] = typer.Option( + # TODO: typer does not support ` | None` yet + # https://github.com/tiangolo/typer/issues/533 + version: Optional[bool] = typer.Option( # noqa None, "--version", callback=version_callback, is_eager=True, help="Print the version and exit", - ) + ), ): """PVI builder interface""" @app.command() -def schema(output: Path = typer.Argument(..., help="filename to write the schema to")): +def schema( + output: Annotated[ + Path, typer.Argument(..., help="filename to write the schema to") + ], +): """Write the JSON schema for the pvi interface""" assert output.name.endswith( ".schema.json" @@ -57,18 +63,25 @@ def schema(output: Path = typer.Argument(..., help="filename to write the schema @app.command() def format( - output_path: Path = typer.Argument( - ..., help="Directory to write output file(s) to" - ), - device_path: Path = typer.Argument(..., help="Path to the .pvi.device.yaml file"), - formatter_path: Path = typer.Argument( - ..., help="Path to the .pvi.formatter.yaml file" - ), - yaml_paths: List[Path] = typer.Option( - [], "--yaml-path", help="Paths to directories with .pvi.device.yaml files" - ), + output_path: Annotated[ + Path, typer.Argument(..., help="Directory to write output file(s) to") + ], + device_path: Annotated[ + Path, typer.Argument(..., help="Path to the .pvi.device.yaml file") + ], + formatter_path: Annotated[ + Path, typer.Argument(..., help="Path to the .pvi.formatter.yaml file") + ], + yaml_paths: Annotated[ + Optional[list[Path]], # noqa + typer.Option( + ..., "--yaml-path", help="Paths to directories with .pvi.device.yaml files" + ), + ] = None, ): """Create screen product from device and formatter YAML""" + yaml_paths = yaml_paths or [] + device = Device.deserialize(device_path) device.deserialize_parents(yaml_paths) @@ -78,9 +91,11 @@ def format( @app.command() def generate_template( - device_path: Path = typer.Argument(..., help="Path to the .pvi.device.yaml file"), - pv_prefix: str = typer.Argument(..., help="Prefix of PVI PV"), - output_path: Path = typer.Argument(..., help="Output file to generate"), + device_path: Annotated[ + Path, typer.Argument(..., help="Path to the .pvi.device.yaml file") + ], + pv_prefix: Annotated[str, typer.Argument(..., help="Prefix of PVI PV")], + output_path: Annotated[Path, typer.Argument(..., help="Output file to generate")], ): """Create template with info tags for device signals""" device = Device.deserialize(device_path) @@ -89,13 +104,18 @@ def generate_template( @convert_app.command() def device( - output: Path = typer.Argument(..., help="Directory to write output file to"), - h: Path = typer.Argument(..., help="Path to the .h file to convert"), - templates: List[Path] = typer.Option( - [], "--template", help="Paths to .template files to convert" - ), + output: Annotated[ + Path, typer.Argument(..., help="Directory to write output file to") + ], + h: Annotated[Path, typer.Argument(..., help="Path to the .h file to convert")], + templates: Annotated[ + Optional[list[Path]], # noqa + typer.Option(..., "--template", help="Paths to .template files to convert"), + ] = None, ): """Convert template to device YAML""" + templates = templates or [] + if not output.exists(): os.mkdir(output) @@ -108,12 +128,13 @@ def device( @app.command() def regroup( - device_path: Path = typer.Argument( - ..., help="Path to the device.yaml file to regroup" - ), - ui_paths: List[Path] = typer.Argument( - ..., help="Paths to the ui files to regroup the PVs by" - ), + device_path: Annotated[ + Path, typer.Argument(..., help="Path to the device.yaml file to regroup") + ], + ui_paths: Annotated[ + list[Path], + typer.Argument(..., help="Paths to the ui files to regroup the PVs by"), + ], ): """Regroup a device.yaml file based on ui files that the PVs appear in""" device = Device.deserialize(device_path) diff --git a/src/pvi/_convert/_asyn_convert.py b/src/pvi/_convert/_asyn_convert.py index 3f27b883..9d6f667e 100644 --- a/src/pvi/_convert/_asyn_convert.py +++ b/src/pvi/_convert/_asyn_convert.py @@ -1,5 +1,5 @@ import re -from typing import Any, ClassVar, List, Optional, Type, cast +from typing import Any, ClassVar, cast from pydantic import Field @@ -40,7 +40,7 @@ def model_post_init(self, __context: Any): if "DESC" not in self.fields.keys(): self.fields["DESC"] = self.name - def get_parameter_name(self) -> Optional[str]: + def get_parameter_name(self) -> str | None: # e.g. from: field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_PATH") # extract: FILE_PATH parameter_name_extractor = r"@asyn\(.*\)(\S+)" @@ -53,7 +53,7 @@ def get_parameter_name(self) -> Optional[str]: parameter_name = match.group(1) return parameter_name - def asyn_component_type(self) -> Type["AsynParameter"]: + def asyn_component_type(self) -> type["AsynParameter"]: # For waveform records the data type is defined by DTYP if self.type == "waveform": return get_waveform_parameter(self.fields["DTYP"]) @@ -236,7 +236,7 @@ class AsynFloat64Waveform(AsynWaveform): WaveformRecordTypes = [AsynWaveform] + cast( - List[Type[AsynWaveform]], rec_subclasses(AsynWaveform) + list[type[AsynWaveform]], rec_subclasses(AsynWaveform) ) @@ -248,4 +248,6 @@ def get_waveform_parameter(dtyp: str): ): return waveform_cls - assert False, f"Waveform type for DTYP {dtyp} not found in {WaveformRecordTypes}" + raise AssertionError( + f"Waveform type for DTYP {dtyp} not found in {WaveformRecordTypes}" + ) diff --git a/src/pvi/_convert/_parameters.py b/src/pvi/_convert/_parameters.py index d7b90517..5d5fb8d2 100644 --- a/src/pvi/_convert/_parameters.py +++ b/src/pvi/_convert/_parameters.py @@ -1,7 +1,6 @@ import re from enum import Enum from functools import cached_property -from typing import Dict, Optional from pydantic import BaseModel, Field @@ -64,8 +63,8 @@ class DisplayForm(Enum): class Record(BaseModel): pv: str # The pv of the record e.g. $(P)$(M)Status type: str # The record type string e.g. ao, stringin - fields: Dict[str, str] # The record fields - infos: Dict[str, str] # Any infos to be added to the record + fields: dict[str, str] # The record fields + infos: dict[str, str] # Any infos to be added to the record @cached_property def name(self) -> str: @@ -78,7 +77,7 @@ class Parameter(BaseModel): invalid: list[str] = ["DESC", "DTYP", "INP", "OUT", "PINI", "VAL"] - def _remove_invalid(self, fields: Dict[str, str]) -> Dict[str, str]: + def _remove_invalid(self, fields: dict[str, str]) -> dict[str, str]: valid_fields = { key: value for (key, value) in fields.items() if key not in self.invalid } @@ -89,5 +88,5 @@ def generate_component(self) -> ComponentUnion: class ReadParameterMixin: - def _get_read_record(self) -> Optional[str]: + def _get_read_record(self) -> str | None: raise NotImplementedError(self) diff --git a/src/pvi/_convert/_template_convert.py b/src/pvi/_convert/_template_convert.py index 62348f7e..bfec625a 100644 --- a/src/pvi/_convert/_template_convert.py +++ b/src/pvi/_convert/_template_convert.py @@ -1,6 +1,5 @@ import re from pathlib import Path -from typing import List, Tuple from pvi.device import ( ComponentUnion, @@ -23,7 +22,7 @@ class TemplateConverter: - def __init__(self, templates: List[Path]): + def __init__(self, templates: list[Path]): self.templates = templates self._text = [t.read_text() for t in self.templates] @@ -35,11 +34,11 @@ def convert(self) -> Tree: children=template_components, ) for template, template_components in zip( - self.templates, self._extract_components() + self.templates, self._extract_components(), strict=True ) ] - def _extract_components(self) -> List[List[ComponentUnion]]: + def _extract_components(self) -> list[list[ComponentUnion]]: components = [] for text in self._text: record_extractor = RecordExtractor(text) @@ -70,7 +69,7 @@ def _extract_record_strs(self): record_extractor = re.compile(r"\s*^[^#\n]*record\([^{]*{[^}]*}", re.MULTILINE) return re.findall(record_extractor, self._text) - def _parse_record(self, record_str: str) -> Tuple: + def _parse_record(self, record_str: str) -> tuple: # extract three groups from a record definition e.g. # from: # record(waveform, "$(P)$(R)FilePath") @@ -100,7 +99,7 @@ def _parse_record(self, record_str: str) -> Tuple: raise RecordError(f"Parse failed on record: {record_str}") return matches[0] - def _extract_fields(self, fields_str: str) -> List[Tuple[str, str]]: + def _extract_fields(self, fields_str: str) -> list[tuple[str, str]]: # extract two groups from a field e.g. # from: field(PINI, "YES") # extract: @@ -111,7 +110,7 @@ def _extract_fields(self, fields_str: str) -> List[Tuple[str, str]]: ) return re.findall(field_extractor, fields_str) - def _extract_infos(self, fields_str: str) -> List[Tuple[str, str]]: + def _extract_infos(self, fields_str: str) -> list[tuple[str, str]]: # extract two groups from an info tag e.g. # from: info(autosaveFields, "VAL") # extract: @@ -136,7 +135,7 @@ def _create_asyn_record(self, record_str: str) -> AsynRecord: record = AsynRecord(pv=record_name, type=record_type, fields=fields, infos=info) return record - def get_asyn_records(self) -> List[AsynRecord]: + def get_asyn_records(self) -> list[AsynRecord]: record_strs = self._extract_record_strs() record_list = [] for record_str in record_strs: @@ -149,10 +148,10 @@ def get_asyn_records(self) -> List[AsynRecord]: class RecordRoleSorter: @staticmethod - def sort_records(records: List[AsynRecord]) -> List[Parameter]: + def sort_records(records: list[AsynRecord]) -> list[Parameter]: def _sort_inputs_outputs( - records: List[AsynRecord], - ) -> Tuple[List[AsynRecord], List[AsynRecord]]: + records: list[AsynRecord], + ) -> tuple[list[AsynRecord], list[AsynRecord]]: inp_records = [r for r in records if "INP" in r.fields] write_records = [r for r in records if "OUT" in r.fields] @@ -169,7 +168,7 @@ def _sort_inputs_outputs( return read_records, write_records read_records, write_records = _sort_inputs_outputs(records) - parameters: List[Parameter] = [] + parameters: list[Parameter] = [] parameters += ParameterRoleMatcher.get_actions(read_records, write_records) parameters += ParameterRoleMatcher.get_readbacks(read_records, write_records) parameters += ParameterRoleMatcher.get_setting_pairs( @@ -241,8 +240,8 @@ def generate_component(self) -> SignalW: class ParameterRoleMatcher: @staticmethod def get_actions( - read_records: List[AsynRecord], write_records: List[AsynRecord] - ) -> List[Action]: + read_records: list[AsynRecord], write_records: list[AsynRecord] + ) -> list[Action]: actions = [ Action(write_record=w) for w in write_records @@ -253,8 +252,8 @@ def get_actions( @staticmethod def get_readbacks( - read_records: List[AsynRecord], write_records: List[AsynRecord] - ) -> List[Readback]: + read_records: list[AsynRecord], write_records: list[AsynRecord] + ) -> list[Readback]: readbacks = [ Readback(read_record=r) for r in read_records @@ -265,8 +264,8 @@ def get_readbacks( @staticmethod def get_setting_pairs( - read_records: List[AsynRecord], write_records: List[AsynRecord] - ) -> List[SettingPair]: + read_records: list[AsynRecord], write_records: list[AsynRecord] + ) -> list[SettingPair]: setting_pairs = [ SettingPair(read_record=r, write_record=w) for r in read_records diff --git a/src/pvi/_convert/utils.py b/src/pvi/_convert/utils.py index fe8deb09..2dd235d3 100644 --- a/src/pvi/_convert/utils.py +++ b/src/pvi/_convert/utils.py @@ -1,8 +1,7 @@ import re -from typing import List, Tuple -def extract_device_and_parent_class(header_text: str) -> Tuple[str, str]: +def extract_device_and_parent_class(header_text: str) -> tuple[str, str]: # e.g. extract 'NDPluginDriver' and 'asynNDArrayDriver' from # class epicsShareClass NDPluginDriver : public asynNDArrayDriver, public epicsThreadRunable { # noqa class_extractor = re.compile(r"class.*\s+(\w+)\s+:\s+\w+\s+(\w+).*") @@ -12,7 +11,7 @@ def extract_device_and_parent_class(header_text: str) -> Tuple[str, str]: return classname, parent -def extract_define_strs(header_text, info_strings: List[str]) -> List[str]: +def extract_define_strs(header_text, info_strings: list[str]) -> list[str]: # e.g. extract: #define SimGainXString "SIM_GAIN_X"; define_extractor = re.compile(r'\#define[_A-Za-z0-9 ]*"[^"]*".*') definitions = re.findall(define_extractor, header_text) @@ -21,7 +20,7 @@ def extract_define_strs(header_text, info_strings: List[str]) -> List[str]: return definitions -def extract_create_param_strs(source_text, param_strings: List[str]) -> List[str]: +def extract_create_param_strs(source_text, param_strings: list[str]) -> list[str]: # e.g. extract: createParam(SimGainXString, asynParamFloat64, &SimGainX); create_param_extractor = re.compile(r"((?:this->)?createParam\([^\)]*\);.*)") create_param_strs = re.findall(create_param_extractor, source_text) @@ -30,7 +29,7 @@ def extract_create_param_strs(source_text, param_strings: List[str]) -> List[str return create_param_strs -def extract_index_declarations(header_text, index_names: List[str]) -> List[str]: +def extract_index_declarations(header_text, index_names: list[str]) -> list[str]: # e.g. extract: int SimGainX; declaration_extractor = re.compile(r"\s*int [^;]*;") declarations = re.findall(declaration_extractor, header_text) @@ -40,7 +39,7 @@ def extract_index_declarations(header_text, index_names: List[str]) -> List[str] return declarations -def parse_definition_str(definition_str: str) -> Tuple[str, str]: +def parse_definition_str(definition_str: str) -> tuple[str, str]: # e.g. from: #define SimGainXString "SIM_GAIN_X"; # extract: # Group1: SimGainXString @@ -50,7 +49,7 @@ def parse_definition_str(definition_str: str) -> Tuple[str, str]: return string_info_pair -def parse_create_param_str(create_param_str: str) -> Tuple[str, str]: +def parse_create_param_str(create_param_str: str) -> tuple[str, str]: # e.g. from: createParam(SimGainXString, asynParamFloat64, &SimGainX); # extract: SimGainXString, asynParamFloat64, &SimGainX create_param_extractor = re.compile(r"(?:createParam\()([^\)]*)(?:\))") @@ -65,7 +64,7 @@ def parse_create_param_str(create_param_str: str) -> Tuple[str, str]: return string_index_pair -def insert_param_set_accessors(source_text: str, parameters: List[str]) -> str: +def insert_param_set_accessors(source_text: str, parameters: list[str]) -> str: for parameter in parameters: # Only match parameter name exactly, not others with same prefix source_text = re.sub( @@ -76,7 +75,7 @@ def insert_param_set_accessors(source_text: str, parameters: List[str]) -> str: return source_text -def filter_strings(strings: List[str], filters: List[str]) -> List[str]: +def filter_strings(strings: list[str], filters: list[str]) -> list[str]: return [ string for string in strings if any(filter_ in string for filter_ in filters) ] diff --git a/src/pvi/_format/adl.py b/src/pvi/_format/adl.py index 66c853de..f4c37b61 100644 --- a/src/pvi/_format/adl.py +++ b/src/pvi/_format/adl.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -from typing import List, Optional from pvi._format.utils import Bounds, split_with_sep from pvi._format.widget import UITemplate, WidgetFormatter @@ -26,8 +25,8 @@ def __init__(self, text: str): def set( self, template: str, - bounds: Optional[Bounds] = None, - widget: Optional[WidgetUnion] = None, + bounds: Bounds | None = None, + widget: WidgetUnion | None = None, **properties, ) -> str: if bounds: @@ -42,7 +41,7 @@ def set( value = f"{value}.adl" # Must include file extension # Only need single line - pattern = re.compile(r"^(\s*%s)=.*$" % item, re.MULTILINE) + pattern = re.compile(rf"^(\s*{item})=.*$", re.MULTILINE) if isinstance(value, str): value = f'"{value}"' @@ -67,11 +66,13 @@ def search(self, search: str) -> str: def create_group( self, - group_object: List[str], - children: List[WidgetFormatter[str]], - padding: Bounds = Bounds(), - ) -> List[str]: - texts: List[str] = [] + group_object: list[str], + children: list[WidgetFormatter[str]], + padding: Bounds | None = None, + ) -> list[str]: + padding = padding or Bounds() + + texts: list[str] = [] for c in children: c.bounds.x += padding.x diff --git a/src/pvi/_format/aps.py b/src/pvi/_format/aps.py index 3393368b..3af2b5a4 100644 --- a/src/pvi/_format/aps.py +++ b/src/pvi/_format/aps.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import List from pydantic import Field @@ -149,7 +148,7 @@ def format(self, device: Device, path: Path): def create_group_widget_formatters( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: title_bounds = Bounds( x=bounds.x + layout.spacing, y=bounds.y + layout.spacing, @@ -164,7 +163,7 @@ def create_group_widget_formatters( def create_screen_widget_formatters( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: title_bounds = Bounds(x=0, y=0, w=bounds.w, h=layout.title_height) return [ label_background_formatter(bounds=title_bounds), diff --git a/src/pvi/_format/base.py b/src/pvi/_format/base.py index 4bafc44f..ff29fb5d 100644 --- a/src/pvi/_format/base.py +++ b/src/pvi/_format/base.py @@ -25,7 +25,7 @@ class Formatter(TypedModel, YamlValidatorMixin): def type_adapter(cls) -> TypeAdapter: """Create TypeAdapter of all child classes""" return TypeAdapter( - as_tagged_union(Union[tuple(cls.__subclasses__())]) # type: ignore + as_tagged_union(Union[tuple(cls.__subclasses__())]) # noqa ) @classmethod diff --git a/src/pvi/_format/bob.py b/src/pvi/_format/bob.py index 819f4400..e1c582fb 100644 --- a/src/pvi/_format/bob.py +++ b/src/pvi/_format/bob.py @@ -1,5 +1,5 @@ +from collections.abc import Sequence from copy import deepcopy -from typing import Dict, List, Optional, Sequence from lxml.etree import ElementBase, SubElement, XMLParser, parse @@ -40,8 +40,8 @@ def __init__(self, text: str): def set( self, template: ElementBase, - bounds: Optional[Bounds] = None, - widget: Optional[WidgetUnion] = None, + bounds: Bounds | None = None, + widget: WidgetUnion | None = None, **properties, ) -> ElementBase: if bounds: @@ -82,9 +82,12 @@ def set( add_combo_box_items(t_copy, combo_box) case ("table", TableRead() | TableWrite() as table): add_table_columns(t_copy, table) - case ("textentry", TextWrite(format=format)) | ( - "textupdate", - TextRead(format=format), + case ( + ("textentry", TextWrite(format=format)) + | ( + "textupdate", + TextRead(format=format), + ) ) if format is not None: add_format(t_copy, BOB_TEXT_FORMATS[TextFormat(format)]) case ("byte_monitor", BitField() as bit_field): @@ -122,10 +125,10 @@ def search(self, search: str) -> ElementBase: def create_group( self, - group_object: List[ElementBase], - children: List[WidgetFormatter[ElementBase]], - padding: Bounds = Bounds(), - ) -> List[ElementBase]: + group_object: list[ElementBase], + children: list[WidgetFormatter[ElementBase]], + padding: Bounds | None = None, + ) -> list[ElementBase]: """Create an xml group object from a list of child widgets Args: @@ -137,6 +140,8 @@ def create_group( Returns: An xml group with children attached as subelements """ + padding = padding or Bounds() + assert ( len(group_object) == 1 ), f"Size of group_object is {len(group_object)}, should be 1" @@ -178,7 +183,7 @@ def add_table_column( SubElement(options_element, "option").text = option -def add_button_macros(widget_element: ElementBase, macros: Dict[str, str]): +def add_button_macros(widget_element: ElementBase, macros: dict[str, str]): """Add action macros to the given element. Args: diff --git a/src/pvi/_format/dls.py b/src/pvi/_format/dls.py index 5bb72bbe..2dfcbaea 100644 --- a/src/pvi/_format/dls.py +++ b/src/pvi/_format/dls.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import List from lxml import etree from pydantic import Field @@ -159,7 +158,7 @@ def format_edl(self, device: Device, path: Path): def create_group_box_formatter( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: x, y, w, h = bounds.x, bounds.y, bounds.w, bounds.h return [ group_box_cls( @@ -178,7 +177,7 @@ def create_group_box_formatter( def create_screen_title_formatter( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: return [ screen_title_cls( bounds=Bounds(x=0, y=0, w=bounds.w, h=screen_layout.title_height), @@ -333,7 +332,7 @@ def format_bob(self, device: Device, path: Path): def create_group_object_formatter( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: return [ group_title_cls( bounds=Bounds(x=bounds.x, y=bounds.y, w=bounds.w, h=bounds.h), @@ -343,7 +342,7 @@ def create_group_object_formatter( def create_screen_title_formatter( bounds: Bounds, title: str - ) -> List[WidgetFormatter[str]]: + ) -> list[WidgetFormatter[str]]: return [ screen_title_cls( bounds=Bounds(x=0, y=0, w=bounds.w, h=screen_layout.title_height), diff --git a/src/pvi/_format/edl.py b/src/pvi/_format/edl.py index 5ba73af4..67a7c7fd 100644 --- a/src/pvi/_format/edl.py +++ b/src/pvi/_format/edl.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -from typing import List, Optional from pvi._format.utils import Bounds, split_with_sep from pvi._format.widget import UITemplate, WidgetFormatter @@ -25,8 +24,8 @@ def __init__(self, text: str): def set( self, template: str, - bounds: Optional[Bounds] = None, - widget: Optional[WidgetUnion] = None, + bounds: Bounds | None = None, + widget: WidgetUnion | None = None, **properties, ) -> str: if bounds: @@ -36,14 +35,14 @@ def set( if item == "displayFileName": value = f"0 {value}" # These are items in an array but we only use one - multiline = re.compile(r"^%s {[^}]*}$" % item, re.MULTILINE | re.DOTALL) + multiline = re.compile(rf"^{item} {{[^}}]*}}$", re.MULTILINE | re.DOTALL) if multiline.search(template): pattern = multiline lines = str(value).splitlines() value = "\n".join(["{"] + [f' "{x}"' for x in lines] + ["}"]) else: # Single line - pattern = re.compile(r"^%s .*$" % item, re.MULTILINE) + pattern = re.compile(rf"^{item} .*$", re.MULTILINE) if isinstance(value, str): value = f'"{value}"' @@ -68,11 +67,13 @@ def search(self, search: str) -> str: def create_group( self, - group_object: List[str], - children: List[WidgetFormatter[str]], - padding: Bounds = Bounds(), - ) -> List[str]: - texts: List[str] = [] + group_object: list[str], + children: list[WidgetFormatter[str]], + padding: Bounds | None = None, + ) -> list[str]: + padding = padding or Bounds() + + texts: list[str] = [] for c in children: c.bounds.x += padding.x diff --git a/src/pvi/_format/screen.py b/src/pvi/_format/screen.py index ae9c7bb7..9c810c36 100644 --- a/src/pvi/_format/screen.py +++ b/src/pvi/_format/screen.py @@ -1,15 +1,9 @@ from __future__ import annotations +from collections.abc import Iterator, Sequence from typing import ( - Dict, Generic, - Iterator, - List, - Sequence, - Tuple, - Type, TypeVar, - Union, ) from pydantic import BaseModel, Field @@ -66,16 +60,16 @@ class ScreenLayout(BaseModel): class ScreenFormatterFactory(BaseModel, Generic[T]): - screen_formatter_cls: Type[GroupFormatter] - group_formatter_cls: Type[GroupFormatter] + screen_formatter_cls: type[GroupFormatter] + group_formatter_cls: type[GroupFormatter] widget_formatter_factory: WidgetFormatterFactory layout: ScreenLayout - components: Dict[str, ComponentUnion] = Field(default={}, init_var=False) + components: dict[str, ComponentUnion] = Field(default={}, init_var=False) base_file_name: str = "" def create_screen_formatter( self, components: Tree, title: str - ) -> Tuple[GroupFormatter[T], List[Tuple[str, GroupFormatter[T]]]]: + ) -> tuple[GroupFormatter[T], list[tuple[str, GroupFormatter[T]]]]: """Create an instance of `screen_cls` populated with widgets of `components` Args: @@ -92,8 +86,8 @@ def create_screen_formatter( ) screen_bounds = Bounds(h=self.layout.max_height) widget_dims = {"w": full_w, "h": self.layout.widget_height} - screen_widgets: List[WidgetFormatter[T]] = [] - columns: List[Bounds] = [Bounds(**widget_dims)] + screen_widgets: list[WidgetFormatter[T]] = [] + columns: list[Bounds] = [Bounds(**widget_dims)] match components: case [Group(layout=SubScreen()) as component]: @@ -149,8 +143,8 @@ def create_screen_formatter( ) def create_sub_screen_formatters( - self, screen_widgets: List[WidgetFormatter[T]] - ) -> List[Tuple[str, GroupFormatter[T]]]: + self, screen_widgets: list[WidgetFormatter[T]] + ) -> list[tuple[str, GroupFormatter[T]]]: """Create and return `ScreenFormatter`s for any `SubScreenWidgetFormatters` When the root screen formatter is created it will format a button that opens a @@ -185,7 +179,7 @@ def create_sub_screen_formatters( if isinstance(group_widget_factory, SubScreenWidgetFormatter) ] - sub_screen_formatters: List[Tuple[str, GroupFormatter[T]]] = [] + sub_screen_formatters: list[tuple[str, GroupFormatter[T]]] = [] for sub_screen_widget_formatter in sub_screen_widget_formatters: if sub_screen_widget_formatter.components is None: # This is a reference to an existing screen - don't create it @@ -215,7 +209,7 @@ def create_group_formatters( screen_bounds: Bounds, column_bounds: Bounds, next_column_bounds: Bounds, - ) -> List[WidgetFormatter[T]]: + ) -> list[WidgetFormatter[T]]: """Create widget formatters for a Group This could either be a list of widget formatters, or a single group formatter, @@ -299,14 +293,14 @@ def create_group_formatter(self, group: Group, bounds: Bounds) -> GroupFormatter self.layout.label_width + self.layout.widget_width + 2 * self.layout.spacing ) column_bounds = Bounds(w=full_w, h=self.layout.widget_height) - widget_factories: List[WidgetFormatter[T]] = [] + widget_factories: list[WidgetFormatter[T]] = [] assert isinstance( - group.layout, (Grid, SubScreen) + group.layout, Grid | SubScreen ), "Can only do Grid and SubScreen at the moment" for c in group.children: - component: Union[Group, Component] + component: Group | Component match c: case Group(layout=Grid()): if is_table(c): @@ -354,7 +348,7 @@ def create_component_widget_formatters( next_column_bounds: Bounds, indent=False, add_label=True, - ) -> List[WidgetFormatter[T]]: + ) -> list[WidgetFormatter[T]]: """Generate widgets from component data and position them in a grid format Args: @@ -417,7 +411,7 @@ def generate_component_formatters( """ # Widgets are allowed to expand bounds - if not isinstance(c, (SignalRef, Group)): + if not isinstance(c, SignalRef | Group): self.components[c.name] = c # Take a copy to modify in this scope @@ -475,8 +469,9 @@ def generate_component_formatters( row_components = [c] # Create one widget for row match c: - case SignalR(read_widget=TableRead(widgets=widgets)) | SignalW( - write_widget=TableWrite(widgets=widgets) + case ( + SignalR(read_widget=TableRead(widgets=widgets)) + | SignalW(write_widget=TableWrite(widgets=widgets)) ): add_label = False # Do not add row labels for Tables component_bounds.w = 100 * len(widgets) @@ -504,13 +499,13 @@ def generate_component_formatters( def generate_row_component_formatters( self, - row_components: Sequence[Union[Group, Component]], + row_components: Sequence[Group | Component], row_bounds: Bounds, ) -> Iterator[WidgetFormatter[T]]: row_component_bounds = row_bounds.clone().split_into( len(row_components), self.layout.spacing ) - for rc_bounds, rc in zip(row_component_bounds, row_components): + for rc_bounds, rc in zip(row_component_bounds, row_components, strict=True): # It is important to check for SignalX/SignalRW first, as they will also # match SignalR/SignalW diff --git a/src/pvi/_format/template.py b/src/pvi/_format/template.py index 6bc9ce1c..07c4a44c 100644 --- a/src/pvi/_format/template.py +++ b/src/pvi/_format/template.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from pathlib import Path -from typing import List from jinja2 import Template @@ -24,7 +23,7 @@ class PviRecord: def format_template(device: Device, pv_prefix: str, output: Path): - records: List[PviRecord] = [] + records: list[PviRecord] = [] for node in walk(device.children): match node: case SignalRW(read_pv=r, write_pv=w) as signal if r == w: @@ -39,7 +38,7 @@ def format_template(device: Device, pv_prefix: str, output: Path): case SignalR() as signal: records.append(PviRecord(signal.name, signal.read_pv, "r")) - with output.open("w") as expanded, open(PVI_TEMPLATE, "r") as template: + with output.open("w") as expanded, open(PVI_TEMPLATE) as template: template_txt = Template(template.read()).render( device=device.label, pv_prefix=pv_prefix, records=records ) diff --git a/src/pvi/_format/utils.py b/src/pvi/_format/utils.py index 6aa1c392..a2533bd2 100644 --- a/src/pvi/_format/utils.py +++ b/src/pvi/_format/utils.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable from enum import Enum -from typing import Callable, List, Tuple, TypeVar +from typing import TypeVar from pydantic import BaseModel @@ -15,7 +16,7 @@ class Bounds(BaseModel): def clone(self) -> Bounds: return Bounds(x=self.x, y=self.y, w=self.w, h=self.h) - def split_left(self, width: int, spacing: int) -> Tuple[Bounds, Bounds]: + def split_left(self, width: int, spacing: int) -> tuple[Bounds, Bounds]: """Split horizontally by width of first element""" to_split = width + spacing assert to_split < self.w, f"Can't split off {to_split} from {self.w}" @@ -24,8 +25,8 @@ def split_left(self, width: int, spacing: int) -> Tuple[Bounds, Bounds]: return left, right def split_by_ratio( - self, ratio: Tuple[float, ...], spacing: int - ) -> Tuple[Bounds, ...]: + self, ratio: tuple[float, ...], spacing: int + ) -> tuple[Bounds, ...]: """Split horizontally by ratio of widths, separated by spacing""" splits = len(ratio) - 1 widget_space = self.w - splits * spacing @@ -36,10 +37,10 @@ def split_by_ratio( return tuple( Bounds(x=x, y=self.y, w=w, h=self.h) - for x, w in zip(widget_xs, widget_widths) + for x, w in zip(widget_xs, widget_widths, strict=True) ) - def split_into(self, count: int, spacing: int) -> Tuple[Bounds, ...]: + def split_into(self, count: int, spacing: int) -> tuple[Bounds, ...]: """Split horizontally into count equal widths, separated by spacing""" return self.split_by_ratio((1 / count,) * count, spacing) @@ -84,11 +85,11 @@ class GroupType(Enum): T = TypeVar("T") -def concat(items: List[List[T]]) -> List[T]: +def concat(items: list[list[T]]) -> list[T]: return [x for seq in items for x in seq] -def split_with_sep(text: str, sep: str, maxsplit: int = -1) -> List[str]: +def split_with_sep(text: str, sep: str, maxsplit: int = -1) -> list[str]: return [t + sep for t in text.split(sep, maxsplit=maxsplit)] diff --git a/src/pvi/_format/widget.py b/src/pvi/_format/widget.py index 9110c70f..b7452040 100644 --- a/src/pvi/_format/widget.py +++ b/src/pvi/_format/widget.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Callable, Dict, Generic, List, Optional, Type, TypeVar +from collections.abc import Callable +from typing import Generic, TypeVar from pydantic import BaseModel, Field, create_model @@ -45,8 +46,8 @@ def search(self, search: str) -> T: def set( self, template: T, - bounds: Optional[Bounds] = None, - widget: Optional[WidgetUnion] = None, + bounds: Bounds | None = None, + widget: WidgetUnion | None = None, **properties, ) -> T: """Modify template elements with component data @@ -67,10 +68,10 @@ def set( def create_group( self, - group_object: List[T], - children: List[WidgetFormatter[T]], - padding: Bounds = Bounds(), - ) -> List[T]: + group_object: list[T], + children: list[WidgetFormatter[T]], + padding: Bounds, + ) -> list[T]: """Create a group widget with its children embedded and appropriately padded""" raise NotImplementedError(self) @@ -81,21 +82,20 @@ def create_group( class WidgetFormatter(BaseModel, Generic[T]): bounds: Bounds - def format(self) -> List[T]: + def format(self) -> list[T]: """Instances should be created using `from_template`, which defines `format`""" raise NotImplementedError(self) @classmethod def from_template( - cls: Type[WF], + cls: type[WF], template: UITemplate[T], search, sized: Callable[[Bounds], Bounds] = Bounds.clone, - widget_formatter_hook: Optional[ - Callable[[Bounds, str], List[WidgetFormatter[T]]] - ] = None, - property_map: Optional[Dict[str, str]] = None, - ) -> Type[WF]: + widget_formatter_hook: Callable[[Bounds, str], list[WidgetFormatter[T]]] + | None = None, + property_map: dict[str, str] | None = None, + ) -> type[WF]: """Create a WidgetFormatter class from the given template Create a `format` method that searches the template for the `search` section and @@ -114,7 +114,7 @@ def from_template( Instances of the macro will be replaced with the value of the property. """ - def format(self: WidgetFormatter[T]) -> List[T]: + def format(self: WidgetFormatter[T]) -> list[T]: properties = {} if property_map is not None: for placeholder, widget_property in property_map.items(): @@ -162,8 +162,8 @@ def tooltip(self) -> str: class SubScreenWidgetFormatter(WidgetFormatter[T]): label: str file_name: str - components: Optional[Group] = None - macros: Dict[str, str] = Field(default={}) + components: Group | None = None + macros: dict[str, str] = Field(default={}) GWF = TypeVar("GWF", bound="GroupFormatter") @@ -172,9 +172,9 @@ class SubScreenWidgetFormatter(WidgetFormatter[T]): class GroupFormatter(WidgetFormatter[T]): bounds: Bounds title: str - children: List[WidgetFormatter[T]] + children: list[WidgetFormatter[T]] - def format(self) -> List[T]: + def format(self) -> list[T]: """Instances should be created using `from_template`, which defines `format`""" raise NotImplementedError(self) @@ -187,15 +187,14 @@ def resize(self): @classmethod def from_template( - cls: Type[GWF], + cls: type[GWF], template: UITemplate[T], search: GroupType, sized: Callable[[Bounds], Bounds] = Bounds.clone, - widget_formatter_hook: Optional[ - Callable[[Bounds, str], List[WidgetFormatter[T]]] - ] = None, - property_map: Optional[Dict[str, str]] = None, - ) -> Type[GWF]: + widget_formatter_hook: Callable[[Bounds, str], list[WidgetFormatter[T]]] + | None = None, + property_map: dict[str, str] | None = None, + ) -> type[GWF]: """Create a WidgetFormatter class from the given template Create a `format` method that searches the template for the `search` section and @@ -213,10 +212,10 @@ def from_template( Instances of the macro will be replaced with the value of the property. """ - def format(self: GroupFormatter[T]) -> List[T]: + def format(self: GroupFormatter[T]) -> list[T]: padding = sized(self.bounds) - texts: List[T] = [] - made_widgets: List[T] = [] + texts: list[T] = [] + made_widgets: list[T] = [] if search == GroupType.SCREEN: properties = {} @@ -266,22 +265,22 @@ def resize(self): class WidgetFormatterFactory(BaseModel, Generic[T]): - header_formatter_cls: Type[LabelWidgetFormatter[T]] - label_formatter_cls: Type[LabelWidgetFormatter[T]] - led_formatter_cls: Type[PVWidgetFormatter[T]] - progress_bar_formatter_cls: Type[PVWidgetFormatter[T]] - text_read_formatter_cls: Type[PVWidgetFormatter[T]] - check_box_formatter_cls: Type[PVWidgetFormatter[T]] - toggle_formatter_cls: Type[PVWidgetFormatter[T]] - combo_box_formatter_cls: Type[PVWidgetFormatter[T]] - text_write_formatter_cls: Type[PVWidgetFormatter[T]] - table_formatter_cls: Type[PVWidgetFormatter[T]] - action_formatter_cls: Type[ActionWidgetFormatter[T]] - sub_screen_formatter_cls: Type[SubScreenWidgetFormatter[T]] - bitfield_formatter_cls: Type[PVWidgetFormatter[T]] - array_trace_formatter_cls: Type[PVWidgetFormatter[T]] - button_panel_formatter_cls: Type[PVWidgetFormatter[T]] - image_read_formatter_cls: Type[PVWidgetFormatter[T]] + header_formatter_cls: type[LabelWidgetFormatter[T]] + label_formatter_cls: type[LabelWidgetFormatter[T]] + led_formatter_cls: type[PVWidgetFormatter[T]] + progress_bar_formatter_cls: type[PVWidgetFormatter[T]] + text_read_formatter_cls: type[PVWidgetFormatter[T]] + check_box_formatter_cls: type[PVWidgetFormatter[T]] + toggle_formatter_cls: type[PVWidgetFormatter[T]] + combo_box_formatter_cls: type[PVWidgetFormatter[T]] + text_write_formatter_cls: type[PVWidgetFormatter[T]] + table_formatter_cls: type[PVWidgetFormatter[T]] + action_formatter_cls: type[ActionWidgetFormatter[T]] + sub_screen_formatter_cls: type[SubScreenWidgetFormatter[T]] + bitfield_formatter_cls: type[PVWidgetFormatter[T]] + array_trace_formatter_cls: type[PVWidgetFormatter[T]] + button_panel_formatter_cls: type[PVWidgetFormatter[T]] + image_read_formatter_cls: type[PVWidgetFormatter[T]] def pv_widget_formatter( self, @@ -300,7 +299,7 @@ def pv_widget_formatter( A WidgetFormatter representing the component """ - widget_formatter_classes: Dict[type, Type[PVWidgetFormatter[T]]] = { + widget_formatter_classes: dict[type, type[PVWidgetFormatter[T]]] = { # Currently supported formatters of ReadWidget/WriteWidget Components LED: self.led_formatter_cls, ProgressBar: self.progress_bar_formatter_cls, @@ -316,14 +315,14 @@ def pv_widget_formatter( ButtonPanel: self.button_panel_formatter_cls, ImageRead: self.image_read_formatter_cls, } - if isinstance(widget, (TextRead, TextWrite)): + if isinstance(widget, TextRead | TextWrite): bounds.h *= widget.get_lines() widget_formatter_cls = widget_formatter_classes[type(widget)] return widget_formatter_cls(bounds=bounds, pv=pv, widget=widget) -def max_x(widgets: List[WidgetFormatter[T]]) -> int: +def max_x(widgets: list[WidgetFormatter[T]]) -> int: """Given multiple widgets, calulate the maximum x position that they occupy""" if widgets: return max(w.bounds.x + w.bounds.w for w in widgets) @@ -331,7 +330,7 @@ def max_x(widgets: List[WidgetFormatter[T]]) -> int: return 0 -def max_y(widgets: List[WidgetFormatter[T]]) -> int: +def max_y(widgets: list[WidgetFormatter[T]]) -> int: """Given multiple widgets, calulate the maximum y position that they occupy""" if widgets: return max(w.bounds.y + w.bounds.h for w in widgets) @@ -339,7 +338,7 @@ def max_y(widgets: List[WidgetFormatter[T]]) -> int: return 0 -def next_x(widgets: List[WidgetFormatter[T]], spacing: int = 0) -> int: +def next_x(widgets: list[WidgetFormatter[T]], spacing: int = 0) -> int: """Given multiple widgets, calulate the next feasible location for an additional widget in the x axis""" if widgets: @@ -348,7 +347,7 @@ def next_x(widgets: List[WidgetFormatter[T]], spacing: int = 0) -> int: return 0 -def next_y(widgets: List[WidgetFormatter[T]], spacing: int = 0) -> int: +def next_y(widgets: list[WidgetFormatter[T]], spacing: int = 0) -> int: """Given multiple widgets, calulate the next feasible location for an additional widget in the y axis""" if widgets: diff --git a/src/pvi/_pv_group.py b/src/pvi/_pv_group.py index 12591a1d..63622fab 100644 --- a/src/pvi/_pv_group.py +++ b/src/pvi/_pv_group.py @@ -1,6 +1,5 @@ import re from pathlib import Path -from typing import Dict, List, Tuple from pvi.device import ( ComponentUnion, @@ -13,13 +12,13 @@ ) -def find_pvs(pvs: List[str], file_path: Path) -> Tuple[List[str], List[str]]: +def find_pvs(pvs: list[str], file_path: Path) -> tuple[list[str], list[str]]: """Search for the PVs in the file and return lists of found and not found pvs""" - with open(file_path, "r") as f: + with open(file_path) as f: file_content = f.read() - pv_coordinates: Dict[int, List[str]] = {} + pv_coordinates: dict[int, list[str]] = {} remaining_pvs = list(pvs) for pv in pvs: if pv not in file_content: @@ -59,13 +58,13 @@ def find_pvs(pvs: List[str], file_path: Path) -> Tuple[List[str], List[str]]: return grouped_pvs, remaining_pvs -def group_by_ui(device: Device, ui_paths: List[Path]) -> Tree: - signals: List[ComponentUnion] = list(walk(device.children)) +def group_by_ui(device: Device, ui_paths: list[Path]) -> Tree: + signals: list[ComponentUnion] = list(walk(device.children)) # PVs without macros to search for in UI pv_names = [s.name for s in signals] - group_pv_map: Dict[str, List[str]] = {} + group_pv_map: dict[str, list[str]] = {} for ui in ui_paths: ui_pvs, pv_names = find_pvs(pv_names, ui) if ui_pvs: @@ -75,7 +74,7 @@ def group_by_ui(device: Device, ui_paths: List[Path]) -> Tree: print(f"Did not find group for {' | '.join(pv_names)}") # Create groups for parameters we found in the files - ui_groups: List[Group] = [ + ui_groups: list[Group] = [ Group( name=enforce_pascal_case(group_name), layout=Grid(labelled=True), diff --git a/src/pvi/_schema_utils.py b/src/pvi/_schema_utils.py index 98a9444b..fc40a42f 100644 --- a/src/pvi/_schema_utils.py +++ b/src/pvi/_schema_utils.py @@ -1,13 +1,13 @@ -from functools import lru_cache -from typing import List, TypeVar +from functools import cache +from typing import TypeVar # A type variable with an upper bound of type Cls = TypeVar("Cls", bound=type) # Permanently cache so we don't include deserialization subclasses defined below -@lru_cache(maxsize=None) -def rec_subclasses(cls: Cls) -> List[Cls]: +@cache +def rec_subclasses(cls: Cls) -> list[Cls]: """Recursive implementation of type.__subclasses__""" subclasses = [] diff --git a/src/pvi/_yaml_utils.py b/src/pvi/_yaml_utils.py index 33ecbe6c..dfc8af82 100644 --- a/src/pvi/_yaml_utils.py +++ b/src/pvi/_yaml_utils.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import Type, TypeVar, overload +from typing import TypeVar, overload from ruamel.yaml import YAML @@ -33,11 +33,11 @@ def type_first(tree: dict | list) -> dict | list: # Walk down tree if isinstance(tree, dict): for key, branch in tree.items(): - if isinstance(branch, (list, dict)): + if isinstance(branch, list | dict): tree[key] = type_first(branch) elif isinstance(tree, list): for idx, branch in enumerate(tree): - if isinstance(branch, (list, dict)): + if isinstance(branch, list | dict): tree[idx] = type_first(branch) return tree @@ -69,7 +69,7 @@ class YamlValidatorMixin: """ @classmethod - def validate_yaml(cls: Type, yaml: Path) -> dict: + def validate_yaml(cls: type, yaml: Path) -> dict: """Validate the YAML file and load into a serialized dictionary of an instance. This method checks that the given YAML file exists and has an appropriate file @@ -105,6 +105,6 @@ def validate_yaml(cls: Type, yaml: Path) -> dict: raise ValueError( f"Could not deserialize '{cls}' as subtype '{cls_type}', " "not found in subclasses." - ) + ) from None return serialized diff --git a/src/pvi/device.py b/src/pvi/device.py index 47a994ff..7b530aca 100644 --- a/src/pvi/device.py +++ b/src/pvi/device.py @@ -2,6 +2,7 @@ import json import re +from collections.abc import Iterator, Sequence from enum import Enum from pathlib import Path from typing import ( @@ -9,10 +10,6 @@ Annotated, Any, ClassVar, - Dict, - Iterator, - Optional, - Sequence, ) from pydantic import ( @@ -122,8 +119,8 @@ class TextRead(ReadWidget): model_config = ConfigDict(use_enum_values=True) # Use Enum value when dumping - lines: Optional[int] = Field(default=None, description="Number of lines to display") - format: Optional[TextFormat] = Field(default=None, description="Display format") + lines: int | None = Field(default=None, description="Number of lines to display") + format: TextFormat | None = Field(default=None, description="Display format") def get_lines(self): return self.lines or 1 @@ -180,7 +177,7 @@ class ButtonPanel(WriteWidget): """ - actions: Dict[PascalStr, str] = Field( + actions: dict[PascalStr, str] = Field( default={"Go": "1"}, description="PV poker buttons" ) @@ -190,8 +187,8 @@ class TextWrite(WriteWidget): model_config = ConfigDict(use_enum_values=True) # Use Enum value when dumping - lines: Optional[int] = Field(default=None, description="Number of lines to display") - format: Optional[TextFormat] = Field(default=None, description="Display format") + lines: int | None = Field(default=None, description="Number of lines to display") + format: TextFormat | None = Field(default=None, description="Display format") def get_lines(self): return self.lines or 1 @@ -241,7 +238,7 @@ class Plot(Layout): class Row(Layout): """Children are columns in the row""" - header: Optional[Sequence[str]] = Field( + header: Sequence[str] | None = Field( None, description="Labels for the items in the row", ) @@ -366,7 +363,7 @@ class SignalRW(SignalR, SignalW): _single_pv_rw: bool = False @model_validator(mode="after") - def _validate_model(self) -> "SignalRW": + def _validate_model(self) -> SignalRW: if self.read_pv and not self._single_pv_rw and self.read_widget is None: # Update default read widget if given a read PV self.read_widget = TextRead() @@ -392,7 +389,7 @@ class DeviceRef(Component): pv: str = Field(description="Child device PVI PV") ui: str = Field(description="UI file to open for referenced Device") - macros: Dict[str, str] = Field( + macros: dict[str, str] = Field( default={}, description="Macro-value pairs for UI file" ) @@ -429,12 +426,7 @@ class Device(TypedModel, YamlValidatorMixin): """Collection of Components""" label: str = Field(description="Label for screen") - parent: Optional[ - Annotated[ - str, - "The parent device (basename of yaml file)", - ] - ] = None + parent: Annotated[str, "The parent device (basename of yaml file)"] | None = None children: Tree = Field([], description="Child Components") def _to_dict(self) -> dict[str, Any]: @@ -510,7 +502,7 @@ def deserialize_parents(self, yaml_paths: list[Path]): def generate_param_tree(self) -> str: param_tree = ", ".join( - json.dumps((group.model_dump_json())) for group in self.children + json.dumps(group.model_dump_json()) for group in self.children ) # Encode again to quote the string as a value and escape double quotes within return json.dumps('{"parameters":[' + param_tree + "]}") @@ -525,7 +517,7 @@ def find_components(yaml_name: str, yaml_paths: list[Path]) -> Tree: device_yaml = find_pvi_yaml(device_name, yaml_paths) if device_yaml is None: - raise IOError(f"Cannot find {device_name} in {yaml_paths}") + raise OSError(f"Cannot find {device_name} in {yaml_paths}") device = Device.deserialize(device_yaml) diff --git a/src/pvi/typed_model.py b/src/pvi/typed_model.py index 56032714..d9817c36 100644 --- a/src/pvi/typed_model.py +++ b/src/pvi/typed_model.py @@ -121,6 +121,6 @@ def as_tagged_union(union): union_members = get_args(union) return Annotated[ - Union[tuple(cls._tag() for cls in union_members)], + Union[tuple(cls._tag() for cls in union_members)], # noqa TypedModel._discriminator(), ] diff --git a/src/pvi/utils.py b/src/pvi/utils.py index 0cfbccd0..6bd57a4e 100644 --- a/src/pvi/utils.py +++ b/src/pvi/utils.py @@ -1,8 +1,7 @@ from pathlib import Path -from typing import List, Union -def find_pvi_yaml(yaml_name: str, yaml_paths: List[Path]) -> Union[Path, None]: +def find_pvi_yaml(yaml_name: str, yaml_paths: list[Path]) -> Path | None: """Find a yaml file in given directory""" for yaml_path in yaml_paths: if yaml_path.is_dir():