diff --git a/.cruft.json b/.cruft.json index ae416b57..6c79e228 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,7 +1,7 @@ { "template": "https://github.com/scverse/cookiecutter-scverse", - "commit": "87a407a65408d75a949c0b54b19fd287475a56f8", - "checkout": "v0.4.0", + "commit": "6ff5b92b5d44ea6d8a88e47538475718d467db95", + "checkout": "v0.7.0", "context": { "cookiecutter": { "project_name": "spatialdata-plot", @@ -10,19 +10,33 @@ "author_full_name": "scverse", "author_email": "scverse", "github_user": "scverse", - "project_repo": "https://github.com/scverse/spatialdata-plot", + "github_repo": "spatialdata-plot", "license": "BSD 3-Clause License", + "ide_integration": false, "_copy_without_render": [ ".github/workflows/build.yaml", ".github/workflows/test.yaml", "docs/_templates/autosummary/**.rst" ], + "_exclude_on_template_update": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "docs/api.md", + "docs/index.md", + "docs/notebooks/example.ipynb", + "docs/references.bib", + "docs/references.md", + "src/**", + "tests/**" + ], "_render_devdocs": false, "_jinja2_env_vars": { "lstrip_blocks": true, "trim_blocks": true }, - "_template": "https://github.com/scverse/cookiecutter-scverse" + "_template": "https://github.com/scverse/cookiecutter-scverse", + "_commit": "6ff5b92b5d44ea6d8a88e47538475718d467db95" } }, "directory": null diff --git a/.editorconfig b/.editorconfig index 050f9118..66678e37 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,10 +8,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{yml,yaml}] -indent_size = 2 - -[.cruft.json] +[{*.{yml,yaml,toml},.cruft.json}] indent_size = 2 [Makefile] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..33720e0b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: Bug report +description: Report something that is broken or incorrect +type: Bug +body: + - type: markdown + attributes: + value: | + **Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) + detailing how to provide the necessary information for us to reproduce your bug. In brief: + * Please provide exact steps how to reproduce the bug in a clean Python environment. + * In case it's not clear what's causing this bug, please provide the data or the data generation procedure. + * Replicate problems on public datasets or share data subsets when full sharing isn't possible. + + - type: textarea + id: report + attributes: + label: Report + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: versions + attributes: + label: Versions + description: | + Which version of packages. + + Please install `session-info2`, run the following command in a notebook, + click the "Copy as Markdown" button, then paste the results into the text box below. + + ```python + In[1]: import session_info2; session_info2.session_info(dependencies=True) + ``` + + Alternatively, run this in a console: + + ```python + >>> import session_info2; print(session_info2.session_info(dependencies=True)._repr_mimebundle_()["text/markdown"]) + ``` + render: python diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..fed9c64f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Scverse Community Forum + url: https://discourse.scverse.org/ + about: If you have questions about "How to do X", please ask them here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..104fd852 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,11 @@ +name: Feature request +description: Propose a new feature for spatialdata-plot +type: Enhancement +body: + - type: textarea + id: description + attributes: + label: Description of feature + description: Please describe your suggestion for a new feature. It might help to describe a problem or use case, plus any alternatives that you have considered. + validations: + required: true diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..c6ecc2f2 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,26 @@ +name: Check Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Build package + run: uv build + - name: Check package + run: uvx twine check --strict dist/*.whl diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9bd6c545..b55b308a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,28 +4,43 @@ on: release: types: [published] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - package_and_release: + build: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 with: - python-version: "3.12" - cache: pip - - name: Install build dependencies - run: python -m pip install --upgrade pip wheel twine build + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Build package - run: python -m build + run: uv build - name: Check package - run: twine check --strict dist/*.whl - - name: Install hatch - run: pip install hatch - - name: Build project for distribution - run: hatch build - - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + run: uvx twine check --strict dist/* + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: - password: ${{ secrets.PYPI_API_TOKEN }} + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write # for PyPI trusted publishing + environment: + name: pypi + url: https://pypi.org/p/spatialdata-plot + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e4c8ef13..7d00e8a3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,73 +3,112 @@ name: Test on: push: branches: [main] - tags: ["v*"] # Push events to matching v*, i.e. v1.0, v20.15.10 + tags: ["v*"] pull_request: - branches: ["*"] + branches: [main] + schedule: + - cron: "0 5 1,15 * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MPLBACKEND: agg jobs: - test: + # Dynamically extract the test matrix from hatch so pyproject.toml is the single source of truth. + # See [[tool.hatch.envs.hatch-test.matrix]] in pyproject.toml. + get-environments: runs-on: ubuntu-latest + outputs: + envs: ${{ steps.get-envs.outputs.envs }} + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Get test environments + id: get-envs + run: | + ENVS_JSON=$(uvx hatch env show --json | jq -c 'to_entries + | map( + select(.key | startswith("hatch-test")) + | { + name: .key, + label: (if (.key | contains("pre")) then .key + " (PRE-RELEASE DEPENDENCIES)" else .key end), + python: .value.python + } + )') + echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT + + test: + needs: get-environments + permissions: + id-token: write # for codecov OIDC + strategy: fail-fast: false matrix: - env: ["dev-py311", "dev-py313"] + os: [ubuntu-latest] + env: ${{ fromJSON(needs.get-environments.outputs.envs) }} - # Configure pytest-xdist - env: - OMP_NUM_THREADS: "1" - OPENBLAS_NUM_THREADS: "1" - MKL_NUM_THREADS: "1" - NUMEXPR_MAX_THREADS: "1" - MPLBACKEND: "agg" - DISPLAY: ":42" - PYTEST_ADDOPTS: "-n auto --dist=load --durations=10" + name: ${{ matrix.env.label }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ contains(matrix.env.name, 'pre') }} steps: - - uses: actions/checkout@v4 - - # Cache rattler's shared package cache (speeds up downloads) - - name: Restore rattler cache - uses: actions/cache@v4 + - uses: actions/checkout@v5 with: - path: ~/.cache/rattler - key: rattler-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - rattler-${{ runner.os }}-${{ matrix.env }}- - rattler-${{ runner.os }}- - - # Install pixi and the requested environment - - uses: prefix-dev/setup-pixi@v0.9.0 + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - environments: ${{ matrix.env }} - # We're not comitting the pixi-lock file - locked: false - cache: false - activate-environment: ${{ matrix.env }} - - - name: Show versions - run: | - python --version - pixi --version - + python-version: ${{ matrix.env.python }} + - name: Ensure figure directory exists + run: mkdir -p tests/figures + - name: Create hatch environment + run: uvx hatch env create ${{ matrix.env.name }} - name: Run tests env: - MPLBACKEND: agg DISPLAY: ":42" + run: >- + uvx hatch run ${{ matrix.env.name }}:${{ + matrix.env.name == 'hatch-test.py3.14-stable' && 'run-cov' || 'run' + }} -v --color=yes ${{ + matrix.env.name == 'hatch-test.py3.14-stable' && ' ' || '-n auto' + }} + - name: Generate coverage report + if: matrix.env.name == 'hatch-test.py3.14-stable' run: | - pytest -v --cov --color=yes --cov-report=xml - - - name: Archive figures generated during testing + test -f .coverage || uvx hatch run ${{ matrix.env.name }}:cov-combine + uvx hatch run ${{ matrix.env.name }}:cov-report + - name: Archive visual test figures if: always() uses: actions/upload-artifact@v4 with: - name: visual_test_results_${{ matrix.env }} - path: /home/runner/work/spatialdata-plot/spatialdata-plot/tests/figures/* + name: visual_test_results_${{ matrix.env.name }} + path: tests/figures/* + - name: Upload coverage + if: matrix.env.name == 'hatch-test.py3.14-stable' + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + use_oidc: true - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + # Single required check for branch protection. + # See https://github.com/re-actors/alls-green#why + check: + name: Tests pass in all hatch environments + if: always() + needs: + - get-environments + - test + runs-on: ubuntu-latest + steps: + - uses: re-actors/alls-green@release/v1 with: - name: coverage - verbose: true - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index 02a43f70..da2f0211 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ buck-out/ # Compiled files .venv/ __pycache__/ -.mypy_cache/ -.ruff_cache/ +.*cache/ /node_modules # Distribution / packaging @@ -16,7 +15,6 @@ __pycache__/ /*.egg-info/ # Tests and coverage -/.pytest_cache/ /.cache/ /data/ *failed-diff.png @@ -31,7 +29,6 @@ __pycache__/ format.sh - # test tests/figures/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c8aa64c..6582aa2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,19 +5,36 @@ default_stages: - pre-commit - pre-push minimum_pre_commit_version: 2.16.0 -ci: - skip: [] repos: - - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.8.1 + - repo: https://github.com/biomejs/pre-commit + rev: v2.3.10 hooks: - - id: prettier + - id: biome-format + exclude: ^\.cruft\.json$ # inconsistent indentation with cruft + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.11.1 + hooks: + - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.5 hooks: - - id: ruff + - id: ruff-check + types_or: [python, pyi, jupyter] args: [--fix, --exit-non-zero-on-fix] - id: ruff-format + types_or: [python, pyi, jupyter] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: detect-private-key + - id: check-ast + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: trailing-whitespace + - id: check-case-conflict + - id: check-merge-conflict + args: [--assume-in-merge] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.19.1 hooks: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dd21449c..cfd291d3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,15 +1,17 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-24.04 tools: - python: "3.12" - commands: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - - uv venv - - uv pip install .[docs,pre] - - .venv/bin/python -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html -sphinx: - configuration: docs/conf.py - fail_on_warning: false + python: "3.13" + nodejs: latest + jobs: + create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + build: + html: + # TODO: remove "--with=virtualenv<21" once hatch is compatible with virtualenv 21+ (pypa/hatch#2193) + - uvx "--with=virtualenv<21" hatch run docs:build + - mv docs/_build $READTHEDOCS_OUTPUT diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 00000000..9f8f2208 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "formatter": { "useEditorconfig": true }, + "overrides": [ + { + "includes": ["./.vscode/*.json", "**/*.jsonc"], + "json": { + "formatter": { "trailingCommas": "all" }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true, + }, + }, + }, + ], +} diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 0e22bef4..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -clean: - rm -r "$(BUILDDIR)" diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst index ee6d05f5..7b4a0cf8 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -9,14 +9,11 @@ {% block attributes %} {% if attributes %} Attributes table -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ .. autosummary:: - {% for item in attributes %} - - ~{{ fullname }}.{{ item }} - + ~{{ name }}.{{ item }} {%- endfor %} {% endif %} {% endblock %} @@ -27,13 +24,10 @@ Methods table ~~~~~~~~~~~~~ .. autosummary:: - {% for item in methods %} - {%- if item != '__init__' %} - ~{{ fullname }}.{{ item }} + ~{{ name }}.{{ item }} {%- endif -%} - {%- endfor %} {% endif %} {% endblock %} @@ -41,15 +35,11 @@ Methods table {% block attributes_documentation %} {% if attributes %} Attributes -~~~~~~~~~~~ +~~~~~~~~~~ {% for item in attributes %} -{{ item }} -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. autoattribute:: {{ [objname, item] | join(".") }} - {%- endfor %} {% endif %} @@ -63,11 +53,7 @@ Methods {% for item in methods %} {%- if item != '__init__' %} -{{ item }} -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. automethod:: {{ [objname, item] | join(".") }} - {%- endif -%} {%- endfor %} diff --git a/docs/conf.py b/docs/conf.py index d1573a31..3e7591a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,11 +5,14 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- +import shutil import sys from datetime import datetime from importlib.metadata import metadata from pathlib import Path +from sphinxcontrib import katex + HERE = Path(__file__).parent sys.path.insert(0, str(HERE / "extensions")) @@ -19,13 +22,12 @@ # NOTE: If you installed your project in editable mode, this might be stale. # If this is the case, reinstall it to refresh the metadata info = metadata("spatialdata-plot") -project_name = info["Name"] +project = info["Name"] author = info["Author"] -copyright = f"{datetime.now():%Y}, {author}" +copyright = f"{datetime.now():%Y}, {author}." version = info["Version"] - -# repository_url = f"https://github.com/scverse/{project_name}" - +urls = dict(pu.split(", ") for pu in info.get_all("Project-URL")) +repository_url = urls["Source"] # The full version, including alpha/beta/rc tags release = info["Version"] @@ -38,7 +40,7 @@ html_context = { "display_github": True, # Integrate GitHub "github_user": "scverse", - "github_repo": "https://github.com/scverse/spatialdata-plot", + "github_repo": project, "github_version": "main", "conf_py_path": "/docs/", } @@ -55,15 +57,15 @@ "sphinx.ext.autosummary", "sphinx.ext.napoleon", "sphinxcontrib.bibtex", + "sphinxcontrib.katex", "sphinx_autodoc_typehints", - "sphinx.ext.mathjax", + "sphinx_tabs.tabs", "IPython.sphinxext.ipython_console_highlighting", - "sphinx_design", + "sphinxext.opengraph", *[p.stem for p in (HERE / "extensions").glob("*.py")], ] autosummary_generate = True -autodoc_process_signature = True autodoc_member_order = "groupwise" default_role = "literal" napoleon_google_docstring = False @@ -71,7 +73,7 @@ napoleon_include_init_with_doc = False napoleon_use_rtype = True # having a separate entry generally helps readability napoleon_use_param = True -myst_heading_anchors = 3 # create anchors for h1-h3 +myst_heading_anchors = 6 # create anchors for h1-h6 myst_enable_extensions = [ "amsmath", "colon_fence", @@ -93,7 +95,9 @@ } intersphinx_mapping = { + "python": ("https://docs.python.org/3.13", None), "anndata": ("https://anndata.readthedocs.io/en/stable/", None), + "scanpy": ("https://scanpy.readthedocs.io/en/stable/", None), "numpy": ("https://numpy.org/doc/stable/", None), "geopandas": ("https://geopandas.org/en/stable/", None), "xarray": ("https://docs.xarray.dev/en/stable/", None), @@ -108,18 +112,13 @@ exclude_patterns = [ "_build", "Thumbs.db", + ".DS_Store", "**.ipynb_checkpoints", "tutorials/notebooks/index.md", "tutorials/notebooks/README.md", "tutorials/notebooks/references.md", "tutorials/notebooks/notebooks/paper_reproducibility/*", ] -# Ignore warnings. -nitpicky = False # TODO: solve upstream. -# nitpick_ignore = [ -# ("py:class", "spatial_image.SpatialImage"), -# ("py:class", "multiscale_spatial_image.multiscale_spatial_image.MultiscaleSpatialImage"), -# ] # -- Options for HTML output ------------------------------------------------- @@ -128,36 +127,33 @@ # a list of builtin themes. # html_theme = "sphinx_book_theme" -# html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] -html_title = project_name +html_css_files = ["css/custom.css"] +html_title = project html_logo = "_static/img/spatialdata_horizontal.png" -# html_theme_options = { -# "repository_url": repository_url, -# "use_repository_button": True, -# } +html_theme_options = { + "repository_url": repository_url, + "use_repository_button": True, + "path_to_docs": "docs/", + "navigation_with_keys": False, +} pygments_style = "default" +katex_prerender = shutil.which(katex.NODEJS_BINARY) is not None + +suppress_warnings = [ + # matplotlib.figure.Figure references ColorType which isn't importable in the docs env + "sphinx_autodoc_typehints.forward_reference", + # xarray (iris) and geopandas (folium) have guarded TYPE_CHECKING imports for optional deps + "sphinx_autodoc_typehints.guarded_import", +] + +# Ignore warnings. +nitpicky = False # TODO: solve upstream. nitpick_ignore = [ # If building the documentation fails because of a missing link that is outside your control, # you can add an exception to this list. ("py:class", "igraph.Graph"), ] - - -def setup(app): - """App setup hook.""" - app.add_config_value( - "recommonmark_config", - { - "auto_toc_tree_section": "Contents", - "enable_auto_toc_tree": True, - "enable_math": True, - "enable_inline_math": False, - "enable_eval_rst": True, - }, - True, - ) - app.add_css_file("css/custom.css") diff --git a/docs/contributing.md b/docs/contributing.md index e7f1ab36..d8b8f1e5 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,8 +1,201 @@ # Contributing guide -Please refer to the [contribution guide from the `spatialdata` repository](https://github.com/scverse/spatialdata/blob/main/docs/contributing.md). +This document aims at summarizing the most important information for getting you started on contributing to this project. +We assume that you are already familiar with git and with making pull requests on GitHub. -## Testing the correctness of the plots +For more extensive tutorials, that also cover the absolute basics, +please refer to other resources such as the [pyopensci tutorials][], +the [scientific Python tutorials][], or the [scanpy developer guide][]. + +[pyopensci tutorials]: https://www.pyopensci.org/learn.html +[scientific Python tutorials]: https://learn.scientific-python.org/development/tutorials/ +[scanpy developer guide]: https://scanpy.readthedocs.io/en/latest/dev/index.html + +:::{tip} The *hatch* project manager + +We highly recommend to familiarize yourself with [`hatch`][hatch]. +Hatch is a Python project manager that + +- manages virtual environments, separately for development, testing and building the documentation. + Separating the environments is useful to avoid dependency conflicts. +- allows to run tests locally in different environments (e.g. different python versions) +- allows to run tasks defined in `pyproject.toml`, e.g. to build documentation. + +While the project is setup with `hatch` in mind, +it is still possible to use different tools to manage dependencies, such as `uv` or `pip`. + +::: + +[hatch]: https://hatch.pypa.io/latest/ + +## Installing dev dependencies + +In addition to the packages needed to _use_ this package, +you need additional python packages to [run tests](#writing-tests) and [build the documentation](#docs-building). + +:::::{tabs} +::::{group-tab} Hatch + +On the command line, you typically interact with hatch through its command line interface (CLI). +Running one of the following commands will automatically resolve the environments for testing and +building the documentation in the background: + +```bash +hatch test # defined in the table [tool.hatch.envs.hatch-test] in pyproject.toml +hatch run docs:build # defined in the table [tool.hatch.envs.docs] +``` + +When using an IDE such as VS Code, +you'll have to point the editor at the paths to the virtual environments manually. +The environment you typically want to use as your main development environment is the `hatch-test` +environment with the latest Python version. + +To get a list of all environments for your projects, run + +```bash +hatch env show -i +``` + +From the `Envs` column, select the environment name you want to use for development. + +Next, create the environment with + +```bash +hatch env create +``` + +Then, obtain the path to the environment using + +```bash +hatch env find +``` + +In case you are using VScode, now open the command palette (Ctrl+Shift+P) and search for `Python: Select Interpreter`. +Choose `Enter Interpreter Path` and paste the path to the virtual environment from above. + +:::: + +::::{group-tab} uv + +A popular choice for managing virtual environments is [uv][]. +The main disadvantage compared to hatch is that it supports only a single environment per project at a time, +which requires you to mix the dependencies for running tests and building docs. + +To initalize a virtual environment in the `.venv` directory of your project, simply run + +```bash +uv sync --all-extras +``` + +The `.venv` directory is typically automatically discovered by IDEs such as VS Code. + +:::: + +::::{group-tab} Pip + +Pip is nowadays mostly superseded by environment managers such as [hatch][]. +However, for the sake of completeness, and since it's ubiquitously available, +we describe how you can manage environments manually using `pip`: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[dev,test,doc]" +``` + +The `.venv` directory is typically automatically discovered by IDEs such as VS Code. + +:::: +::::: + +[hatch environments]: https://hatch.pypa.io/latest/tutorials/environment/basic-usage/ +[uv]: https://docs.astral.sh/uv/ + +## Code-style + +This package uses [pre-commit][] to enforce consistent code-styles. +On every commit, pre-commit checks will either automatically fix issues with the code, or raise an error message. + +To enable pre-commit locally, simply run + +```bash +pre-commit install +``` + +in the root of the repository. +Pre-commit will automatically download all dependencies when it is run for the first time. + +Alternatively, you can rely on the [pre-commit.ci][] service enabled on GitHub. +If you didn't run `pre-commit` before pushing changes to GitHub it will automatically commit fixes to your pull request, or show an error message. + +If pre-commit.ci added a commit on a branch you still have been working on locally, simply use + +```bash +git pull --rebase +``` + +to integrate the changes into yours. +While the [pre-commit.ci][] is useful, we strongly encourage installing and running pre-commit locally first to understand its usage. + +Finally, most editors have an _autoformat on save_ feature. +Consider enabling this option for [ruff][ruff-editors] and [biome][biome-editors]. + +[pre-commit]: https://pre-commit.com/ +[pre-commit.ci]: https://pre-commit.ci/ +[ruff-editors]: https://docs.astral.sh/ruff/integrations/ +[biome-editors]: https://biomejs.dev/guides/integrate-in-editor/ + +(writing-tests)= + +## Writing tests + +This package uses [pytest][] for automated testing. +Please write {doc}`scanpy:dev/testing` for every function added to the package. + +Most IDEs integrate with pytest and provide a GUI to run tests. +Just point yours to one of the environments returned by + +```bash +hatch env create hatch-test # create test environments for all supported versions +hatch env find hatch-test # list all possible test environment paths +``` + +Alternatively, you can run all tests from the command line by executing + +:::::{tabs} +::::{group-tab} Hatch + +```bash +hatch test # test with the highest supported Python version +# or +hatch test --all # test with all supported Python versions +``` + +:::: + +::::{group-tab} uv + +```bash +uv run pytest +``` + +:::: + +::::{group-tab} Pip + +```bash +source .venv/bin/activate +pytest +``` + +:::: +::::: + +in the root of the repository. + +[pytest]: https://docs.pytest.org/ + +### Testing the correctness of the plots Many tests will produce plots and check that they are correct by comparing them with a previously saved and serialized version of the same plots. The ground truth images are located in `tests/_images`. Different OS/versions may produce similar but not identical plots (for instance the ticks/padding could vary). To take into account for this please consider the following: @@ -11,4 +204,116 @@ Many tests will produce plots and check that they are correct by comparing them - please never replace the ground truth images without having manually reviewed them. - if you run the tests locally in macOS or Windows they will likely fail because the ground truth images are generated using Ubuntu. To overcome this you can use `act`, which will generate a Docker reproducing the environment used in the GitHub Action. After the Docker container is generated you can use it within IDEs to run tests and debug code. - in the case of PyCharm, it is easier to create a container from a `Dockerfile` instead of using `act`. Please in such case use the `Dockerfile` made available in the repository. In this [thread](https://github.com/scverse/spatialdata-plot/pull/397) you can find extra details, and the process is shown [in this Loom recording](https://www.loom.com/share/172e309e5803419bb3b3107eee1f3a4e). -- If you encountering problems with `act` or `docker`, please [get in touch with the developers via Zulip](https://scverse.zulipchat.com/#narrow/channel/443514-spatialdata-dev) and we will help troubleshoot the issue. +- If you are encountering problems with `act` or `docker`, please [get in touch with the developers via Zulip](https://scverse.zulipchat.com/#narrow/channel/443514-spatialdata-dev) and we will help troubleshoot the issue. + +### Continuous integration + +Continuous integration via GitHub actions will automatically run the tests on all pull requests and test +against the minimum and maximum supported Python version. + +Additionally, there's a CI job that tests against pre-releases of all dependencies (if there are any). +The purpose of this check is to detect incompatibilities of new package versions early on and +gives you time to fix the issue or reach out to the developers of the dependency before the package +is released to a wider audience. + +The CI job is defined in `.github/workflows/test.yaml`, +however the single point of truth for CI jobs is the Hatch test matrix defined in `pyproject.toml`. +This means that local testing via hatch and remote testing on CI tests against the same python versions and uses the same environments. + +## Publishing a release + +### Updating the version number + +Before making a release, you need to update the version number in the `pyproject.toml` file. +Please adhere to [Semantic Versioning][semver], in brief + +> Given a version number MAJOR.MINOR.PATCH, increment the: +> +> 1. MAJOR version when you make incompatible API changes, +> 2. MINOR version when you add functionality in a backwards compatible manner, and +> 3. PATCH version when you make backwards compatible bug fixes. +> +> Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + +Once you are done, commit and push your changes and navigate to the "Releases" page of this project on GitHub. +Specify `vX.X.X` as a tag name and create a release. +For more information, see [managing GitHub releases][]. +This will automatically create a git tag and trigger a Github workflow that creates a release on [PyPI][]. + +[semver]: https://semver.org/ +[managing GitHub releases]: https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository +[pypi]: https://pypi.org/ + +## Writing documentation + +Please write documentation for new or changed features and use-cases. +This project uses [sphinx][] with the following features: + +- The [myst][] extension allows to write documentation in markdown/Markedly Structured Text +- [Numpy-style docstrings][numpydoc] (through the [napoleon][numpydoc-napoleon] extension). +- Jupyter notebooks as tutorials through [myst-nb][] (See [Tutorials with myst-nb](#tutorials-with-myst-nb-and-jupyter-notebooks)) +- [sphinx-autodoc-typehints][], to automatically reference annotated input and output types +- Citations (like {cite:p}`Virshup_2023`) can be included with [sphinxcontrib-bibtex](https://sphinxcontrib-bibtex.readthedocs.io/) + +See scanpy's {doc}`scanpy:dev/documentation` for more information on how to write your own. + +[sphinx]: https://www.sphinx-doc.org/en/master/ +[myst]: https://myst-parser.readthedocs.io/en/latest/intro.html +[myst-nb]: https://myst-nb.readthedocs.io/en/latest/ +[numpydoc-napoleon]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html +[numpydoc]: https://numpydoc.readthedocs.io/en/latest/format.html +[sphinx-autodoc-typehints]: https://github.com/tox-dev/sphinx-autodoc-typehints + +### Tutorials with myst-nb and jupyter notebooks + +The documentation is set-up to render jupyter notebooks stored in the `docs/notebooks` directory using [myst-nb][]. +Currently, only notebooks in `.ipynb` format are supported that will be included with both their input and output cells. +It is your responsibility to update and re-run the notebook whenever necessary. + +If you are interested in automatically running notebooks as part of the continuous integration, +please check out [this feature request][issue-render-notebooks] in the `cookiecutter-scverse` repository. + +[issue-render-notebooks]: https://github.com/scverse/cookiecutter-scverse/issues/40 + +#### Hints + +- If you refer to objects from other packages, please add an entry to `intersphinx_mapping` in `docs/conf.py`. + Only if you do so can sphinx automatically create a link to the external documentation. +- If building the documentation fails because of a missing link that is outside your control, + you can add an entry to the `nitpick_ignore` list in `docs/conf.py` + +(docs-building)= + +### Building the docs locally + +:::::{tabs} +::::{group-tab} Hatch + +```bash +hatch run docs:build +hatch run docs:open +``` + +:::: + +::::{group-tab} uv + +```bash +cd docs +uv run sphinx-build -M html . _build -W +(xdg-)open _build/html/index.html +``` + +:::: + +::::{group-tab} Pip + +```bash +source .venv/bin/activate +cd docs +sphinx-build -M html . _build -W +(xdg-)open _build/html/index.html +``` + +:::: +::::: diff --git a/pyproject.toml b/pyproject.toml index dd7b9633..32da5206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,89 +1,69 @@ [build-system] build-backend = "hatchling.build" -requires = ["hatchling", "hatch-vcs"] +requires = [ "hatch-vcs", "hatchling" ] [project] name = "spatialdata-plot" description = "Static plotting for spatial data." -authors = [ - {name = "scverse"}, -] +readme = "README.md" +license = { file = "LICENSE" } maintainers = [ - {name = "Tim Treis", email = "tim.treis@helmholtz-munich.de"}, + { name = "Tim Treis", email = "tim.treis@helmholtz-munich.de" }, +] +authors = [ + { name = "scverse" }, ] -urls.Documentation = "https://spatialdata.scverse.org/projects/plot/en/latest/index.html" -urls.Source = "https://github.com/scverse/spatialdata-plot.git" -urls.Home-page = "https://github.com/scverse/spatialdata-plot.git" requires-python = ">=3.11" -dynamic= [ - "version" # allow version to be set by git tags +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dynamic = [ + "version", # allow version to be set by git tags ] -license = {file = "LICENSE"} -readme = "README.md" dependencies = [ - "spatialdata>=0.3.0", - "matplotlib", - "scikit-learn", - "scanpy", - "matplotlib_scalebar", + "matplotlib", + "matplotlib-scalebar", + "scanpy", + "scikit-learn", + "spatialdata>=0.3", ] -[project.optional-dependencies] +urls.Documentation = "https://spatialdata.scverse.org/projects/plot/en/latest/index.html" +urls.Home-page = "https://github.com/scverse/spatialdata-plot.git" +urls.Source = "https://github.com/scverse/spatialdata-plot.git" + +[dependency-groups] dev = [ - "jupyterlab", - "notebook", - "ipykernel", - "ipywidgets", - "jupytext", - "pytest", - "pytest-cov", - "pooch", - "ruff", - "pre-commit", -] -docs = [ - "sphinx>=4.5", - "sphinx-book-theme>=1.0.0", - "sphinx_rtd_theme", - "myst-nb", - "sphinxcontrib-bibtex>=1.0.0", - "sphinx-autodoc-typehints", - "sphinx-design", - # For notebooks - "ipython>=8.6.0", - "sphinx-copybutton", + "pre-commit", + "twine>=4.0.2", ] test = [ - "pytest", - "pytest-cov", - "pytest-xdist", - "pooch", # for scipy.datasets module -] - -[tool.coverage.run] -source = ["spatialdata_plot"] -omit = [ - "**/test_*.py", -] - -[tool.pytest.ini_options] -testpaths = ["tests"] -xfail_strict = true -addopts = [ -# "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below - "--import-mode=importlib", # allow using test files with same name - "-s" # print output from tests + "coverage[toml]>=7.4", + "pooch", + "pytest", + "pytest-cov", + "pytest-xdist[psutil]", ] -# info on how to use this https://stackoverflow.com/questions/57925071/how-do-i-avoid-getting-deprecationwarning-from-inside-dependencies-with-pytest -filterwarnings = [ - # "ignore:.*U.*mode is deprecated:DeprecationWarning", +doc = [ + "ipykernel", + "ipython>=8.6", + "myst-nb>=1.1", + "sphinx>=8.1", + "sphinx-autodoc-typehints", + "sphinx-book-theme>=1", + "sphinx-copybutton", + "sphinx-tabs", + "sphinxcontrib-bibtex>=1", + "sphinxcontrib-katex", + "sphinxext-opengraph", ] -[tool.jupytext] -formats = "ipynb,md" - [tool.hatch.build.targets.wheel] -packages = ['src/spatialdata_plot'] +packages = [ 'src/spatialdata_plot' ] [tool.hatch.version] source = "vcs" @@ -94,70 +74,144 @@ version-file = "_version.py" [tool.hatch.metadata] allow-direct-references = true +[tool.hatch.envs.default] +installer = "uv" +dependency-groups = [ "dev" ] + +[tool.hatch.envs.docs] +dependency-groups = [ "doc" ] + +[tool.hatch.envs.docs.scripts] +build = "sphinx-build -M html docs docs/_build -W {args}" +open = "python -m webbrowser -t docs/_build/html/index.html" +clean = "git clean -fdX -- {args:docs}" + +[tool.hatch.envs.hatch-test] +dependency-groups = [ "test" ] + +[tool.hatch.envs.hatch-test.scripts] +run = "pytest{env:HATCH_TEST_ARGS:} -p no:cov {args}" +run-cov = "coverage run -m pytest{env:HATCH_TEST_ARGS:} -p no:cov {args}" +cov-combine = "coverage combine" +cov-report = [ "coverage report", "coverage xml -o coverage.xml" ] + +[[tool.hatch.envs.hatch-test.matrix]] +deps = [ "stable" ] +python = [ "3.11", "3.14" ] + +[[tool.hatch.envs.hatch-test.matrix]] +deps = [ "pre" ] +python = [ "3.14" ] + +[tool.hatch.envs.hatch-test.overrides] +matrix.deps.env-vars = [ + { key = "UV_PRERELEASE", value = "allow", if = [ "pre" ] }, +] + [tool.ruff] line-length = 120 exclude = [ - ".git", - ".tox", - "__pycache__", - "build", - "docs/_build", - "dist", - "setup.py", -] -[tool.ruff.lint] -ignore = [ - # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient - "E731", - # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation - "E741", - # Missing docstring in public package - "D104", - # Missing docstring in public module - "D100", - # Missing docstring in __init__ - "D107", - # Missing docstring in magic method - "D105", - # Do not perform function calls in argument defaults. - "B008", - # Missing docstring in magic method - "D105", -] -select = [ - "D", # flake8-docstrings - "I", # isort - "E", # pycodestyle - "F", # pyflakes - "W", # pycodestyle - "Q", # flake8-quotes - "SIM", # flake8-simplify - "TID", # flake-8-tidy-imports - "NPY", # NumPy-specific rules - "PT", # flake8-pytest-style - "B", # flake8-bugbear - "UP", # pyupgrade - "C4", # flake8-comprehensions - "BLE", # flake8-blind-except - "T20", # flake8-print - "RET", # flake8-raise - "PGH", # pygrep-hooks -] -unfixable = ["B", "UP", "C4", "BLE", "T20", "RET"] - -[tool.ruff.lint.per-file-ignores] - "tests/*" = ["D", "PT", "B024"] - "*/__init__.py" = ["F401", "D104", "D107", "E402"] - "docs/*" = ["D","B","E","A"] - "tests/conftest.py"= ["E402", "RET504"] - "src/spatialdata_plot/pl/utils.py"= ["PGH003"] - -[tool.ruff.lint.pydocstyle] -convention = "numpy" + ".git", + ".tox", + "__pycache__", + "build", + "dist", + "docs/_build", + "setup.py", +] +lint.select = [ + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "D", # flake8-docstrings + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "NPY", # NumPy-specific rules + "PGH", # pygrep-hooks + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RET", # flake8-raise + "SIM", # flake8-simplify + "T20", # flake8-print + "TID", # flake-8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle +] +lint.ignore = [ + # Do not perform function calls in argument defaults. + "B008", + # Missing docstring in public module + "D100", + # Missing docstring in public package + "D104", + # Missing docstring in magic method + "D105", + # Missing docstring in __init__ + "D107", + # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient + "E731", + # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation + "E741", +] + +lint.per-file-ignores."*/__init__.py" = [ "D104", "D107", "E402", "F401" ] + +lint.per-file-ignores."docs/*" = [ "A", "B", "D", "E" ] + +lint.per-file-ignores."src/spatialdata_plot/pl/utils.py" = [ "PGH003" ] + +lint.per-file-ignores."tests/*" = [ "B024", "D", "PT" ] + +lint.per-file-ignores."tests/conftest.py" = [ "E402", "RET504" ] +lint.unfixable = [ "B", "BLE", "C4", "RET", "T20", "UP" ] +lint.pydocstyle.convention = "numpy" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +xfail_strict = true +addopts = [ + # "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below + "--import-mode=importlib", # allow using test files with same name + "-s", # print output from tests +] +# info on how to use this https://stackoverflow.com/questions/57925071/how-do-i-avoid-getting-deprecationwarning-from-inside-dependencies-with-pytest +filterwarnings = [ + # "ignore:.*U.*mode is deprecated:DeprecationWarning", +] + +[tool.coverage.run] +source = [ "spatialdata_plot" ] +branch = true +parallel = true +omit = [ + "**/test_*.py", +] + +[tool.coverage.paths] +source = [ + "src/spatialdata_plot", + "*/site-packages/spatialdata_plot", +] + +[tool.jupytext] +formats = "ipynb,md" + +[tool.cruft] +skip = [ + "tests", + "src/**/__init__.py", + "src/**/basic.py", + "docs/api.md", + "docs/changelog.md", + "docs/references.bib", + "docs/references.md", + "docs/notebooks/example.ipynb", +] [tool.pixi.workspace] -channels = ["conda-forge"] -platforms = ["osx-arm64", "linux-64"] +channels = [ "conda-forge" ] +platforms = [ "osx-arm64", "linux-64" ] [tool.pixi.dependencies] python = ">=3.11" @@ -174,14 +228,14 @@ python = "3.13.*" [tool.pixi.environments] # 3.11 lane (for gh-actions) -dev-py311 = { features = ["dev", "test", "py311"], solve-group = "py311" } -docs-py311 = { features = ["docs", "py311"], solve-group = "py311" } +dev-py311 = { features = [ "dev", "test", "py311" ], solve-group = "py311" } +docs-py311 = { features = [ "doc", "py311" ], solve-group = "py311" } # 3.13 lane -default = { features = ["py313"], solve-group = "py313" } -dev-py313 = { features = ["dev", "test", "py313"], solve-group = "py313" } -docs-py313 = { features = ["docs", "py313"], solve-group = "py313" } -test-py313 = { features = ["test", "py313"], solve-group = "py313" } +default = { features = [ "py313" ], solve-group = "py313" } +dev-py313 = { features = [ "dev", "test", "py313" ], solve-group = "py313" } +docs-py313 = { features = [ "doc", "py313" ], solve-group = "py313" } +test-py313 = { features = [ "test", "py313" ], solve-group = "py313" } [tool.pixi.tasks] lab = "jupyter lab" @@ -190,4 +244,4 @@ test = "pytest -v --color=yes --tb=short --durations=10" lint = "ruff check ." format = "ruff format ." pre-commit-install = "pre-commit install" -pre-commit-run = "pre-commit run --all-files" \ No newline at end of file +pre-commit-run = "pre-commit run --all-files" diff --git a/tests/conftest.py b/tests/conftest.py index 2299f126..c094ca78 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -441,7 +441,7 @@ def compare(cls, basename: str, tolerance: float | None = None): # Try to get a reasonable layout first (helps with axes/labels) if not fig.get_constrained_layout(): try: - fig.set_constrained_layout(True) + fig.set_layout_engine("constrained") except (ValueError, RuntimeError): try: fig.tight_layout(pad=2.0, rect=[0.02, 0.02, 0.98, 0.98]) diff --git a/tests/pl/test_render_labels.py b/tests/pl/test_render_labels.py index 4449dbd3..031148c3 100644 --- a/tests/pl/test_render_labels.py +++ b/tests/pl/test_render_labels.py @@ -244,7 +244,7 @@ def _make_tablemodel_with_categorical_labels(self, sdata_blobs, labels_name: str ) adata.obs["instance_id"] = instances.values adata.obs["category"] = get_standard_RNG().choice(["a", "b", "c"], size=adata.n_obs) - adata.obs["category"][:3] = ["a", "b", "c"] + adata.obs.loc[adata.obs.index[:3], "category"] = ["a", "b", "c"] adata.obs["region"] = labels_name table = TableModel.parse( adata=adata, @@ -320,7 +320,7 @@ def test_plot_respects_custom_colors_from_uns(self, sdata_blobs: SpatialData): ) adata.obs["instance_id"] = instances.values adata.obs["category"] = get_standard_RNG().choice(["a", "b", "c"], size=adata.n_obs) - adata.obs["category"][:3] = ["a", "b", "c"] + adata.obs.loc[adata.obs.index[:3], "category"] = ["a", "b", "c"] adata.obs["region"] = labels_name table = TableModel.parse( adata=adata, @@ -347,7 +347,7 @@ def test_plot_respects_custom_colors_from_uns_with_groups_and_palette( ) adata.obs["instance_id"] = instances.values adata.obs["category"] = get_standard_RNG().choice(["a", "b", "c"], size=adata.n_obs) - adata.obs["category"][:3] = ["a", "b", "c"] + adata.obs.loc[adata.obs.index[:3], "category"] = ["a", "b", "c"] adata.obs["region"] = labels_name table = TableModel.parse( adata=adata,