diff --git a/.copier-answers.yml b/.copier-answers.yml index c499717c..62a4edea 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,15 +1,19 @@ # Changes here will be overwritten by Copier -_commit: 1.2.0 +_commit: 2.2.0-25-gcefd623 _src_path: gh:DiamondLightSource/python-copier-template author_email: tom.cobb@diamond.ac.uk author_name: Tom Cobb +component_lifecycle: experimental component_owner: group:default/sscc +component_type: library description: Specify step and flyscan paths in a serializable, efficient and Pythonic way distribution_name: scanspec docker: true docs_type: sphinx git_platform: github.com -github_org: dls-controls +github_org: bluesky package_name: scanspec +pypi: true repo_name: scanspec +type_checker: pyright diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 79b85ff4..d3d639a5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -43,4 +43,4 @@ "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind", // After the container is created, install the python project in editable form "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 index 58db3d99..328eb421 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,14 +1,14 @@ # Contribute to the project Contributions and issues are most welcome! All issues and pull requests are -handled through [GitHub](https://github.com/dls-controls/scanspec/issues). Also, please check for any existing issues before +handled through [GitHub](https://github.com/bluesky/scanspec/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/dls-controls/scanspec/discussions) as a place to ask questions and share ideas. If +Github also offers [discussions](https://github.com/bluesky/scanspec/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. @@ -24,4 +24,4 @@ It is recommended that developers use a [vscode devcontainer](https://code.visua 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/1.2.0/how-to.html). +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.2.0/how-to.html). diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..aa65892f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug Report +about: The template to use for reporting bugs and usability issues +title: " " +labels: 'bug' +assignees: '' + +--- + +Describe the bug, including a clear and concise description of the expected behavior, the actual behavior and the context in which you encountered it (ideally include details of your environment). + +## Steps To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + + +## Acceptance Criteria +- Specific criteria that will be used to judge if the issue is fixed diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 00000000..52c84dd8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,13 @@ +--- +name: Issue +about: The standard template to use for feature requests, design discussions and tasks +title: " " +labels: '' +assignees: '' + +--- + +A brief description of the issue, including specific stakeholders and the business case where appropriate + +## Acceptance Criteria +- Specific criteria that will be used to judge if the issue is fixed diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..8200afe5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,8 @@ +Fixes #ISSUE + +### Instructions to reviewer on how to test: +1. Do thing x +2. Confirm thing y happens + +### Checks for reviewer +- [ ] Would the PR title make sense to a user on a set of release notes diff --git a/.github/pages/index.html b/.github/pages/index.html index 80f0a009..c495f39f 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 6d90f490..c06813af 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -1,3 +1,5 @@ +"""Make switcher.json to allow docs to switch between different versions.""" + import json import logging from argparse import ArgumentParser @@ -6,6 +8,7 @@ def report_output(stdout: bytes, label: str) -> list[str]: + """Print and return something received frm stdout.""" ret = stdout.decode().strip().split("\n") print(f"{label}: {ret}") return ret @@ -52,7 +55,8 @@ def get_versions(ref: str, add: str | None) -> list[str]: return versions -def write_json(path: Path, repository: str, versions: str): +def write_json(path: Path, repository: str, versions: list[str]): + """Write the JSON switcher to path.""" org, repo_name = repository.split("/") struct = [ {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"} @@ -64,6 +68,7 @@ def write_json(path: Path, repository: str, versions: str): def main(args=None): + """Parse args and write switcher.""" parser = ArgumentParser( description="Make a versions.json file from gh-pages directories" ) diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml index 4857ee9e..da5e4936 100644 --- a/.github/workflows/_container.yml +++ b/.github/workflows/_container.yml @@ -25,7 +25,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and export to Docker local cache - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false with: context: . # Need load and tags so we can test it below @@ -46,7 +48,9 @@ jobs: - name: Push cached image to container registry if: github.ref_type == 'tag' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false # This does not build the image again, it will find the image in the # Docker cache and publish it with: diff --git a/.github/workflows/_docs.yml b/.github/workflows/_docs.yml index 40446e33..a1cafcae 100644 --- a/.github/workflows/_docs.yml +++ b/.github/workflows/_docs.yml @@ -47,8 +47,8 @@ jobs: 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 + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages - keep_files: true \ No newline at end of file + keep_files: true diff --git a/.github/workflows/_pypi.yml b/.github/workflows/_pypi.yml index f2ead1bc..0c5258db 100644 --- a/.github/workflows/_pypi.yml +++ b/.github/workflows/_pypi.yml @@ -1,8 +1,5 @@ on: workflow_call: - secrets: - PYPI_TOKEN: - required: true jobs: upload: @@ -18,5 +15,3 @@ jobs: - name: Publish to PyPI using trusted publishing uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index b49fa7dc..10d8ed87 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -23,7 +23,7 @@ jobs: - 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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} files: "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce814ba1..e1c0a71d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest - python-version: ["3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] include: # Include one that runs in the dev environment - runs-on: "ubuntu-latest" @@ -39,6 +39,7 @@ jobs: if: needs.check.outputs.branch-pr == '' uses: ./.github/workflows/_container.yml permissions: + contents: read packages: write docs: @@ -50,16 +51,14 @@ jobs: 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 - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - + release: if: github.ref_type == 'tag' needs: [dist, docs] diff --git a/.gitignore b/.gitignore index 2593ec75..0f33bf29 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ cov.xml # Sphinx documentation docs/_build/ +docs/_api # PyBuilder target/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a4cbf7b..60fc23f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: - id: check-added-large-files - id: check-yaml - id: check-merge-conflict + - id: end-of-file-fixer - repo: local hooks: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 66ad6324..933c580c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,4 +2,4 @@ "recommendations": [ "ms-vscode-remote.remote-containers", ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index df954ee5..2c5f015e 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}", @@ -14,6 +14,10 @@ "debug-test" ], "console": "integratedTerminal", + "env": { + // Enable break on exception when debugging tests (see: tests/conftest.py) + "PYTEST_RAISE": "1", + }, }, { "name": "Python (Current File)", @@ -35,4 +39,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index c129d991..101c75fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,8 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, + "files.insertFinalNewline": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", }, -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 946e69d4..c999e864 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 5be05df7..e0e38655 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ ENV PATH=/venv/bin:$PATH FROM developer as build COPY . /context WORKDIR /context -RUN pip install . +RUN touch dev-requirements.txt && pip install -c dev-requirements.txt . # The runtime stage copies the built venv into a slim runtime container FROM python:${PYTHON_VERSION}-slim as runtime diff --git a/README.md b/README.md index d2ab2de9..451a67ed 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ can be produced and expanded Paths created to consume chunk by chunk. Source | :---: | :---: PyPI | `pip install scanspec` +Docker | `docker run ghcr.io/bluesky/scanspec:latest` Documentation | Releases | diff --git a/catalog-info.yaml b/catalog-info.yaml index 2c93efd0..21cc4a14 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -5,6 +5,6 @@ metadata: title: scanspec description: Specify step and flyscan paths in a serializable, efficient and Pythonic way spec: - type: documentation + type: library lifecycle: experimental - owner: group:default/sscc \ No newline at end of file + owner: group:default/sscc diff --git a/docs/_api.rst b/docs/_api.rst new file mode 100644 index 00000000..b4b0a6c9 --- /dev/null +++ b/docs/_api.rst @@ -0,0 +1,16 @@ +:orphan: + +.. + This page is not included in the TOC tree, but must exist so that the + autosummary pages are generated for scanspec and all its + subpackages + +API +=== + +.. autosummary:: + :toctree: _api + :template: custom-module-template.rst + :recursive: + + scanspec diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst new file mode 100644 index 00000000..9aeca540 --- /dev/null +++ b/docs/_templates/custom-module-template.rst @@ -0,0 +1,37 @@ +{{ ('``' + fullname + '``') | underline }} + +{%- set filtered_members = [] %} +{%- for item in members %} + {%- if item in functions + classes + exceptions + attributes %} + {% set _ = filtered_members.append(item) %} + {%- endif %} +{%- endfor %} + +.. automodule:: {{ fullname }} + :members: + + {% block modules %} + {% if modules %} + .. rubric:: Submodules + + .. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: + {% for item in modules %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block members %} + {% if filtered_members %} + .. rubric:: Members + + .. autosummary:: + :nosignatures: + {% for item in filtered_members %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 33b043e4..de976947 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,9 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" import sys from pathlib import Path @@ -32,6 +33,8 @@ extensions = [ # Use this for generating API docs "sphinx.ext.autodoc", + # and making summary tables at the top of API docs + "sphinx.ext.autosummary", # This can parse google style docstrings "sphinx.ext.napoleon", # For linking to external sphinx documentation @@ -104,6 +107,12 @@ # Include source in plot directive by default plot_include_source = True +# Document only what is in __all__ +autosummary_ignore_module_all = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + # Output graphviz directive produced images in a scalable format graphviz_output_format = "svg" @@ -162,10 +171,10 @@ # Theme options for pydata_sphinx_theme # We don't check switcher because there are 3 possible states for a repo: # 1. New project, docs are not published so there is no switcher -# 2. Existing project with latest skeleton, switcher exists and works -# 3. Existing project with old skeleton that makes broken switcher, +# 2. Existing project with latest copier template, switcher exists and works +# 3. Existing project with old copier template that makes broken switcher, # switcher exists but is broken -# Point 3 makes checking switcher difficult, because the updated skeleton +# Point 3 makes checking switcher difficult, because the updated copier template # will fix the switcher at the end of the docs workflow, but never gets a chance # to complete as the docs build warns and fails. html_theme_options = { @@ -193,7 +202,7 @@ # A dictionary of values to pass into the template engine’s context for all pages html_context = { "github_user": github_user, - "github_repo": project, + "github_repo": github_repo, "github_version": version, "doc_path": "docs", } diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md index f9c4ca1d..6e419797 100644 --- a/docs/how-to/contribute.md +++ b/docs/how-to/contribute.md @@ -1,2 +1,2 @@ ```{include} ../../.github/CONTRIBUTING.md -``` \ No newline at end of file +``` diff --git a/docs/how-to/run-container.md b/docs/how-to/run-container.md index ab104335..8737adee 100644 --- a/docs/how-to/run-container.md +++ b/docs/how-to/run-container.md @@ -8,7 +8,7 @@ installed are available on [Github Container Registry](https://ghcr.io/bluesky/s To pull the container from github container registry and run: ``` -$ docker run ghcr.io/bluesky/scanspec:main --version +$ docker run ghcr.io/bluesky/scanspec:latest --version ``` -To get a released version, use a numbered release instead of `main`. +To get a released version, use a numbered release instead of `latest`. diff --git a/docs/reference.md b/docs/reference.md index ff85f567..e2ef404a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -6,6 +6,7 @@ Technical reference material including APIs and release notes. :maxdepth: 1 :glob: +API <_api/scanspec> reference/* genindex Release Notes diff --git a/docs/reference/api.md b/docs/reference/api.md deleted file mode 100644 index c4bab762..00000000 --- a/docs/reference/api.md +++ /dev/null @@ -1,73 +0,0 @@ -# API - -```{eval-rst} -.. automodule:: scanspec - - ``scanspec`` - ------------ - -``` - -The top level scanspec module contains a number of packages that can be used -from code: - -- [](#scanspec.core): Core classes like [](#Frames) and [](#Path) -- [](#scanspec.specs): [](#Spec) and its subclasses -- [](#scanspec.regions): [](#Region) and its subclasses -- [](#scanspec.plot): [](#plot_spec) to visualize a scan -- [](#scanspec.service): Defines queries and field structure in REST such as [](#MidpointsResponse) - -```{eval-rst} -.. data:: scanspec.__version__ - :type: str - - Version number as calculated by https://github.com/bluesky/versiongit -``` - -```{eval-rst} -.. automodule:: scanspec.core - :members: - - ``scanspec.core`` - ----------------- -``` - -```{eval-rst} -.. automodule:: scanspec.specs - :members: - - ``scanspec.specs`` - ------------------ - - .. inheritance-diagram:: scanspec.specs - :top-classes: scanspec.specs.Spec - :parts: 1 -``` - -```{eval-rst} -.. automodule:: scanspec.regions - :members: - - ``scanspec.regions`` - -------------------- - - .. inheritance-diagram:: scanspec.regions - :top-classes: scanspec.regions.Region - :parts: 1 -``` - -```{eval-rst} -.. automodule:: scanspec.plot - :members: - - ``scanspec.plot`` - ----------------- -``` - -```{eval-rst} -.. automodule:: scanspec.service - :members: - - ``scanspec.service`` - -------------------- -``` diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md index 592c9da3..b9c85bd1 100644 --- a/docs/tutorials/installation.md +++ b/docs/tutorials/installation.md @@ -2,7 +2,7 @@ ## Check your version of python -You will need python 3.8 or later. You can check your version of python by +You will need python 3.10 or later. You can check your version of python by typing into a terminal: ``` diff --git a/pyproject.toml b/pyproject.toml index c5b48619..f62469ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2"] +requires = ["setuptools>=64", "setuptools_scm[toml]>=8"] build-backend = "setuptools.build_meta" [project] @@ -9,6 +9,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] description = "Specify step and flyscan paths in a serializable, efficient and Pythonic way" dependencies = ["numpy", "click>=8.1", "pydantic>=2.0"] @@ -59,11 +60,11 @@ name = "Tom Cobb" [tool.setuptools_scm] -write_to = "src/scanspec/_version.py" +version_file = "src/scanspec/_version.py" [tool.pyright] -# strict = ["src", "tests"] -reportMissingImports = false # Ignore missing stubs in imported modules +typeCheckingMode = "standard" +reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error @@ -100,7 +101,7 @@ allowlist_externals = sphinx-build sphinx-autobuild commands = - pre-commit: pre-commit run --all-files {posargs} + pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs} type-checking: pyright src tests {posargs} tests: pytest --cov=scanspec --cov-report term --cov-report xml:cov.xml {posargs} docs: sphinx-{posargs:build -E --keep-going} -T docs build/html diff --git a/src/scanspec/__init__.py b/src/scanspec/__init__.py index b99257eb..4ab90134 100644 --- a/src/scanspec/__init__.py +++ b/src/scanspec/__init__.py @@ -1,3 +1,11 @@ +"""Top level API. + +.. data:: __version__ + :type: str + + Version number as calculated by https://github.com/pypa/setuptools_scm +""" + from . import regions, specs from ._version import __version__ diff --git a/src/scanspec/__main__.py b/src/scanspec/__main__.py index cd9cfea2..633c3d88 100644 --- a/src/scanspec/__main__.py +++ b/src/scanspec/__main__.py @@ -1,5 +1,4 @@ from scanspec import cli -# test with: python -m scanspec if __name__ == "__main__": cli.cli() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..ebe9c10f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import os +from typing import Any + +import pytest + +# 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: pytest.CallInfo[Any]): + if call.excinfo is not None: + raise call.excinfo.value + else: + raise RuntimeError( + f"{call} has no exception data, an unknown error has occurred" + ) + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]): + raise excinfo.value