-
Notifications
You must be signed in to change notification settings - Fork 780
docs: document array type support #3895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
0d4bfa0
docs: add array-types directive
flying-sheep ecb5676
prettier
flying-sheep bea1f95
All pp
flying-sheep 49a6e29
tl
flying-sheep aa29cac
relnote
flying-sheep 62fae97
fix noremalize-total
flying-sheep 153929c
nocov
flying-sheep 320f511
no csc in highly_variable_genes
flying-sheep a1f9ddc
Merge branch 'main' into pa/array-support-docs
flying-sheep 36fd335
CSS
flying-sheep a00e23b
centralize
flying-sheep 2e7c994
restructure
flying-sheep d35305e
overview
flying-sheep ce07344
short
flying-sheep c9c352b
Merge branch 'main' into pa/array-support-docs
flying-sheep f4a8e7b
Update docs/extensions/array_support.py
flying-sheep 60b344d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 6b86add
Merge branch 'main' into pa/array-support-docs
flying-sheep 1b4e305
link table and add hover title
flying-sheep edc661a
simplify
flying-sheep 0bce8e5
Merge branch 'main' into pa/array-support-docs
flying-sheep File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| code.literal > a > code.literal { | ||
| border: none; | ||
| padding: 0; | ||
| font-size: inherit; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| """Add `array-support` directive.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from itertools import groupby | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from docutils import nodes | ||
| from sphinx.util.docutils import SphinxDirective | ||
|
|
||
| from scanpy._utils import _docs | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Collection, Generator, Iterable, Sequence | ||
| from typing import ClassVar | ||
|
|
||
| from sphinx.application import Sphinx | ||
|
|
||
|
|
||
| ALL_INNER = list(_docs.parse(["np", "sp"], inner=True)) | ||
|
|
||
|
|
||
| class ArraySupport(SphinxDirective): | ||
| """Document array support.""" | ||
|
|
||
| required_arguments: ClassVar = 1 | ||
|
|
||
| @property | ||
| def _array_support(self) -> dict[str, tuple[list[str], list[str]]]: | ||
| return self.config.array_support | ||
|
|
||
| def run(self) -> list[nodes.Node]: # noqa: D102 | ||
| if self.arguments[0] == "all": | ||
| return self._render_overview() | ||
|
|
||
| if not self.arguments[0] not in self._array_support: | ||
| self.error( | ||
| f"API not in `array_support`, add it in `docs/conf.py`: {self.arguments[0]}" | ||
| ) | ||
| array_types = list(_docs.parse(*self._array_support[self.arguments[0]])) | ||
| headers = ( | ||
| "Array type", | ||
| "supported", | ||
| "… experimentally in dask :class:`~dask.array.Array`", | ||
| ) | ||
| data: list[tuple[_docs.Inner, bool, bool]] = [] | ||
| for array_type in ALL_INNER: | ||
| dask_array_type = _docs.DaskArray(array_type) | ||
| data.append(( | ||
| array_type, | ||
| array_type in array_types, | ||
| dask_array_type in array_types, | ||
| )) | ||
|
|
||
| title = nodes.title("", "", *self.parse_inline(":ref:`array-support`")[0]) | ||
| rows = self._render_support_data(data) | ||
| return self._render_table(headers, rows, title=title) | ||
|
|
||
| def _render_overview(self) -> list[nodes.Node]: | ||
| headers = ["Function", *(at.rst(short=True) for at in ALL_INNER)] | ||
| rows: list[nodes.row] = [] | ||
| for fn, (include, exclude) in self._array_support.items(): | ||
| row_header, _ = self.parse_inline(f":func:`scanpy.{fn}`") | ||
| ats = frozenset(_docs.parse(include, exclude)) | ||
| cells: list[Sequence[nodes.Node]] = [ | ||
| row_header, | ||
| *( | ||
| self._render_support(at in ats, dask=dt in ats) | ||
| for at, dt in zip( | ||
| ALL_INNER, map(_docs.DaskArray, ALL_INNER), strict=True | ||
| ) | ||
| ), | ||
| ] | ||
| rows.append( | ||
| nodes.row( | ||
| "", | ||
| *( | ||
| nodes.entry("", nodes.paragraph("", "", *cell)) | ||
| for cell in cells | ||
| ), | ||
| ) | ||
| ) | ||
| return self._render_table(headers, rows) | ||
|
|
||
| def _render_support_data( | ||
| self, | ||
| data: list[tuple[_docs.Inner, bool, bool]], | ||
| ) -> Generator[nodes.row, None, None]: | ||
| for t, group in groupby(data, key=lambda r: type(r[0])): | ||
| group = list(group) # noqa: PLW2901 | ||
| if ( # if all sparse types have the same support, just one row | ||
| t is _docs.ScipySparse | ||
| and (support := one({s for _, s, _ in group})) is not None | ||
| and (in_dask := one({d for _, _, d in group})) is not None | ||
| ): | ||
| refs: list[nodes.Node] = [ | ||
| nodes.inline("", "scipy.sparse.{"), | ||
| *self.parse_inline(":class:`csr <scipy.sparse.csr_array>`")[0], | ||
| nodes.inline("", ","), | ||
| *self.parse_inline(":class:`csc <scipy.sparse.csc_matrix>`")[0], | ||
| nodes.inline("", "}_{"), | ||
| *self.parse_inline(":class:`array <scipy.sparse.csc_array>`")[0], | ||
| nodes.inline("", ","), | ||
| *self.parse_inline(":class:`matrix <scipy.sparse.csr_matrix>`")[0], | ||
| nodes.inline("", "}"), | ||
| ] | ||
| header = [nodes.literal("", "", *refs)] | ||
| yield self._render_row(header, support=support, in_dask=in_dask) | ||
| else: # otherwise, show them individually | ||
| for array_type, support, in_dask in group: | ||
| yield self._render_row( | ||
| self._render_array_type(array_type), | ||
| support=support, | ||
| in_dask=in_dask, | ||
| ) | ||
|
|
||
| def _render_row( | ||
| self, header: Sequence[nodes.Node], *, support: bool, in_dask: bool | ||
| ) -> nodes.row: | ||
| cells: list[Sequence[nodes.Node]] = [ | ||
| header, | ||
| self._render_support(support), | ||
| self._render_support(in_dask), | ||
| ] | ||
| children = (nodes.entry("", nodes.paragraph("", "", *cell)) for cell in cells) | ||
| return nodes.row("", *children) | ||
|
|
||
| def _render_table( | ||
| self, | ||
| headers: Collection[str], | ||
| rows: Iterable[nodes.row], | ||
| *, | ||
| title: nodes.title | None = None, | ||
| ) -> list[nodes.Node]: | ||
| colspecs = [ | ||
| nodes.colspec(stub=True), | ||
| *(nodes.colspec() for _ in range(len(headers) - 1)), | ||
| ] | ||
| header_nodes = [ | ||
| nodes.entry("", nodes.paragraph("", "", *self.parse_inline(t)[0])) | ||
| for t in headers | ||
| ] | ||
| thead = nodes.thead("", nodes.row("", *header_nodes)) | ||
| tbody = nodes.tbody("", *rows) | ||
| return [ | ||
| nodes.table( | ||
| "", | ||
| *([title] if title else []), | ||
| nodes.tgroup("", *colspecs, thead, tbody, cols=len(colspecs)), | ||
| ids=["array-support"], | ||
| ) | ||
| ] | ||
|
|
||
| def _render_support( | ||
| self, | ||
| support: bool, # noqa: FBT001 | ||
| /, | ||
| *, | ||
| dask: bool = False, | ||
| ) -> Sequence[nodes.Node]: | ||
| dask_expl = "Also supports this type as chunk in a dask Array" | ||
| return [ | ||
| nodes.Text(("✅" if support else "❌") + " " * dask), | ||
| *([nodes.abbreviation(text="⚡", explanation=dask_expl)] if dask else []), | ||
| ] | ||
|
|
||
| def _render_array_type(self, array_type: _docs.ArrayType, /) -> list[nodes.Node]: | ||
| nodes_, msgs = self.parse_inline(array_type.rst()) | ||
| assert not msgs, msgs | ||
| return nodes_ | ||
|
|
||
|
|
||
| def one[T](arg: Collection[T]) -> T | None: | ||
| """Return the only item in `arg` or None if `arg` is not of length 1.""" | ||
| try: | ||
| [item] = arg | ||
| except ValueError: | ||
| return None | ||
| return item | ||
|
|
||
|
|
||
| def setup(app: Sphinx) -> None: | ||
| """App setup hook.""" | ||
| app.add_directive("array-support", ArraySupport) | ||
| app.add_config_value("array_support", {}, "env") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Document array type support for most functions in {mod}`~scanpy.pp` and {mod}`~scanpy.tl` {smaller}`P Angerer` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| """Utilities for `array-support` directive (see `/docs/extensions/array_support.py`).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from abc import ABC, abstractmethod | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, overload | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Collection, Generator | ||
| from typing import Literal | ||
|
|
||
|
|
||
| __all__ = ["ArrayType", "DaskArray", "Numpy", "ScipySparse", "parse"] | ||
|
|
||
|
|
||
| class ArrayType(ABC): | ||
| def rst(self, *, short: bool = False) -> str: # pragma: no cover | ||
| return f":class:`{'~' if short else ''}{self}`" | ||
|
|
||
| @abstractmethod | ||
| def __hash__(self) -> int: ... | ||
|
|
||
|
|
||
| @dataclass(unsafe_hash=True, frozen=True) | ||
| class Numpy(ArrayType): | ||
| def __str__(self) -> str: # pragma: no cover | ||
| return "numpy.ndarray" | ||
|
|
||
| def rst(self, *, short: bool = False) -> str: # pragma: no cover | ||
| return f":class:`{'~' if short else ''}{self}`" | ||
|
|
||
|
|
||
| @dataclass(unsafe_hash=True, frozen=True) | ||
| class ScipySparse(ArrayType): | ||
| format: Literal["csr", "csc"] | ||
|
|
||
| def __str__(self) -> str: # pragma: no cover | ||
| return f"scipy.sparse.{self.format}_{{array,matrix}}" | ||
|
|
||
| def rst(self, *, short: bool = False) -> str: # pragma: no cover | ||
| return ( | ||
| f":class:`{'~' if short else ''}scipy.sparse.{self.format}_array` / " | ||
| f":class:`~scipy.sparse.{self.format}_matrix`" | ||
| ) | ||
|
|
||
|
|
||
| type Inner = Numpy | ScipySparse | ||
|
|
||
|
|
||
| @dataclass(unsafe_hash=True, frozen=True) | ||
| class DaskArray(ArrayType): | ||
| chunk: Inner | ||
|
|
||
| def __str__(self) -> str: # pragma: no cover | ||
| return f"dask.array.Array[{self.chunk}]" | ||
|
|
||
| def rst(self, *, short: bool = False) -> str: # pragma: no cover | ||
| return rf":class:`{'~' if short else ''}dask.array.Array`\ \[{self.chunk.rst(short=short)}\]" | ||
|
|
||
|
|
||
| @overload | ||
| def parse( | ||
| include: Collection[str], | ||
| exclude: Collection[str] = (), | ||
| *, | ||
| inner: Literal[False] = False, | ||
| ) -> Generator[ArrayType]: ... | ||
| @overload | ||
| def parse( | ||
| include: Collection[str], exclude: Collection[str] = (), *, inner: Literal[True] | ||
| ) -> Generator[Inner]: ... | ||
| def parse( | ||
| include: Collection[str], exclude: Collection[str] = (), *, inner: bool = False | ||
| ) -> Generator[ArrayType]: | ||
| if exclude: | ||
| excluded = dict.fromkeys(parse(exclude)).keys() | ||
| yield from (t for t in parse(include) if t not in excluded) | ||
| return | ||
|
|
||
| inner_includes = [i for i in include if not i.startswith("da")] | ||
| for t in include: | ||
| if ( | ||
| match := re.fullmatch(r"([^\[]+)(?:\[(.+)\])?", t) | ||
| ) is None: # pragma: no cover | ||
| msg = f"invalid {t!r}" | ||
| raise ValueError(msg) | ||
| mod, tags = match.groups("") | ||
| if mod == "da" and inner: # pragma: no cover | ||
| msg = "Can’t nest dask arrays" | ||
| raise ValueError(msg) | ||
| tags = set(re.split(r",(?![^\[]+\])", tags)) if tags else set() | ||
| yield from _parse_mod(mod, tags, inner_includes=inner_includes) | ||
|
|
||
|
|
||
| def _parse_mod( | ||
| mod: str, tags: set[str], *, inner_includes: Collection[str] | ||
| ) -> Generator[ArrayType]: | ||
| match mod: | ||
| case "np": | ||
| if tags: # pragma: no cover | ||
| msg = f"`np` takes no tags {tags!r}" | ||
| raise ValueError(msg) | ||
| yield Numpy() | ||
| case "sp": | ||
| if tags - {"csr", "csc"}: # pragma: no cover | ||
| msg = f"invalid tags {tags!r}" | ||
| raise ValueError(msg) | ||
| for format in ("csr", "csc"): | ||
| if tags & {"csr", "csc"} and format not in tags: | ||
| continue | ||
| yield ScipySparse(format=format) | ||
| case "da": | ||
| for chunk in parse(tags if tags else inner_includes, inner=True): | ||
| yield DaskArray(chunk=chunk) | ||
| case _: # pragma: no cover | ||
| msg = f"invalid module {mod!r}" | ||
| raise ValueError(msg) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.