diff --git a/codecov.yml b/.github/codecov.yml similarity index 65% rename from codecov.yml rename to .github/codecov.yml index 10c7d48..61e7246 100644 --- a/codecov.yml +++ b/.github/codecov.yml @@ -1,9 +1,12 @@ +codecov: + notify: + after_n_builds: 3 coverage: status: project: default: target: auto - threshold: 2% + threshold: 5% patch: false changes: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..9d1e098 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 2c8c9e7..0000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: lint - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: actions/setup-python@v2.1.4 - - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/pypipublish.yaml b/.github/workflows/pypipublish.yaml index 52f5595..256ff9f 100644 --- a/.github/workflows/pypipublish.yaml +++ b/.github/workflows/pypipublish.yaml @@ -1,26 +1,46 @@ name: Upload Python Package on: + workflow_dispatch: release: - types: [created] + types: [published] + pull_request: + paths: + - ".github/workflows/pypipublish.yml" + - "pyproject.toml" jobs: - deploy: + dist: + name: Build & inspect package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 - - name: Set up Python - uses: actions/setup-python@v2.1.4 + - uses: actions/checkout@v4 with: - python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install setuptools setuptools-scm wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + fetch-depth: 0 + + - uses: hynek/build-and-inspect-python-package@v2 + + publish: + name: Upload package to PyPI + needs: [dist] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + attestations: write + contents: read + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - name: Get dist files + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v2 + with: + subject-path: "dist/*" + + - name: Publish on PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d0a1686..bb5eb3e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,4 +1,4 @@ -name: test +name: Tests on: push: @@ -12,41 +12,36 @@ jobs: test: name: pytest (${{ matrix.os }}, ${{ matrix.python-version }}) runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -el {0} strategy: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.10", "3.13"] steps: - name: Checkout - uses: actions/checkout@v2 - - name: Install miniconda - uses: conda-incubator/setup-miniconda@v2 + uses: actions/checkout@v5 with: - python-version: ${{ matrix.python-version }} - mamba-version: "*" - channels: conda-forge,defaults - channel-priority: true - auto-activate-base: false - - name: Conda info - shell: bash -l {0} - run: | - conda info - conda list - conda config --show-sources - conda config --show - printenv | sort - - name: Install dependencies - shell: bash -l {0} - run: mamba install xarray dask numpy scipy scikit-learn pys2index pytest pytest-cov - - name: Build and install xoak - shell: bash -l {0} + fetch-depth: 0 + + - name: Setup micromamba + uses: mamba-org/setup-micromamba@v2 + with: + environment-file: ci/environment.yml + cache-environment: true + cache-downloads: false + create-args: >- + python=${{ matrix.python-version }} + + - name: Install xoak run: | - python -m pip install . --no-deps -v - python -OO -c "import xoak" + python -m pip install . + - name: Run tests - shell: bash -l {0} - run: pytest . --verbose --color=yes - - name: Codecov - if: matrix.os == 'macos-latest' && matrix.python-version == '3.8' - uses: codecov/codecov-action@v1 + run: | + python -m pytest -vv --cov=xoak --color=yes src/xoak/tests + + - name: Upload coverage report + uses: codecov/codecov-action@v5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 284a0ae..a309b18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,21 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-docstring-first - id: check-yaml - - id: double-quote-string-fixer - - repo: https://github.com/ambv/black - rev: 20.8b1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 hooks: - - id: black - args: ["--line-length", "100", "--skip-string-normalization"] + - id: ruff-format + types_or: [ python, pyi ] + - id: ruff + args: [--fix] + types_or: [ python, pyi ] - - repo: https://gitlab.com/PyCQA/flake8 - rev: 3.8.4 - hooks: - - id: flake8 - - - repo: https://github.com/PyCQA/isort - rev: 5.6.4 - hooks: - - id: isort +ci: + autofix_prs: false + autoupdate_schedule: quarterly diff --git a/.readthedocs.yml b/.readthedocs.yml index 45f714a..78444fe 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,15 +1,20 @@ # Required version: 2 -# Build documentation in the doc/ directory with Sphinx +build: + os: "ubuntu-20.04" + tools: + python: "mambaforge-4.10" + sphinx: configuration: doc/conf.py conda: - environment: environment_doc.yml + environment: doc/environment.yml python: - version: 3.8 install: - method: pip path: . + +formats: [] diff --git a/ci/environment.yml b/ci/environment.yml new file mode 100644 index 0000000..9c660d9 --- /dev/null +++ b/ci/environment.yml @@ -0,0 +1,15 @@ +name: xoak +channels: + - conda-forge +dependencies: + - python + - numpy + - xarray + - dask + - scipy + - scikit-learn + - pys2index + - pytest + - pytest-cov + - pydata-sphinx-theme=0.15.4 + - sphinx-book-theme=1.1.4 diff --git a/doc/_static/style.css b/doc/_static/style.css new file mode 100644 index 0000000..9aeba0a --- /dev/null +++ b/doc/_static/style.css @@ -0,0 +1,55 @@ + +code.literal { + background-color: unset; + border: unset; +} + +.xr-wrap { + /* workaround when (ipy)widgets are present in output cells + * https://github.com/xarray-contrib/xarray-indexes/issues/14 + * only rely on pydata-sphinx-theme variables here! + */ + --xr-font-color0: var(--pst-color-text-base); + --xr-font-color2: var(--pst-color-text-base); + --xr-font-color3: var(--pst-color-text-base); + --xr-border-color: hsl(from var(--pst-color-text-base) h s calc(l + 40)); + --xr-disabled-color: hsl(from var(--pst-color-text-base) h s calc(l + 40)); + --xr-background-color: var(--pst-color-on-background); + --xr-background-color-row-even: hsl( + from var(--pst-color-on-background) h s calc(l - 5) + ); + --xr-background-color-row-odd: hsl( + from var(--pst-color-on-background) h s calc(l - 15) + ); + + font-size: 0.9em; + margin-left: 1.25em; +} + +html[data-theme="dark"] .xr-wrap { + --xr-border-color: hsl(from var(--pst-color-text-base) h s calc(l - 40)); + --xr-disabled-color: hsl(from var(--pst-color-text-base) h s calc(l - 40)); + --xr-background-color-row-even: hsl( + from var(--pst-color-on-background) h s calc(l + 5) + ); + --xr-background-color-row-odd: hsl( + from var(--pst-color-on-background) h s calc(l + 15) + ); +} + +.xr-array-wrap, +.xr-var-data, +.xr-var-preview { + /* font-size: 0.9em; */ +} +.gp { + color: darkorange; +} + +/* workaround Pydata Sphinx theme using light colors for widget cell outputs in dark-mode */ +/* works for many widgets but not for Xarray html reprs */ +/* https://github.com/pydata/pydata-sphinx-theme/issues/2189 */ +html[data-theme="dark"] .output_area.rendered_html:has(div.xr-wrap) { + background-color: var(--pst-color-on-background) !important; + color: var(--pst-color-text-base) !important; +} diff --git a/doc/conf.py b/doc/conf.py index 53c5c30..e4d1cf3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,7 +13,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../src/xoak/')) +sys.path.insert(0, os.path.abspath("../src/xoak/")) import sphinx_autosummary_accessors @@ -21,44 +21,44 @@ # -- Project information ----------------------------------------------------- -project = 'xoak' -copyright = '2020, Benoît Bovy, Willi Rath' -author = 'Benoît Bovy, Willi Rath' +project = "xoak" +copyright = "2020, Benoît Bovy, Willi Rath" +author = "Benoît Bovy, Willi Rath" # -- General configuration --------------------------------------------------- -master_doc = 'index' +master_doc = "index" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', - 'sphinx.ext.extlinks', - 'sphinx_autosummary_accessors', - 'nbsphinx', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.extlinks", + "sphinx_autosummary_accessors", + "nbsphinx", ] extlinks = { - 'issue': ('https://github.com/xarray-contrib/xoak/issues/%s', '#'), - 'pull': ('https://github.com/xarray-contrib/xoak/pull/%s', '#'), + "issue": ("https://github.com/xarray-contrib/xoak/issues/%s", "#%s"), + "pull": ("https://github.com/xarray-contrib/xoak/pull/%s", "#%s"), } # Add any paths that contain templates here, relative to this directory. templates_path = [ - '_templates', + "_templates", sphinx_autosummary_accessors.templates_path, ] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- @@ -66,48 +66,39 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = { -# "github_url": "https://github.com/xarray-contrib/xoak", -# "use_edit_page_button": True, -# "search_bar_position": "navbar", -# } - +html_theme = "sphinx_book_theme" +html_title = "Xoak" + +html_theme_options = dict( + repository_url="https://github.com/xarray-contrib/xoak", + repository_branch="master", + path_to_docs="doc", + use_edit_page_button=True, + use_repository_button=True, + use_issues_button=True, + home_page_in_toc=False, +) html_context = { - 'github_user': 'xarray-contrib', - 'github_repo': 'xoak', - 'github_version': 'master', - 'doc_path': 'doc', + "github_user": "xarray-contrib", + "github_repo": "xoak", + "github_version": "master", + "doc_path": "doc", } -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - +html_static_path = ["_static"] +html_css_files = ["style.css"] -# Enable notebook execution -# https://nbsphinx.readthedocs.io/en/0.4.2/never-execute.html -# nbsphinx_execute = 'auto' -# Allow errors in all notebooks by -# nbsphinx_allow_errors = True -nbsphinx_kernel_name = 'python3' - -# Disable cell timeout +nbsphinx_kernel_name = "python3" nbsphinx_timeout = -1 - nbsphinx_prolog = """ {% set docname = env.doc2path(env.docname, base=None) %} |Binder| You can run this notebook in a `live session `_ or view it `on Github `_. +docname }}>`_ +or view it `on Github `_. .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/xarray-contrib/xoak/master?filepath=doc/{{ docname }} @@ -115,15 +106,15 @@ intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), - 'sklearn': ('http://scikit-learn.org/stable', None), - 'xarray': ('http://xarray.pydata.org/en/stable/', None), + "python": ("https://docs.python.org/3/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "sklearn": ("http://scikit-learn.org/stable", None), + "xarray": ("http://xarray.pydata.org/en/stable/", None), } autosummary_generate = True -autodoc_typehints = 'none' +autodoc_typehints = "none" napoleon_use_param = True napoleon_use_rtype = True diff --git a/doc/environment.yml b/doc/environment.yml new file mode 100644 index 0000000..4ecbbf0 --- /dev/null +++ b/doc/environment.yml @@ -0,0 +1,18 @@ +name: xoak +channels: + - conda-forge +dependencies: + - python=3.12 + - dask + - ipykernel + - matplotlib-base + - nbsphinx + - numpy + - pys2index + - scikit-learn + - sphinx + - sphinx-autosummary-accessors + - pydata-sphinx-theme=0.15.4 + - sphinx-book-theme=1.1.4 + - xarray + - pip diff --git a/doc/examples/dask_support.ipynb b/doc/examples/dask_support.ipynb index a52c084..bb36bcf 100644 --- a/doc/examples/dask_support.ipynb +++ b/doc/examples/dask_support.ipynb @@ -22,8 +22,7 @@ "import numpy as np\n", "import xarray as xr\n", "import xoak\n", - "\n", - "xr.set_options(display_style='text');" + "\n" ] }, { diff --git a/doc/examples/introduction.ipynb b/doc/examples/introduction.ipynb index 082a960..a0e9b63 100644 --- a/doc/examples/introduction.ipynb +++ b/doc/examples/introduction.ipynb @@ -18,8 +18,7 @@ "import numpy as np\n", "import xarray as xr\n", "import xoak\n", - "\n", - "xr.set_options(display_style='text');" + "\n" ] }, { diff --git a/environment.yml b/environment.yml deleted file mode 100644 index a038edf..0000000 --- a/environment.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: xoak -channels: - - conda-forge -dependencies: - - python=3.8 - - numpy - - pandas - - pys2index - - dask - - dask-labextension - - xarray - - scikit-learn - - scipy - - jupyter - - jupyterlab - - notebook - - ipykernel - - matplotlib - - pip - - pip: - - git+https://github.com/xarray-contrib/xoak diff --git a/environment_doc.yml b/environment_doc.yml deleted file mode 100644 index 1d5ab3b..0000000 --- a/environment_doc.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: xoak -channels: - - conda-forge -dependencies: - - python=3.9.6 - - dask=2021.7.2 - - ipykernel=6.0.3 - - matplotlib-base=3.4.2 - - nbsphinx=0.8.6 - - numpy=1.21.1 - - pys2index=0.1.2 - - scikit-learn=0.24.2 - - sphinx=4.1.2 - - sphinx-autosummary-accessors=0.2.1 - - sphinx_rtd_theme=0.5.2 - - xarray=0.19.0 - - pip=21.2.2 - - pip: - - git+https://github.com/xarray-contrib/xoak diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..25edad7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,96 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[tool.hatch] +version.source = "vcs" + +[tool.hatch.metadata.hooks.vcs] + +[tool.hatch.build.targets.sdist] +only-include = ["src/xoak"] + +[project] +name = "xoak" +dynamic = ["version"] +authors = [ + { name = "Benoît Bovy" }, + { name = "Willi Rath" }, +] +maintainers = [ + { name = "xoak contributors" }, +] +license = { text = "MIT" } +description = "Xarray extension that provides indexes for selecting irregular, n-dimensional data." +keywords = ["xarray", "index"] +readme = "README.md" +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: GIS", +] +requires-python = ">=3.10" +dependencies = [ + "xarray", + "numpy", + "scipy", + "dask", +] + +[project.optional-dependencies] +extras = [ + "scikit-learn", + "pys2index", +] +test = [ + "pytest", +] + +[project.urls] +Documentation = "https://xoak.readthedocs.io" +Repository = "https://github.com/xarray-contrib/xoak" + +[tool.ruff] +builtins = ["ellipsis"] +exclude = [ + ".git", + ".eggs", + "build", + "dist", + "__pycache__", + "doc/examples", +] +line-length = 100 + +[tool.ruff.lint] +ignore = [ + "E402", # E402: module level import not at top of file +] +select = [ + "F", # Pyflakes + "E", # Pycodestyle + "I", # isort + "UP", # Pyupgrade + "TID", # flake8-tidy-imports + "W", +] +extend-safe-fixes = [ + "TID252", # absolute imports +] +fixable = ["I", "TID252"] + +[tool.ruff.lint.isort] +known-first-party = ["xoak"] +known-third-party = [ + "xarray", + "scipy", + "sklearn", + "pys2index", +] + +[tool.mypy] +files = ["xoak"] +show_error_codes = true +warn_unused_ignores = false diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 949ce65..0000000 --- a/setup.cfg +++ /dev/null @@ -1,39 +0,0 @@ -[flake8] -exclude = docs -ignore = E203,E266,E501,W503,E722,E402,C901 -per-file-ignores = - src/xoak/__init__.py:F401 - src/xoak/index/__init__.py:F401 - tests/__init__.py:F401 -max-line-length = 100 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 - -[isort] -known_first_party=xoak -known_third_party=xarray,numpy,dask,sklearn -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -combine_as_imports=True -line_length=100 -skip= - setup.py - -[tool:pytest] -log_cli = True -log_level = INFO -testpaths = src/xoak/tests -console_output_style = "progress" -addopts = - -v - -rs - --durations=5 - --cov=src/xoak/ - --cov-append - --cov-report="term-missing" - --cov-report="xml" - --cov-config=setup.cfg - -[coverage:run] -omit = src/xoak/tests/* diff --git a/setup.py b/setup.py deleted file mode 100644 index 88863c1..0000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python - -"""The setup script.""" - -from os.path import exists - -from setuptools import find_packages, setup - -if exists('README.md'): - with open('README.md') as f: - long_description = f.read() -else: - long_description = '' - -CLASSIFIERS = [ - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Intended Audience :: Science/Research', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Scientific/Engineering', -] - -setup( - name='xoak', - description='Xarray extension that provides indexes for selecting irregular, n-dimensional data.', - long_description=long_description, - python_requires='>=3.6', - maintainer='Benoit Bovy, Willi Rath', - maintainer_email='benbovy@gmail.com', - classifiers=CLASSIFIERS, - url='https://github.com/xarray-contrib/xoak', - packages=find_packages(where='src'), - package_dir={'': 'src'}, - include_package_data=True, - install_requires=['xarray', 'numpy', 'dask', 'scipy'], - extras_require={ - 'dev': ['scikit-learn', 'pytest', 'pytest-cov', 'pre-commit'], - }, - license='MIT', - zip_safe=False, - keywords=['xarray', 'index'], - use_scm_version={'version_scheme': 'post-release', 'local_scheme': 'dirty-tag'}, - setup_requires=['setuptools_scm', 'setuptools>=30.3.0'], -) diff --git a/src/xoak/__init__.py b/src/xoak/__init__.py index 3d87655..6fac9d2 100644 --- a/src/xoak/__init__.py +++ b/src/xoak/__init__.py @@ -1,14 +1,12 @@ -try: - from importlib.metadata import version, PackageNotFoundError -except ImportError: - # Python < 3.8 - from importlib_metadata import version, PackageNotFoundError +from importlib.metadata import version from .accessor import XoakAccessor from .index import IndexAdapter, IndexRegistry -try: - __version__ = version(__name__) -except PackageNotFoundError: # pragma: no cover - # package is not installed - pass +__all__ = [ + "XoakAccessor", + "IndexAdapter", + "IndexRegistry", +] + +__version__ = version("xoak") diff --git a/src/xoak/accessor.py b/src/xoak/accessor.py index 12e52f4..d3a1f1e 100644 --- a/src/xoak/accessor.py +++ b/src/xoak/accessor.py @@ -1,4 +1,7 @@ -from typing import Any, Hashable, Iterable, List, Mapping, Tuple, Type, Union +from __future__ import annotations + +from collections.abc import Hashable, Iterable, Mapping +from typing import Any import numpy as np import xarray as xr @@ -9,10 +12,10 @@ try: from dask.delayed import Delayed except ImportError: # pragma: no cover - Delayed = Type[None] + Delayed = type[None] -def coords_to_point_array(coords: List[Any]) -> np.ndarray: +def coords_to_point_array(coords: list[Any]) -> np.ndarray: """Re-arrange data from a list of xarray coordinates into a 2-d array of shape (npoints, ncoords). @@ -34,12 +37,12 @@ def coords_to_point_array(coords: List[Any]) -> np.ndarray: return X -IndexAttr = Union[XoakIndexWrapper, Iterable[XoakIndexWrapper], Iterable[Delayed]] -IndexType = Union[str, Type[IndexAdapter]] +IndexAttr = XoakIndexWrapper | Iterable[XoakIndexWrapper] | Iterable[Delayed] +IndexType = str | type[IndexAdapter] -@xr.register_dataarray_accessor('xoak') -@xr.register_dataset_accessor('xoak') +@xr.register_dataarray_accessor("xoak") +@xr.register_dataset_accessor("xoak") class XoakAccessor: """A xarray Dataset or DataArray extension for indexing irregular, n-dimensional data using a ball tree. @@ -48,11 +51,11 @@ class XoakAccessor: _index: IndexAttr _index_type: IndexType - _index_coords: Tuple[str] - _index_coords_dims: Tuple[Hashable, ...] - _index_coords_shape: Tuple[int, ...] + _index_coords: tuple[str, ...] + _index_coords_dims: tuple[Hashable, ...] + _index_coords_shape: tuple[int, ...] - def __init__(self, xarray_obj: Union[xr.Dataset, xr.DataArray]): + def __init__(self, xarray_obj: xr.Dataset | xr.DataArray): self._xarray_obj = xarray_obj def _build_index_forest_delayed(self, X, persist=False, **kwargs) -> IndexAttr: @@ -73,7 +76,11 @@ def _build_index_forest_delayed(self, X, persist=False, **kwargs) -> IndexAttr: return tuple(indexes) def set_index( - self, coords: Iterable[str], index_type: IndexType, persist: bool = True, **kwargs + self, + coords: Iterable[str], + index_type: IndexType, + persist: bool = True, + **kwargs, ): """Create an index tree from a subset of coordinates of the DataArray / Dataset. @@ -102,7 +109,7 @@ def set_index( if len(set([c.dims for c in coord_objs])) > 1: raise ValueError( - 'Coordinates {coords} must all have the same dimensions in the same order' + "Coordinates {coords} must all have the same dimensions in the same order" ) self._index_coords_dims = coord_objs[0].dims @@ -116,14 +123,14 @@ def set_index( self._index = self._build_index_forest_delayed(X, persist=persist, **kwargs) @property - def index(self) -> Union[None, Index, Iterable[Index]]: + def index(self) -> None | Index | Iterable[Index]: """Returns the underlying index object(s), or ``None`` if no index has been set yet. May trigger computation of lazy indexes. """ - if not getattr(self, '_index', False): + if not getattr(self, "_index", False): return None elif isinstance(self._index, XoakIndexWrapper): return self._index.index @@ -139,7 +146,7 @@ def _query(self, indexers): if isinstance(X, np.ndarray) and isinstance(self._index, XoakIndexWrapper): # directly call index wrapper's query method res = self._index.query(X) - results = res['indices'][:, 0] + results = res["indices"][:, 0] else: # Two-stage lazy query with dask @@ -157,7 +164,8 @@ def _query(self, indexers): # 1st "map" stage: # - execute `IndexWrapperCls.query` for each query array chunk and each index instance - # - concatenate all distances/positions results in two dask arrays of shape (n_points, n_indexes) + # - concatenate all distances/positions results in two dask arrays of shape + # (n_points, n_indexes) res_chunk = [] @@ -176,8 +184,8 @@ def _query(self, indexers): res_chunk.append(da.concatenate(res_chunk_idx, axis=1)) map_results = da.concatenate(res_chunk, axis=0) - distances = map_results['distances'] - indices = map_results['indices'] + distances = map_results["distances"] + indices = map_results["indices"] # 2nd "reduce" stage: # - brute force lookup over the indexes dimension (columns) @@ -186,11 +194,11 @@ def _query(self, indexers): results = da.blockwise( lambda arr, icol: np.take_along_axis(arr, icol[:, None], 1).squeeze(), - 'i', + "i", indices, - 'ik', + "ik", indices_col, - 'i', + "i", dtype=np.intp, concatenate=True, ) @@ -212,7 +220,7 @@ def _get_pos_indexers(self, indices, indexers): indexer_shapes = [idx.shape for idx in indexers.values()] if len(set(indexer_dims)) > 1: - raise ValueError('All indexers must have the same dimensions.') + raise ValueError("All indexers must have the same dimensions.") u_indices = list(np.unravel_index(indices.ravel(), self._index_coords_shape)) @@ -225,8 +233,8 @@ def _get_pos_indexers(self, indices, indexers): return pos_indexers def sel( - self, indexers: Mapping[Hashable, Any] = None, **indexers_kwargs: Any - ) -> Union[xr.Dataset, xr.DataArray]: + self, indexers: Mapping[Hashable, Any] | None = None, **indexers_kwargs: Any + ) -> xr.Dataset | xr.DataArray: """Selection based on a ball tree index. The index must have been already built using `xoak.set_index()`. @@ -244,12 +252,12 @@ def sel( coordinates are chunked. """ - if not getattr(self, '_index', False): + if not getattr(self, "_index", False): raise ValueError( - 'The index(es) has/have not been built yet. Call `.xoak.set_index()` first' + "The index(es) has/have not been built yet. Call `.xoak.set_index()` first" ) - indexers = either_dict_or_kwargs(indexers, indexers_kwargs, 'xoak.sel') + indexers = either_dict_or_kwargs(indexers, indexers_kwargs, "xoak.sel") indices = self._query(indexers) if not isinstance(indices, np.ndarray): diff --git a/src/xoak/index/__init__.py b/src/xoak/index/__init__.py index 095a529..9d6b9cf 100644 --- a/src/xoak/index/__init__.py +++ b/src/xoak/index/__init__.py @@ -3,15 +3,15 @@ from .base import IndexAdapter, IndexRegistry # noqa: F401 adapters = [ - 'scipy_adapters', - 'sklearn_adapters', - 's2_adapters', + "scipy_adapters", + "sklearn_adapters", + "s2_adapters", ] for mod in adapters: try: # importing the module registers the adapters - importlib.import_module('.' + mod, package='xoak.index') + importlib.import_module("." + mod, package="xoak.index") except ImportError: # pragma: no cover pass diff --git a/src/xoak/index/base.py b/src/xoak/index/base.py index 540f202..c54721a 100644 --- a/src/xoak/index/base.py +++ b/src/xoak/index/base.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import abc import warnings +from collections.abc import Mapping from contextlib import suppress -from typing import Any, Dict, List, Mapping, Tuple, Type, TypeVar, Union +from typing import Any, TypeVar import numpy as np -Index = TypeVar('Index') +Index = TypeVar("Index") class IndexAdapter(abc.ABC): @@ -42,7 +45,7 @@ def build(self, points: np.ndarray) -> Index: raise NotImplementedError() @abc.abstractmethod - def query(self, index: Index, points: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def query(self, index: Index, points: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """Query points/samples, Parameters @@ -69,13 +72,13 @@ class IndexRegistrationWarning(Warning): """Warning for conflicts in index registration.""" -class IndexRegistry(Mapping[str, Type[IndexAdapter]]): +class IndexRegistry(Mapping[str, type[IndexAdapter]]): """A registry of all indexes adapters that can be used to select data with xoak. """ - _default_indexes: Dict[str, Type[IndexAdapter]] = {} + _default_indexes: dict[str, type[IndexAdapter]] = {} def __init__(self, use_default=True): """Creates a new index registry. @@ -107,9 +110,9 @@ def register(self, name: str): """ - def wrap(cls: Type[IndexAdapter]): + def wrap(cls: type[IndexAdapter]): if not issubclass(cls, IndexAdapter): - raise TypeError('can only register IndexAdapter subclasses.') + raise TypeError("can only register IndexAdapter subclasses.") if name in self._indexes: warnings.warn( @@ -125,20 +128,20 @@ def wrap(cls: Type[IndexAdapter]): return wrap def __getattr__(self, name): - if name not in {'__dict__', '__setstate__'}: + if name not in {"__dict__", "__setstate__"}: # this avoids an infinite loop when pickle looks for the # __setstate__ attribute before the xarray object is initialized with suppress(KeyError): return self._indexes[name] - raise AttributeError(f'IndexRegistry object has no attribute {name!r}') + raise AttributeError(f"IndexRegistry object has no attribute {name!r}") def __setattr__(self, name, value): - if name == '_indexes': + if name == "_indexes": object.__setattr__(self, name, value) else: raise AttributeError( - f'cannot set attribute {name!r} on a IndexRegistry object. ' - 'Use `.register()` to add a new index adapter to the registry.' + f"cannot set attribute {name!r} on a IndexRegistry object. " + "Use `.register()` to add a new index adapter to the registry." ) def __dir__(self): @@ -158,8 +161,8 @@ def __len__(self): return len(self._indexes) def __repr__(self): - header = f'\n' - return header + '\n'.join([name for name in self._indexes]) + header = f"\n" + return header + "\n".join([name for name in self._indexes]) def register_default(name: str): @@ -178,7 +181,7 @@ def register_default(name: str): """ - def decorator(cls: Type[IndexAdapter]): + def decorator(cls: type[IndexAdapter]): if cls.__doc__ is not None: cls.__doc__ += doc_extra else: @@ -190,8 +193,7 @@ def decorator(cls: Type[IndexAdapter]): return decorator -def normalize_index(name_or_cls: Union[str, Any]) -> Type[IndexAdapter]: - +def normalize_index(name_or_cls: str | Any) -> type[IndexAdapter]: if isinstance(name_or_cls, str): cls = IndexRegistry._default_indexes[name_or_cls] else: @@ -209,14 +211,14 @@ class XoakIndexWrapper: """ - _query_result_dtype: List[Tuple[str, Any]] = [ - ('distances', np.double), - ('indices', np.intp), + _query_result_dtype: list[tuple[str, Any]] = [ + ("distances", np.double), + ("indices", np.intp), ] def __init__( self, - index_adapter: Union[str, Type[IndexAdapter]], + index_adapter: str | type[IndexAdapter], points: np.ndarray, offset: int, **kwargs, @@ -235,7 +237,7 @@ def query(self, points: np.ndarray) -> np.ndarray: distances, positions = self._index_adapter.query(self._index, points) result = np.empty(shape=points.shape[0], dtype=self._query_result_dtype) - result['distances'] = distances.ravel().astype(np.double) - result['indices'] = positions.ravel().astype(np.intp) + self._offset + result["distances"] = distances.ravel().astype(np.double) + result["indices"] = positions.ravel().astype(np.intp) + self._offset return result[:, None] diff --git a/src/xoak/index/s2_adapters.py b/src/xoak/index/s2_adapters.py index f971eca..f85a346 100644 --- a/src/xoak/index/s2_adapters.py +++ b/src/xoak/index/s2_adapters.py @@ -3,7 +3,7 @@ from .base import IndexAdapter, register_default -@register_default('s2point') +@register_default("s2point") class S2PointIndexAdapter(IndexAdapter): """Xoak index adapter for :class:`pys2index.S2PointIndex`. diff --git a/src/xoak/index/scipy_adapters.py b/src/xoak/index/scipy_adapters.py index eb35d92..afd632e 100644 --- a/src/xoak/index/scipy_adapters.py +++ b/src/xoak/index/scipy_adapters.py @@ -3,7 +3,7 @@ from .base import IndexAdapter, register_default -@register_default('scipy_kdtree') +@register_default("scipy_kdtree") class ScipyKDTreeAdapter(IndexAdapter): """Xoak index adapter for :class:`scipy.spatial.cKDTree`.""" diff --git a/src/xoak/index/sklearn_adapters.py b/src/xoak/index/sklearn_adapters.py index e913132..99a491f 100644 --- a/src/xoak/index/sklearn_adapters.py +++ b/src/xoak/index/sklearn_adapters.py @@ -4,7 +4,7 @@ from .base import IndexAdapter, register_default -@register_default('sklearn_kdtree') +@register_default("sklearn_kdtree") class SklearnKDTreeAdapter(IndexAdapter): """Xoak index adapter for :class:`sklearn.neighbors.KDTree`.""" @@ -18,7 +18,7 @@ def query(self, kdtree, points): return kdtree.query(points) -@register_default('sklearn_balltree') +@register_default("sklearn_balltree") class SklearnBallTreeAdapter(IndexAdapter): """Xoak index adapter for :class:`sklearn.neighbors.BallTree`.""" @@ -32,7 +32,7 @@ def query(self, btree, points): return btree.query(points) -@register_default('sklearn_geo_balltree') +@register_default("sklearn_geo_balltree") class SklearnGeoBallTreeAdapter(IndexAdapter): """Xoak index adapter for :class:`sklearn.neighbors.BallTree`, using the 'haversine' metric. @@ -48,7 +48,7 @@ class SklearnGeoBallTreeAdapter(IndexAdapter): """ def __init__(self, **kwargs): - kwargs.update({'metric': 'haversine'}) + kwargs.update({"metric": "haversine"}) self._index_options = kwargs def build(self, points): diff --git a/src/xoak/tests/conftest.py b/src/xoak/tests/conftest.py index e8b4314..a0ae7bf 100644 --- a/src/xoak/tests/conftest.py +++ b/src/xoak/tests/conftest.py @@ -1,4 +1,5 @@ import dask +import dask.array as da import numpy as np import pytest import xarray as xr @@ -7,16 +8,16 @@ # use single-threaded dask scheduler for all tests, as multi-threads or # multi-processes may not be supported by some index adapters. # TODO: enable multi-threaded and/or multi-processes per index -dask.config.set(scheduler='single-threaded') +dask.config.set(scheduler="single-threaded") -@pytest.fixture(params=[np, dask.array], scope='session') +@pytest.fixture(params=[np, da], scope="session") def dataset_array_lib(request): """Array lib that is used for creation of the data.""" return request.param -@pytest.fixture(params=[np, dask.array], scope='session') +@pytest.fixture(params=[np, da], scope="session") def indexer_array_lib(request): """Array lib that is used for creation of the indexer.""" return request.param @@ -24,11 +25,11 @@ def indexer_array_lib(request): @pytest.fixture( params=[ - (('d1',), (200,), (100,)), - (('d1', 'd2'), (20, 10), (10, 10)), - (('d1', 'd2', 'd3'), (4, 10, 5), (2, 10, 5)), + (("d1",), (200,), (100,)), + (("d1", "d2"), (20, 10), (10, 10)), + (("d1", "d2", "d3"), (4, 10, 5), (2, 10, 5)), ], - scope='session', + scope="session", ) def dataset_dims_shape_chunks(request): return request.param @@ -36,18 +37,22 @@ def dataset_dims_shape_chunks(request): @pytest.fixture( params=[ - (('i1',), (100,), (50,)), - (('i1', 'i2'), (10, 10), (5, 10)), - (('i1', 'i2', 'i3'), (2, 10, 5), (1, 10, 5)), + (("i1",), (100,), (50,)), + (("i1", "i2"), (10, 10), (5, 10)), + (("i1", "i2", "i3"), (2, 10, 5), (1, 10, 5)), ], - scope='session', + scope="session", ) def indexer_dims_shape_chunks(request): return request.param def query_brute_force( - dataset, dataset_dims_shape_chunks, indexer, indexer_dims_shape_chunks, metric='euclidean' + dataset, + dataset_dims_shape_chunks, + indexer, + indexer_dims_shape_chunks, + metric="euclidean", ): """Find nearest neighbors using brute-force approach.""" @@ -55,7 +60,7 @@ def query_brute_force( X = np.stack([np.ravel(c) for c in indexer.coords.values()]).T Y = np.stack([np.ravel(c) for c in dataset.coords.values()]).T - if metric == 'haversine': + if metric == "haversine": X = np.deg2rad(X) Y = np.deg2rad(Y) @@ -74,92 +79,92 @@ def query_brute_force( return dataset.isel(indexers=pos_indexers) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def geo_dataset(dataset_dims_shape_chunks, dataset_array_lib): """Dataset with coords lon and lat on a grid of different shapes.""" dims, shape, chunks = dataset_dims_shape_chunks - if dataset_array_lib is dask.array: - kwargs = {'size': shape, 'chunks': chunks} + if dataset_array_lib is da: + kwargs = {"size": shape, "chunks": chunks} else: - kwargs = {'size': shape} + kwargs = {"size": shape} lat = xr.DataArray(dataset_array_lib.random.uniform(-80, 80, **kwargs), dims=dims) lon = xr.DataArray(dataset_array_lib.random.uniform(-160, 160, **kwargs), dims=dims) - ds = xr.Dataset(coords={'lat': lat, 'lon': lon}) + ds = xr.Dataset(coords={"lat": lat, "lon": lon}) return ds -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def geo_indexer(indexer_dims_shape_chunks, indexer_array_lib): """Indexer dataset with coords longitude and latitude of parametrized shapes.""" dims, shape, chunks = indexer_dims_shape_chunks - if indexer_array_lib is dask.array: - kwargs = {'size': shape, 'chunks': chunks} + if indexer_array_lib is da: + kwargs = {"size": shape, "chunks": chunks} else: - kwargs = {'size': shape} + kwargs = {"size": shape} latitude = xr.DataArray(indexer_array_lib.random.uniform(-80, 80, **kwargs), dims=dims) longitude = xr.DataArray(indexer_array_lib.random.uniform(-160, 160, **kwargs), dims=dims) - ds = xr.Dataset(coords={'latitude': latitude, 'longitude': longitude}) + ds = xr.Dataset(coords={"latitude": latitude, "longitude": longitude}) return ds -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def geo_expected(geo_dataset, dataset_dims_shape_chunks, geo_indexer, indexer_dims_shape_chunks): return query_brute_force( geo_dataset, dataset_dims_shape_chunks, geo_indexer, indexer_dims_shape_chunks, - metric='haversine', + metric="haversine", ) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def xyz_dataset(dataset_dims_shape_chunks, dataset_array_lib): """Dataset with coords x, y, z on a grid of different shapes.""" dims, shape, chunks = dataset_dims_shape_chunks - if dataset_array_lib is dask.array: - kwargs = {'size': shape, 'chunks': chunks} + if dataset_array_lib is da: + kwargs = {"size": shape, "chunks": chunks} else: - kwargs = {'size': shape} + kwargs = {"size": shape} x = xr.DataArray(dataset_array_lib.random.uniform(0, 10, **kwargs), dims=dims) y = xr.DataArray(dataset_array_lib.random.uniform(0, 10, **kwargs), dims=dims) z = xr.DataArray(dataset_array_lib.random.uniform(0, 10, **kwargs), dims=dims) - ds = xr.Dataset(coords={'x': x, 'y': y, 'z': z}) + ds = xr.Dataset(coords={"x": x, "y": y, "z": z}) return ds -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def xyz_indexer(indexer_dims_shape_chunks, indexer_array_lib): """Indexer dataset with coords xx, yy, zz of parametrized shapes.""" dims, shape, chunks = indexer_dims_shape_chunks - if indexer_array_lib is dask.array: - kwargs = {'size': shape, 'chunks': chunks} + if indexer_array_lib is da: + kwargs = {"size": shape, "chunks": chunks} else: - kwargs = {'size': shape} + kwargs = {"size": shape} xx = xr.DataArray(indexer_array_lib.random.uniform(0, 10, **kwargs), dims=dims) yy = xr.DataArray(indexer_array_lib.random.uniform(0, 10, **kwargs), dims=dims) zz = xr.DataArray(indexer_array_lib.random.uniform(0, 10, **kwargs), dims=dims) - ds = xr.Dataset(coords={'xx': xx, 'yy': yy, 'zz': zz}) + ds = xr.Dataset(coords={"xx": xx, "yy": yy, "zz": zz}) return ds -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def xyz_expected(xyz_dataset, dataset_dims_shape_chunks, xyz_indexer, indexer_dims_shape_chunks): return query_brute_force( xyz_dataset, dataset_dims_shape_chunks, xyz_indexer, indexer_dims_shape_chunks diff --git a/src/xoak/tests/test_accessor.py b/src/xoak/tests/test_accessor.py index 32877ac..4c55eb8 100644 --- a/src/xoak/tests/test_accessor.py +++ b/src/xoak/tests/test_accessor.py @@ -8,29 +8,29 @@ def test_set_index_error(): ds = xr.Dataset( coords={ - 'x': (('a', 'b'), [[0, 1], [2, 3]]), - 'y': (('b', 'a'), [[0, 1], [2, 3]]), - 'z': ('a', [0, 1]), + "x": (("a", "b"), [[0, 1], [2, 3]]), + "y": (("b", "a"), [[0, 1], [2, 3]]), + "z": ("a", [0, 1]), } ) - with pytest.raises(ValueError, match='.*must all have the same dimensions.*'): - ds.xoak.set_index(['x', 'y'], 'scipy_kdtree') + with pytest.raises(ValueError, match=".*must all have the same dimensions.*"): + ds.xoak.set_index(["x", "y"], "scipy_kdtree") - with pytest.raises(ValueError, match='.*must all have the same dimensions.*'): - ds.xoak.set_index(['x', 'z'], 'scipy_kdtree') + with pytest.raises(ValueError, match=".*must all have the same dimensions.*"): + ds.xoak.set_index(["x", "z"], "scipy_kdtree") def test_set_index_persist_false(): ds = xr.Dataset( coords={ - 'x': ('a', [0, 1, 2, 3]), - 'y': ('a', [0, 1, 2, 3]), + "x": ("a", [0, 1, 2, 3]), + "y": ("a", [0, 1, 2, 3]), } ) ds = ds.chunk(2) - ds.xoak.set_index(['x', 'y'], 'scipy_kdtree', persist=False) + ds.xoak.set_index(["x", "y"], "scipy_kdtree", persist=False) assert isinstance(ds.xoak._index, tuple) @@ -38,45 +38,45 @@ def test_set_index_persist_false(): def test_sel_error(): ds = xr.Dataset( coords={ - 'x': ('a', [0, 1, 2, 3]), - 'y': ('a', [0, 1, 2, 3]), + "x": ("a", [0, 1, 2, 3]), + "y": ("a", [0, 1, 2, 3]), } ) indexer = xr.Dataset( coords={ - 'x': ('p', [1.2, 2.9]), - 'y': ('p', [1.2, 2.9]), + "x": ("p", [1.2, 2.9]), + "y": ("p", [1.2, 2.9]), } ) - with pytest.raises(ValueError, match='.*not been built yet.*'): + with pytest.raises(ValueError, match=".*not been built yet.*"): ds.xoak.sel(x=indexer.x, y=indexer.y) - ds.xoak.set_index(['x', 'y'], 'scipy_kdtree') + ds.xoak.set_index(["x", "y"], "scipy_kdtree") indexer = xr.Dataset( coords={ - 'x': ('p', [1.2, 2.9]), - 'y': ('p2', [1.2, 2.9]), + "x": ("p", [1.2, 2.9]), + "y": ("p2", [1.2, 2.9]), } ) - with pytest.raises(ValueError, match='.*must have the same dimensions.*'): + with pytest.raises(ValueError, match=".*must have the same dimensions.*"): ds.xoak.sel(x=indexer.x, y=indexer.y) def test_index_property(): ds = xr.Dataset( coords={ - 'x': ('a', [0, 1, 2, 3]), - 'y': ('a', [0, 1, 2, 3]), + "x": ("a", [0, 1, 2, 3]), + "y": ("a", [0, 1, 2, 3]), } ) assert ds.xoak.index is None - ds.xoak.set_index(['x', 'y'], 'scipy_kdtree') + ds.xoak.set_index(["x", "y"], "scipy_kdtree") assert isinstance(ds.xoak.index, cKDTree) ds_chunk = ds.chunk(2) - ds_chunk.xoak.set_index(['x', 'y'], 'scipy_kdtree') + ds_chunk.xoak.set_index(["x", "y"], "scipy_kdtree") assert isinstance(ds_chunk.xoak.index, list) diff --git a/src/xoak/tests/test_index_base.py b/src/xoak/tests/test_index_base.py index 2974921..98daebe 100644 --- a/src/xoak/tests/test_index_base.py +++ b/src/xoak/tests/test_index_base.py @@ -17,8 +17,8 @@ def __init__(self, points, option=1): self.option = option def query(self, points): - distances = np.zeros((points.shape[0])) - indices = np.ones((points.shape[0])) + distances = np.zeros(points.shape[0]) + indices = np.ones(points.shape[0]) return distances, indices @@ -61,60 +61,60 @@ def test_index_registery_constructor(): def test_index_registery_register(): registry = IndexRegistry(use_default=False) - registry.register('dummy')(DummyIndexAdapter) + registry.register("dummy")(DummyIndexAdapter) - with pytest.warns(IndexRegistrationWarning, match='overriding an already registered index.*'): - registry.register('dummy')(DummyIndexAdapter) + with pytest.warns(IndexRegistrationWarning, match="overriding an already registered index.*"): + registry.register("dummy")(DummyIndexAdapter) - with pytest.raises(TypeError, match='can only register IndexAdapter subclasses.'): - registry.register('invalid')(DummyIndex) + with pytest.raises(TypeError, match="can only register IndexAdapter subclasses."): + registry.register("invalid")(DummyIndex) def test_index_registry_dict_interface(): registry = IndexRegistry(use_default=False) - registry.register('dummy')(DummyIndexAdapter) + registry.register("dummy")(DummyIndexAdapter) - assert registry['dummy'] is DummyIndexAdapter - assert list(registry) == ['dummy'] + assert registry["dummy"] is DummyIndexAdapter + assert list(registry) == ["dummy"] assert len(registry) == 1 - assert repr(registry) == '\ndummy' + assert repr(registry) == "\ndummy" def test_index_registry_attr_access(): registry = IndexRegistry(use_default=False) - registry.register('dummy')(DummyIndexAdapter) + registry.register("dummy")(DummyIndexAdapter) assert registry.dummy is DummyIndexAdapter - assert 'dummy' in dir(registry) + assert "dummy" in dir(registry) - with pytest.raises(AttributeError, match='.*has no attribute.*'): + with pytest.raises(AttributeError, match=".*has no attribute.*"): registry.invalid_attr - with pytest.raises(AttributeError, match='.*cannot set attribute.*'): + with pytest.raises(AttributeError, match=".*cannot set attribute.*"): registry.custom = DummyIndexAdapter def test_index_registry_ipython_completion(): registry = IndexRegistry(use_default=False) - registry.register('dummy')(DummyIndexAdapter) + registry.register("dummy")(DummyIndexAdapter) - assert 'dummy' in registry._ipython_key_completions_() + assert "dummy" in registry._ipython_key_completions_() def test_register_default(): # check that docstrings are updated - assert 'This index adapter is registered in xoak' in ScipyKDTreeAdapter.__doc__ + assert "This index adapter is registered in xoak" in ScipyKDTreeAdapter.__doc__ - register_default('dummy')(DummyIndexAdapter) - assert 'This index adapter is registered in xoak' in DummyIndexAdapter.__doc__ - del IndexRegistry._default_indexes['dummy'] + register_default("dummy")(DummyIndexAdapter) + assert "This index adapter is registered in xoak" in DummyIndexAdapter.__doc__ + del IndexRegistry._default_indexes["dummy"] def test_normalize_index(): assert normalize_index(DummyIndexAdapter) is DummyIndexAdapter - assert normalize_index('scipy_kdtree') is ScipyKDTreeAdapter + assert normalize_index("scipy_kdtree") is ScipyKDTreeAdapter - with pytest.raises(TypeError, match='.*is not a subclass of IndexAdapter'): + with pytest.raises(TypeError, match=".*is not a subclass of IndexAdapter"): normalize_index(DummyIndex) @@ -132,7 +132,7 @@ def test_xoak_index_wrapper(): results = wrapper.query(np.zeros((5, 2))).ravel() - assert results['distances'].dtype == np.double - assert results['indices'].dtype == np.intp - np.testing.assert_equal(results['distances'], np.zeros(5)) - np.testing.assert_equal(results['indices'], np.ones(5) + offset) + assert results["distances"].dtype == np.double + assert results["indices"].dtype == np.intp + np.testing.assert_equal(results["distances"], np.zeros(5)) + np.testing.assert_equal(results["indices"], np.ones(5) + offset) diff --git a/src/xoak/tests/test_s2_adapters.py b/src/xoak/tests/test_s2_adapters.py index fd47a68..523e0cc 100644 --- a/src/xoak/tests/test_s2_adapters.py +++ b/src/xoak/tests/test_s2_adapters.py @@ -6,20 +6,20 @@ import xoak # noqa:F401 -pytest.importorskip('pys2index') +pytest.importorskip("pys2index") def test_s2point(geo_dataset, geo_indexer, geo_expected): - geo_dataset.xoak.set_index(['lat', 'lon'], 's2point') + geo_dataset.xoak.set_index(["lat", "lon"], "s2point") ds_sel = geo_dataset.xoak.sel(lat=geo_indexer.latitude, lon=geo_indexer.longitude) xr.testing.assert_equal(ds_sel.load(), geo_expected.load()) def test_s2point_sizeof(): - ds = xr.Dataset(coords={'lat': ('points', [0.0, 10.0]), 'lon': ('points', [-5.0, 5.0])}) + ds = xr.Dataset(coords={"lat": ("points", [0.0, 10.0]), "lon": ("points", [-5.0, 5.0])}) points = np.array([[0.0, -5.0], [10.0, 5.0]]) - ds.xoak.set_index(['lat', 'lon'], 's2point') + ds.xoak.set_index(["lat", "lon"], "s2point") assert sys.getsizeof(ds.xoak._index._index_adapter) > points.nbytes diff --git a/src/xoak/tests/test_scipy_adapters.py b/src/xoak/tests/test_scipy_adapters.py index a15f119..a4b1522 100644 --- a/src/xoak/tests/test_scipy_adapters.py +++ b/src/xoak/tests/test_scipy_adapters.py @@ -3,19 +3,19 @@ import xoak # noqa: F401 -pytest.importorskip('scipy') +pytest.importorskip("scipy") def test_scipy_kdtree(xyz_dataset, xyz_indexer, xyz_expected): - xyz_dataset.xoak.set_index(['x', 'y', 'z'], 'scipy_kdtree') + xyz_dataset.xoak.set_index(["x", "y", "z"], "scipy_kdtree") ds_sel = xyz_dataset.xoak.sel(x=xyz_indexer.xx, y=xyz_indexer.yy, z=xyz_indexer.zz) xr.testing.assert_equal(ds_sel.load(), xyz_expected.load()) def test_scipy_kdtree_options(): - ds = xr.Dataset(coords={'x': ('points', [1, 2]), 'y': ('points', [1, 2])}) + ds = xr.Dataset(coords={"x": ("points", [1, 2]), "y": ("points", [1, 2])}) - ds.xoak.set_index(['x', 'y'], 'scipy_kdtree', leafsize=10) + ds.xoak.set_index(["x", "y"], "scipy_kdtree", leafsize=10) assert ds.xoak.index.leafsize == 10 diff --git a/src/xoak/tests/test_sklearn_adapters.py b/src/xoak/tests/test_sklearn_adapters.py index 0a019c5..87c7a6b 100644 --- a/src/xoak/tests/test_sklearn_adapters.py +++ b/src/xoak/tests/test_sklearn_adapters.py @@ -3,53 +3,56 @@ import xoak # noqa: F401 -pytest.importorskip('sklearn') +pytest.importorskip("sklearn") def test_sklearn_kdtree(xyz_dataset, xyz_indexer, xyz_expected): - xyz_dataset.xoak.set_index(['x', 'y', 'z'], 'sklearn_kdtree') + xyz_dataset.xoak.set_index(["x", "y", "z"], "sklearn_kdtree") ds_sel = xyz_dataset.xoak.sel(x=xyz_indexer.xx, y=xyz_indexer.yy, z=xyz_indexer.zz) xr.testing.assert_equal(ds_sel.load(), xyz_expected.load()) def test_sklearn_kdtree_options(): - ds = xr.Dataset(coords={'x': ('points', [1, 2]), 'y': ('points', [1, 2])}) + ds = xr.Dataset(coords={"x": ("points", [1, 2]), "y": ("points", [1, 2])}) - ds.xoak.set_index(['x', 'y'], 'sklearn_kdtree', leaf_size=10) + ds.xoak.set_index(["x", "y"], "sklearn_kdtree", leaf_size=10) # sklearn tree classes init options are not exposed as class properties - assert ds.xoak._index._index_adapter._index_options == {'leaf_size': 10} + assert ds.xoak._index._index_adapter._index_options == {"leaf_size": 10} def test_sklearn_balltree(xyz_dataset, xyz_indexer, xyz_expected): - xyz_dataset.xoak.set_index(['x', 'y', 'z'], 'sklearn_balltree') + xyz_dataset.xoak.set_index(["x", "y", "z"], "sklearn_balltree") ds_sel = xyz_dataset.xoak.sel(x=xyz_indexer.xx, y=xyz_indexer.yy, z=xyz_indexer.zz) xr.testing.assert_equal(ds_sel.load(), xyz_expected.load()) def test_sklearn_balltree_options(): - ds = xr.Dataset(coords={'x': ('points', [1, 2]), 'y': ('points', [1, 2])}) + ds = xr.Dataset(coords={"x": ("points", [1, 2]), "y": ("points", [1, 2])}) - ds.xoak.set_index(['x', 'y'], 'sklearn_balltree', leaf_size=10) + ds.xoak.set_index(["x", "y"], "sklearn_balltree", leaf_size=10) # sklearn tree classes init options are not exposed as class properties - assert ds.xoak._index._index_adapter._index_options == {'leaf_size': 10} + assert ds.xoak._index._index_adapter._index_options == {"leaf_size": 10} def test_sklearn_geo_balltree(geo_dataset, geo_indexer, geo_expected): - geo_dataset.xoak.set_index(['lat', 'lon'], 'sklearn_geo_balltree') + geo_dataset.xoak.set_index(["lat", "lon"], "sklearn_geo_balltree") ds_sel = geo_dataset.xoak.sel(lat=geo_indexer.latitude, lon=geo_indexer.longitude) xr.testing.assert_equal(ds_sel.load(), geo_expected.load()) def test_sklearn_geo_balltree_options(): - ds = xr.Dataset(coords={'x': ('points', [1, 2]), 'y': ('points', [1, 2])}) + ds = xr.Dataset(coords={"x": ("points", [1, 2]), "y": ("points", [1, 2])}) - ds.xoak.set_index(['x', 'y'], 'sklearn_geo_balltree', leaf_size=10, metric='euclidean') + ds.xoak.set_index(["x", "y"], "sklearn_geo_balltree", leaf_size=10, metric="euclidean") # sklearn tree classes init options are not exposed as class properties # user-defined metric should be ignored - assert ds.xoak._index._index_adapter._index_options == {'leaf_size': 10, 'metric': 'haversine'} + assert ds.xoak._index._index_adapter._index_options == { + "leaf_size": 10, + "metric": "haversine", + }