diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 43ded24c..00000000 --- a/.flake8 +++ /dev/null @@ -1,62 +0,0 @@ -# Can't yet be moved to the pyproject.toml due to https://github.com/PyCQA/flake8/issues/234 -[flake8] -max-line-length = 120 -ignore = - # line break before a binary operator -> black does not adhere to PEP8 - W503 - # line break occured after a binary operator -> black does not adhere to PEP8 - W504 - # line too long -> we accept long comment lines; black gets rid of long code lines - E501 - # whitespace before : -> black does not adhere to PEP8 - E203 - # line break before binary operator -> black does not adhere to PEP8 - W503 - # missing whitespace after ,', ';', or ':' -> black does not adhere to PEP8 - E231 - # continuation line over-indented for hanging indent -> black does not adhere to PEP8 - E126 - # too many leading '#' for block comment -> this is fine for indicating sections - E262 - # 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 - E741 - # Missing docstring in public package - D104 - # Missing docstring in public module - D100 - # Missing docstring in __init__ - D107 - # Errors from function calls in argument defaults. These are fine when the result is immutable. - B008 - # Missing docstring in magic method - D105 - # format string does contain unindexed parameters - P101 - # first line should end with a period [Bug: doesn't work with single-line docstrings] - D400 - # First line should be in imperative mood; try rephrasing - D401 - # Abstract base class without abstractmethod. - B024 -exclude = .git,__pycache__,build,docs/_build,dist -per-file-ignores = - tests/*: D - */__init__.py: F401 - src/spatialdata_io/_constants/_enum.py: RST, D -rst-roles = - class, - func, - ref, - attr, - cite:p, - cite:t, -rst-directives = - envvar, - exception, - seealso, -rst-substitutions = - version, -extend-ignore = - RST307, diff --git a/.github/workflows/prepare_test_data.yaml b/.github/workflows/prepare_test_data.yaml index bf979d96..728d12c3 100644 --- a/.github/workflows/prepare_test_data.yaml +++ b/.github/workflows/prepare_test_data.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download test datasets run: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 299ed867..5fc710d5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c4d08869..9dc14695 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,9 +26,9 @@ jobs: PYTHON: ${{ matrix.python }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -37,7 +37,7 @@ jobs: run: | echo "::set-output name=dir::$(pip cache dir)" - name: Restore pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: pip-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/pyproject.toml') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a133e82c..6f2127a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,39 +6,22 @@ default_stages: - pre-push minimum_pre_commit_version: 2.16.0 repos: - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - repo: https://github.com/rbubley/mirrors-prettier rev: v3.5.1 hooks: - id: prettier - - repo: https://github.com/asottile/blacken-docs - rev: 1.19.1 - hooks: - - id: blacken-docs - - repo: https://github.com/PyCQA/isort - rev: 6.0.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.2 hooks: - - id: isort + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: - id: mypy additional_dependencies: [numpy, types-PyYAML] exclude: docs/ - - repo: https://github.com/asottile/yesqa - rev: v1.5.0 - hooks: - - id: yesqa - additional_dependencies: - - flake8-tidy-imports - - flake8-docstrings - - flake8-rst-docstrings - - flake8-comprehensions - - flake8-bugbear - - flake8-blind-except - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: @@ -49,31 +32,6 @@ repos: args: [--fix=lf] - id: trailing-whitespace - id: check-case-conflict - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: - - --in-place - - --remove-all-unused-imports - - --remove-unused-variable - - --ignore-init-module-imports - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 - hooks: - - id: flake8 - additional_dependencies: - - flake8-tidy-imports - - flake8-docstrings - - flake8-rst-docstrings - - flake8-comprehensions - - flake8-bugbear - - flake8-blind-except - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 - hooks: - - id: pyupgrade - args: [--py3-plus, --py310-plus, --keep-runtime-typing] - repo: local hooks: - id: forbid-to-commit @@ -83,7 +41,3 @@ repos: Fix the merge conflicts manually and remove the .rej files. language: fail files: '.*\.rej$' - - repo: https://github.com/PyCQA/doc8 - rev: v1.1.2 - hooks: - - id: doc8 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f3a78576..31d8fef4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,9 +1,8 @@ -# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: "3.10" + python: "3.12" sphinx: configuration: docs/conf.py fail_on_warning: true diff --git a/docs/conf.py b/docs/conf.py index 24e42ba9..5a10a5b4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ from datetime import datetime from importlib.metadata import metadata from pathlib import Path + import spatialdata_io.experimental _ = spatialdata_io.experimental diff --git a/docs/extensions/typed_returns.py b/docs/extensions/typed_returns.py index 94478130..d044c698 100644 --- a/docs/extensions/typed_returns.py +++ b/docs/extensions/typed_returns.py @@ -11,7 +11,7 @@ def _process_return(lines): m = re.fullmatch(r"(?P\w+)\s+:\s+(?P[\w.]+)", line) if m: # Once this is in scanpydoc, we can use the fancy hover stuff - yield f'-{m["param"]} (:class:`~{m["type"]}`)' + yield f"-{m['param']} (:class:`~{m['type']}`)" else: yield line diff --git a/pyproject.toml b/pyproject.toml index 81f0ae28..fefc1a37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,32 +78,108 @@ addopts = [ "--import-mode=importlib", # allow using test files with same name ] -[tool.isort] -include_trailing_comma = true -multi_line_output = 3 -profile = "black" -skip_glob = ["docs/*"] - -[tool.black] +[tool.ruff] line-length = 120 -target-version = ['py310'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ -) -''' +exclude = [ + ".git", + ".tox", + "__pycache__", + "build", + "docs/_build", + "dist", + "setup.py", +] +lint.select = [ + "F", # Errors detected by Pyflakes + "E", # Error detected by Pycodestyle + "W", # Warning detected by Pycodestyle + "I", # isort + "D", # pydocstyle + "B", # flake8-bugbear + "TID", # flake8-tidy-imports + "C4", # flake8-comprehensions + "BLE", # flake8-blind-except + "UP", # pyupgrade + "RUF100", # Report unused noqa directives + "TCH", # Typing imports + "NPY", # Numpy specific rules + # "PTH", # Use pathlib + # "S" # Security +] +lint.ignore = [ + # Do not catch blind exception: `Exception` + "BLE001", + # Errors from function calls in argument defaults. These are fine when the result is immutable. + "B008", + # line too long -> we accept long comment lines; black gets rid of long code lines + "E501", + # 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 + "E741", + # Missing docstring in public module + "D100", + # undocumented-public-class + "D101", + # Missing docstring in public method + "D102", + # Missing docstring in public function + "D103", + # Missing docstring in public package + "D104", + # __magic__ methods are are often self-explanatory, allow missing docstrings + "D105", + # Missing docstring in public nested class + "D106", + # Missing docstring in __init__ + "D107", + ## Disable one in each pair of mutually incompatible rules + # We don’t want a blank line before a class docstring + "D203", + # 1 blank line required after class docstring + "D204", + # first line should end with a period [Bug: doesn't work with single-line docstrings] + # We want docstrings to start immediately after the opening triple quote + "D213", + # Section underline is over-indented ("{name}") + "D215", + # First line should be in imperative mood; try rephrasing + "D401", + # First word of the first line should be capitalized: {} -> {} + "D403", + # First word of the docstring should not be "This" + "D404", + # Section name should end with a newline ("{name}") + "D406", + # Missing dashed underline after section ("{name}") + "D407", + # Section underline should be in the line following the section's name ("{name}") + "D408", + # Section underline should match the length of its name ("{name}") + "D409", + # No blank lines allowed between a section header and its content ("{name}") + "D412", + # Missing blank line after last section ("{name}") + "D413", + # Missing argument description in the docstring + "D417", + # camcelcase imported as lowercase + "N813", + # module import not at top level of file + "E402", + # open()` should be replaced by `Path.open() + "PTH123", + # subprocess` call: check for execution of untrusted input - https://github.com/PyCQA/bandit/issues/333 + "S603", + # Starting a process with a partial executable path + "S607", + # Prefer absolute imports over relative imports from parent modules + "TID252", + # Standard pseudo-random generators are not suitable for cryptographic purposes + "S311", + # Unused imports + "F401", +] [tool.jupytext] formats = "ipynb,md" diff --git a/src/spatialdata_io/__main__.py b/src/spatialdata_io/__main__.py index 1d2f714a..c65e46f4 100644 --- a/src/spatialdata_io/__main__.py +++ b/src/spatialdata_io/__main__.py @@ -29,8 +29,7 @@ @click.group() def cli() -> None: - """ - Convert standard technology data formats to SpatialData object. + """Convert standard technology data formats to SpatialData object. Usage: @@ -66,7 +65,7 @@ def _input_output_click_options(func: Callable[..., None]) -> Callable[..., None help="Whether the .fcs file is provided if False a .csv file is expected. [default: True]", ) def codex_wrapper(input: str, output: str, fcs: bool = True) -> None: - """Codex conversion to SpatialData""" + """Codex conversion to SpatialData.""" sdata = codex(input, fcs=fcs) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -76,7 +75,7 @@ def codex_wrapper(input: str, output: str, fcs: bool = True) -> None: @click.option("--dataset-id", type=str, default=None, help="Name of the dataset [default: None]") @click.option("--transcripts", type=bool, default=True, help="Whether to load transcript information. [default: True]") def cosmx_wrapper(input: str, output: str, dataset_id: str | None = None, transcripts: bool = True) -> None: - """Cosmic conversion to SpatialData""" + """Cosmic conversion to SpatialData.""" sdata = cosmx(input, dataset_id=dataset_id, transcripts=transcripts) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -84,7 +83,7 @@ def cosmx_wrapper(input: str, output: str, dataset_id: str | None = None, transc @cli.command(name="curio") @_input_output_click_options def curio_wrapper(input: str, output: str) -> None: - """Curio conversion to SpatialData""" + """Curio conversion to SpatialData.""" sdata = curio(input) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -117,7 +116,7 @@ def dbit_wrapper( border: bool = True, border_scale: float = 1, ) -> None: - """Conversion of DBit-seq to SpatialData""" + """Conversion of DBit-seq to SpatialData.""" sdata = dbit( # type: ignore[name-defined] # noqa: F821 input, anndata_path=anndata_path, @@ -174,7 +173,7 @@ def iss_wrapper( multiscale_image: bool = True, multiscale_labels: bool = True, ) -> None: - """ISS conversion to SpatialData""" + """ISS conversion to SpatialData.""" sdata = iss( # type: ignore[name-defined] # noqa: F821 input, raw_relative_path, @@ -194,7 +193,7 @@ def iss_wrapper( ) @click.option("--output", "-o", type=click.Path(), help="Path to the output.zarr file.", required=True) def mcmicro_wrapper(input: str, output: str) -> None: - """Conversion of MCMicro to SpatialData""" + """Conversion of MCMicro to SpatialData.""" sdata = mcmicro(input) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -233,7 +232,7 @@ def merscope_wrapper( cells_table: bool = True, mosaic_images: bool = True, ) -> None: - """Merscope conversion to SpatialData""" + """Merscope conversion to SpatialData.""" sdata = merscope( # type: ignore[name-defined] # noqa: F821 input, vpt_outputs=vpt_outputs, @@ -273,7 +272,7 @@ def seqfish_wrapper( cells_as_circles: bool = False, rois: list[int] | None = None, ) -> None: - """Seqfish conversion to SpatialData""" + """Seqfish conversion to SpatialData.""" rois = list(rois) if rois else None sdata = seqfish( # type: ignore[name-defined] # noqa: F821 input, @@ -296,7 +295,7 @@ def seqfish_wrapper( help="What kind of labels to use. [default: 'deepcell']", ) def steinbock_wrapper(input: str, output: str, labels_kind: Literal["deepcell", "ilastik"] = "deepcell") -> None: - """Steinbock conversion to SpatialData""" + """Steinbock conversion to SpatialData.""" sdata = steinbock(input, labels_kind=labels_kind) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -320,7 +319,7 @@ def stereoseq_wrapper( read_square_bin: bool = True, optional_tif: bool = False, ) -> None: - """Stereoseq conversion to SpatialData""" + """Stereoseq conversion to SpatialData.""" sdata = stereoseq(input, dataset_id=dataset_id, read_square_bin=read_square_bin, optional_tif=optional_tif) # type: ignore[name-defined] # noqa: F821 sdata.write(output) @@ -361,7 +360,7 @@ def visium_wrapper( tissue_positions_file: str | Path | None = None, scalefactors_file: str | Path | None = None, ) -> None: - """Visium conversion to SpatialData""" + """Visium conversion to SpatialData.""" sdata = visium( # type: ignore[name-defined] # noqa: F821 input, dataset_id=dataset_id, @@ -424,7 +423,7 @@ def visium_hd_wrapper( load_all_images: bool = False, annotate_table_by_labels: bool = False, ) -> None: - """Visium HD conversion to SpatialData""" + """Visium HD conversion to SpatialData.""" sdata = visium_hd( # type: ignore[name-defined] # noqa: F821 path=input, dataset_id=dataset_id, @@ -483,7 +482,7 @@ def xenium_wrapper( cells_table: bool = True, n_jobs: int = 1, ) -> None: - """Xenium conversion to SpatialData""" + """Xenium conversion to SpatialData.""" sdata = xenium( # type: ignore[name-defined] # noqa: F821 input, cells_boundaries=cells_boundaries, @@ -637,7 +636,7 @@ def read_generic_wrapper( data_axes: str | None = None, coordinate_system: str | None = None, ) -> None: - """Read generic data to SpatialData""" + """Read generic data to SpatialData.""" if data_axes is not None and "".join(sorted(data_axes)) not in ["cxy", "cxyz"]: raise ValueError("data_axes must be a permutation of 'cyx' or 'czyx'.") generic_to_zarr(input=input, output=output, name=name, data_axes=data_axes, coordinate_system=coordinate_system) diff --git a/src/spatialdata_io/_constants/_constants.py b/src/spatialdata_io/_constants/_constants.py index 70b09e63..33795f92 100644 --- a/src/spatialdata_io/_constants/_constants.py +++ b/src/spatialdata_io/_constants/_constants.py @@ -301,7 +301,7 @@ class McmicroKeys(ModeEnum): @unique class MerscopeKeys(ModeEnum): - """Keys for *MERSCOPE* data (Vizgen plateform)""" + """Keys for *MERSCOPE* data (Vizgen plateform).""" # files and directories IMAGES_DIR = "images" diff --git a/src/spatialdata_io/_constants/_enum.py b/src/spatialdata_io/_constants/_enum.py index 55decf37..764f5efd 100644 --- a/src/spatialdata_io/_constants/_enum.py +++ b/src/spatialdata_io/_constants/_enum.py @@ -42,7 +42,7 @@ def wrapper(*args: Any, **kwargs: Any) -> "ErrorFormatterABC": class ABCEnumMeta(EnumMeta, ABCMeta): def __call__(cls, *args: Any, **kwargs: Any) -> Any: if getattr(cls, "__error_format__", None) is None: - raise TypeError(f"Can't instantiate class `{cls.__name__}` " f"without `__error_format__` class attribute.") + raise TypeError(f"Can't instantiate class `{cls.__name__}` without `__error_format__` class attribute.") return super().__call__(*args, **kwargs) def __new__(cls, clsname: str, superclasses: tuple[type], attributedict: dict[str, Any]) -> "ABCEnumMeta": @@ -51,7 +51,7 @@ def __new__(cls, clsname: str, superclasses: tuple[type], attributedict: dict[st return res -class ErrorFormatterABC(ABC): +class ErrorFormatterABC(ABC): # noqa """Mixin class that formats invalid value when constructing an enum.""" __error_format__ = "Invalid option `{0}` for `{1}`. Valid options are: `{2}`." @@ -59,7 +59,9 @@ class ErrorFormatterABC(ABC): @classmethod def _format(cls, value: Enum) -> str: return cls.__error_format__.format( - value, cls.__name__, [m.value for m in cls.__members__.values()] # type: ignore[attr-defined] + value, + cls.__name__, + [m.value for m in cls.__members__.values()], # type: ignore[attr-defined] ) diff --git a/src/spatialdata_io/_docs.py b/src/spatialdata_io/_docs.py index 34efbd5e..958d8f67 100644 --- a/src/spatialdata_io/_docs.py +++ b/src/spatialdata_io/_docs.py @@ -1,11 +1,13 @@ from __future__ import annotations -from collections.abc import Callable from textwrap import dedent -from typing import Any +from typing import TYPE_CHECKING, Any +if TYPE_CHECKING: + from collections.abc import Callable -def inject_docs(**kwargs: Any) -> Callable[..., Any]: # noqa: D103 + +def inject_docs(**kwargs: Any) -> Callable[..., Any]: # taken from scanpy def decorator(obj: Any) -> Any: obj.__doc__ = dedent(obj.__doc__).format(**kwargs) diff --git a/src/spatialdata_io/_utils.py b/src/spatialdata_io/_utils.py index 90dee2ac..c73a4d69 100644 --- a/src/spatialdata_io/_utils.py +++ b/src/spatialdata_io/_utils.py @@ -2,8 +2,10 @@ import functools import warnings -from collections.abc import Callable -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar + +if TYPE_CHECKING: + from collections.abc import Callable RT = TypeVar("RT") @@ -11,8 +13,7 @@ # these two functions should be removed and imported from spatialdata._utils once the multi_table branch, which # introduces them, is merged def deprecation_alias(**aliases: str) -> Callable[[Callable[..., RT]], Callable[..., RT]]: - """ - Decorate a function to warn user of use of arguments set for deprecation. + """Decorate a function to warn user of use of arguments set for deprecation. Parameters ---------- diff --git a/src/spatialdata_io/converters/generic_to_zarr.py b/src/spatialdata_io/converters/generic_to_zarr.py index 11c5c658..eaf4f0f8 100644 --- a/src/spatialdata_io/converters/generic_to_zarr.py +++ b/src/spatialdata_io/converters/generic_to_zarr.py @@ -21,8 +21,7 @@ def generic_to_zarr( data_axes: str | None = None, coordinate_system: str | None = None, ) -> None: - """ - Read generic data from an input file and save it as a SpatialData zarr store. + """Read generic data from an input file and save it as a SpatialData zarr store. Parameters ---------- diff --git a/src/spatialdata_io/converters/legacy_anndata.py b/src/spatialdata_io/converters/legacy_anndata.py index ed0806c2..3dcf05ee 100644 --- a/src/spatialdata_io/converters/legacy_anndata.py +++ b/src/spatialdata_io/converters/legacy_anndata.py @@ -1,9 +1,9 @@ from __future__ import annotations import warnings +from typing import TYPE_CHECKING import numpy as np -from anndata import AnnData from spatialdata import ( SpatialData, get_centroids, @@ -15,6 +15,9 @@ from spatialdata.models import Image2DModel, ShapesModel, TableModel, get_table_keys from spatialdata.transformations import Identity, Scale +if TYPE_CHECKING: + from anndata import AnnData + def to_legacy_anndata( sdata: SpatialData, @@ -22,8 +25,7 @@ def to_legacy_anndata( table_name: str | None = None, include_images: bool = False, ) -> AnnData: - """ - Convert a SpatialData object to a (legacy) spatial AnnData object. + """Convert a SpatialData object to a (legacy) spatial AnnData object. This is useful for using packages expecting spatial information in AnnData, for example Scanpy and older versions of Squidpy. Using this format for any new package is not recommended. @@ -113,9 +115,9 @@ def to_legacy_anndata( assert len(css) == 1, "The SpatialData object has more than one coordinate system. Please specify one." coordinate_system = css[0] else: - assert ( - coordinate_system in css - ), f"The SpatialData object does not have the coordinate system {coordinate_system}." + assert coordinate_system in css, ( + f"The SpatialData object does not have the coordinate system {coordinate_system}." + ) sdata = sdata.filter_by_coordinate_system(coordinate_system) if table_name is None: @@ -219,8 +221,7 @@ def to_legacy_anndata( def from_legacy_anndata(adata: AnnData) -> SpatialData: - """ - Convert (legacy) spatial AnnData object to SpatialData object. + """Convert (legacy) spatial AnnData object to SpatialData object. This is useful for parsing a (legacy) spatial AnnData object, for example the ones produced by Scanpy and older version of Squidpy. @@ -288,9 +289,9 @@ def from_legacy_anndata(adata: AnnData) -> SpatialData: # construct the spatialdata elements if hires is not None: # prepare the hires image - assert ( - tissue_hires_scalef is not None - ), "tissue_hires_scalef is required when an the hires image is present" + assert tissue_hires_scalef is not None, ( + "tissue_hires_scalef is required when an the hires image is present" + ) hires_image = Image2DModel.parse( hires, dims=("y", "x", "c"), transformations={f"{dataset_id}_downscaled_hires": Identity()} ) @@ -301,9 +302,9 @@ def from_legacy_anndata(adata: AnnData) -> SpatialData: shapes_transformations[f"{dataset_id}_downscaled_hires"] = scale_hires if lowres is not None: # prepare the lowres image - assert ( - tissue_lowres_scalef is not None - ), "tissue_lowres_scalef is required when an the lowres image is present" + assert tissue_lowres_scalef is not None, ( + "tissue_lowres_scalef is required when an the lowres image is present" + ) lowres_image = Image2DModel.parse( lowres, dims=("y", "x", "c"), transformations={f"{dataset_id}_downscaled_lowres": Identity()} ) diff --git a/src/spatialdata_io/readers/_utils/_read_10x_h5.py b/src/spatialdata_io/readers/_utils/_read_10x_h5.py index 4c5b33cc..a7ffa566 100644 --- a/src/spatialdata_io/readers/_utils/_read_10x_h5.py +++ b/src/spatialdata_io/readers/_utils/_read_10x_h5.py @@ -44,8 +44,7 @@ def _read_10x_h5( genome: str | None = None, gex_only: bool = True, ) -> AnnData: - """ - Read 10x-Genomics-formatted hdf5 file. + """Read 10x-Genomics-formatted hdf5 file. Parameters ---------- @@ -84,7 +83,7 @@ def _read_10x_h5( if genome not in adata.var["genome"].values: raise ValueError( f"Could not find data corresponding to genome `{genome}` in `{filename}`. " - f'Available genomes are: {list(adata.var["genome"].unique())}.' + f"Available genomes are: {list(adata.var['genome'].unique())}." ) adata = adata[:, adata.var["genome"] == genome] if gex_only: @@ -126,7 +125,7 @@ def _read_v3_10x_h5(filename: str | Path, *, start: Any | None = None) -> AnnDat ) return adata except KeyError: - raise Exception("File is missing one or more required datasets.") + raise Exception("File is missing one or more required datasets.") from None def _collect_datasets(dsets: dict[str, Any], group: h5py.Group) -> None: diff --git a/src/spatialdata_io/readers/_utils/_utils.py b/src/spatialdata_io/readers/_utils/_utils.py index 82676098..6072fbc8 100644 --- a/src/spatialdata_io/readers/_utils/_utils.py +++ b/src/spatialdata_io/readers/_utils/_utils.py @@ -1,11 +1,10 @@ from __future__ import annotations import os -from collections.abc import Mapping from pathlib import Path -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union -from anndata import AnnData, read_text +from anndata.io import read_text from h5py import File from ome_types import from_tiff from ome_types.model import Pixels, UnitsLength @@ -13,7 +12,12 @@ from spatialdata_io.readers._utils._read_10x_h5 import _read_10x_h5 -PathLike = Union[os.PathLike, str] # type:ignore[type-arg] +if TYPE_CHECKING: + from collections.abc import Mapping + + from anndata import AnnData + +PathLike = os.PathLike | str # type:ignore[type-arg] def _read_counts( @@ -53,7 +57,7 @@ def _read_counts( try: from scanpy.readwrite import read_10x_mtx except ImportError: - raise ImportError("Please install scanpy to read 10x mtx files, `pip install scanpy`.") + raise ImportError("Please install scanpy to read 10x mtx files, `pip install scanpy`.") from None prefix = counts_file.replace("matrix.mtx.gz", "") adata = read_10x_mtx(path, prefix=prefix, **kwargs) else: diff --git a/src/spatialdata_io/readers/codex.py b/src/spatialdata_io/readers/codex.py index 3fd7a6d8..ef36b01c 100644 --- a/src/spatialdata_io/readers/codex.py +++ b/src/spatialdata_io/readers/codex.py @@ -2,10 +2,9 @@ import os import re -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad import pandas as pd @@ -18,6 +17,9 @@ from spatialdata_io._constants._constants import CodexKeys from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["codex"] @@ -27,8 +29,7 @@ def codex( fcs: bool = True, imread_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *CODEX* formatted dataset. + """Read *CODEX* formatted dataset. This function reads the following files: diff --git a/src/spatialdata_io/readers/cosmx.py b/src/spatialdata_io/readers/cosmx.py index 561fe914..a3960168 100644 --- a/src/spatialdata_io/readers/cosmx.py +++ b/src/spatialdata_io/readers/cosmx.py @@ -2,17 +2,15 @@ import os import re -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import dask.array as da import numpy as np import pandas as pd import pyarrow as pa from anndata import AnnData -from dask.dataframe import DataFrame as DaskDataFrame from dask_image.imread import imread from scipy.sparse import csr_matrix from skimage.transform import estimate_transform @@ -24,6 +22,11 @@ from spatialdata_io._constants._constants import CosmxKeys from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Mapping + + from dask.dataframe import DataFrame as DaskDataFrame + __all__ = ["cosmx"] @@ -35,8 +38,7 @@ def cosmx( imread_kwargs: Mapping[str, Any] = MappingProxyType({}), image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *Cosmx Nanostring* data. + """Read *Cosmx Nanostring* data. This function reads the following files: diff --git a/src/spatialdata_io/readers/curio.py b/src/spatialdata_io/readers/curio.py index d0d9d1a9..55b0b00e 100644 --- a/src/spatialdata_io/readers/curio.py +++ b/src/spatialdata_io/readers/curio.py @@ -19,8 +19,7 @@ def curio( path: str | Path, ) -> SpatialData: - """ - Read *Curio* formatted dataset. + """Read *Curio* formatted dataset. This function reads the following files: @@ -73,7 +72,7 @@ def curio( categories = metrics[CurioKeys.CATEGORY].unique() for cat in categories: df = metrics.loc[metrics[CurioKeys.CATEGORY] == cat] - adata.uns[cat] = dict(zip(df.iloc[:, 0], df.iloc[:, 1])) + adata.uns[cat] = dict(zip(df.iloc[:, 0], df.iloc[:, 1], strict=False)) adata.uns[CurioKeys.TOP_CLUSTER_DEFINING_FEATURES] = var_features_clusters # adding Moran's I information in adata.var, for the variable for which it is available diff --git a/src/spatialdata_io/readers/dbit.py b/src/spatialdata_io/readers/dbit.py index 4fc1afdb..bd4d20d2 100644 --- a/src/spatialdata_io/readers/dbit.py +++ b/src/spatialdata_io/readers/dbit.py @@ -4,6 +4,7 @@ import re from pathlib import Path from re import Pattern +from typing import TYPE_CHECKING import anndata as ad import numpy as np @@ -11,7 +12,6 @@ import shapely import spatialdata as sd from dask_image.imread import imread -from numpy.typing import NDArray from spatialdata import SpatialData from spatialdata._logging import logger from xarray import DataArray @@ -19,6 +19,9 @@ from spatialdata_io._constants._constants import DbitKeys from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from numpy.typing import NDArray + __all__ = ["dbit"] @@ -29,8 +32,7 @@ def _check_path( path_specific: str | Path | None = None, optional_arg: bool = False, ) -> tuple[Path | None, bool]: - """ - Check that the path is valid and match a regex pattern. + """Check that the path is valid and match a regex pattern. Parameters ---------- @@ -104,15 +106,14 @@ def _check_path( logger.warning(message) return file_path, flag else: - raise IndexError(message) + raise IndexError(message) from None logger.warning(f"{file_path} is used.") return file_path, flag def _barcode_check(barcode_file: Path) -> pd.DataFrame: - """ - Check that the barcode file is formatted as expected. + """Check that the barcode file is formatted as expected. What do we expect : A tab separated file, headless, with 2 columns: @@ -176,8 +177,7 @@ def _barcode_check(barcode_file: Path) -> pd.DataFrame: def _xy2edges(xy: list[int], scale: float = 1.0, border: bool = True, border_scale: float = 1) -> NDArray[np.double]: - """ - Construct vertex coordinate of a square from the barcode coordinates. + """Construct vertex coordinate of a square from the barcode coordinates. The constructed square has a scalable border. @@ -225,8 +225,7 @@ def dbit( border: bool = True, border_scale: float = 1, ) -> SpatialData: - """ - Read DBiT experiment data (Deterministic Barcoding in Tissue) + """Read DBiT experiment data (Deterministic Barcoding in Tissue). This function reads the following files: @@ -279,10 +278,16 @@ def dbit( # search for files paths. Gives priority to files matching the pattern found in path. anndata_path_checked = _check_path( - path=path, path_specific=anndata_path, pattern=patt_h5ad, key=DbitKeys.COUNTS_FILE # type: ignore + path=path, # type: ignore + path_specific=anndata_path, + pattern=patt_h5ad, + key=DbitKeys.COUNTS_FILE, )[0] barcode_position_checked = _check_path( - path=path, path_specific=barcode_position, pattern=patt_barcode, key=DbitKeys.BARCODE_POSITION # type: ignore + path=path, # type: ignore + path_specific=barcode_position, + pattern=patt_barcode, + key=DbitKeys.BARCODE_POSITION, )[0] image_path_checked, hasimage = _check_path( path=path, # type: ignore diff --git a/src/spatialdata_io/readers/generic.py b/src/spatialdata_io/readers/generic.py index 3ada671d..904b94dc 100644 --- a/src/spatialdata_io/readers/generic.py +++ b/src/spatialdata_io/readers/generic.py @@ -1,17 +1,21 @@ from __future__ import annotations import warnings -from collections.abc import Sequence from pathlib import Path +from typing import TYPE_CHECKING import numpy as np from dask_image.imread import imread -from geopandas import GeoDataFrame from spatialdata._docs import docstring_parameter from spatialdata.models import Image2DModel, ShapesModel from spatialdata.models._utils import DEFAULT_COORDINATE_SYSTEM from spatialdata.transformations import Identity -from xarray import DataArray + +if TYPE_CHECKING: + from collections.abc import Sequence + + from geopandas import GeoDataFrame + from xarray import DataArray VALID_IMAGE_TYPES = [".tif", ".tiff", ".png", ".jpg", ".jpeg"] VALID_SHAPE_TYPES = [".geojson"] @@ -29,8 +33,7 @@ def generic( data_axes: Sequence[str] | None = None, coordinate_system: str | None = None, ) -> DataArray | GeoDataFrame: - """ - Read a generic shapes or image file and save it as SpatialData zarr. + """Read a generic shapes or image file and save it as SpatialData zarr. Supported image types: {valid_image_types}. Supported shape types: {valid_shape_types} (only Polygons and MultiPolygons are supported). @@ -66,12 +69,12 @@ def generic( def geojson(input: Path, coordinate_system: str) -> GeoDataFrame: - """Reads a GeoJSON file and returns a parsed GeoDataFrame spatial element""" + """Reads a GeoJSON file and returns a parsed GeoDataFrame spatial element.""" return ShapesModel.parse(input, transformations={coordinate_system: Identity()}) def image(input: Path, data_axes: Sequence[str], coordinate_system: str) -> DataArray: - """Reads an image file and returns a parsed Image2D spatial element""" + """Reads an image file and returns a parsed Image2D spatial element.""" # this function is just a draft, the more general one will be available when # https://github.com/scverse/spatialdata-io/pull/234 is merged image = imread(input) diff --git a/src/spatialdata_io/readers/iss.py b/src/spatialdata_io/readers/iss.py index 78547f9e..63c86571 100644 --- a/src/spatialdata_io/readers/iss.py +++ b/src/spatialdata_io/readers/iss.py @@ -1,9 +1,8 @@ from __future__ import annotations -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad from dask_image.imread import imread @@ -14,6 +13,9 @@ from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["iss"] @@ -31,8 +33,7 @@ def iss( image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), labels_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *Sanger ISS* formatted dataset. + """Read *Sanger ISS* formatted dataset. This function reads the following files: diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index 58e962eb..8bbe0532 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -2,12 +2,11 @@ import warnings from collections import defaultdict -from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad import dask.array as da @@ -24,6 +23,9 @@ parse_physical_size, ) +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["macsima"] @@ -185,8 +187,7 @@ def macsima( skip_rounds: list[int] | None = None, include_cycle_in_channel_name: bool = False, ) -> SpatialData: - """ - Read *MACSima* formatted dataset. + """Read *MACSima* formatted dataset. This function reads images from a MACSima cyclic imaging experiment. Metadata of the cycle rounds is parsed from the image names. The channel names are parsed from the OME metadata. diff --git a/src/spatialdata_io/readers/mcmicro.py b/src/spatialdata_io/readers/mcmicro.py index 04096c71..f6cca509 100644 --- a/src/spatialdata_io/readers/mcmicro.py +++ b/src/spatialdata_io/readers/mcmicro.py @@ -1,10 +1,9 @@ from __future__ import annotations import re -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad import numpy as np @@ -12,8 +11,6 @@ import yaml from anndata import AnnData from dask_image.imread import imread -from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage -from spatial_image import SpatialImage from spatialdata import SpatialData from spatialdata.models import Image2DModel, Labels2DModel, TableModel from spatialdata.transformations import Identity, Translation, set_transformation @@ -21,6 +18,12 @@ from spatialdata_io._constants._constants import McmicroKeys +if TYPE_CHECKING: + from collections.abc import Mapping + + from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage + from spatial_image import SpatialImage + __all__ = ["mcmicro"] @@ -48,8 +51,7 @@ def mcmicro( image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), labels_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read a *Mcmicro* output into a SpatialData object. + """Read a *Mcmicro* output into a SpatialData object. .. seealso:: diff --git a/src/spatialdata_io/readers/merscope.py b/src/spatialdata_io/readers/merscope.py index fe0f73fb..3c20639b 100644 --- a/src/spatialdata_io/readers/merscope.py +++ b/src/spatialdata_io/readers/merscope.py @@ -2,10 +2,9 @@ import re import warnings -from collections.abc import Callable, Mapping from pathlib import Path from types import MappingProxyType -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import anndata import dask.dataframe as dd @@ -24,6 +23,9 @@ from spatialdata_io._constants._constants import MerscopeKeys from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + SUPPORTED_BACKENDS = ["dask_image", "rioxarray"] @@ -37,8 +39,7 @@ def _get_channel_names(images_dir: Path) -> list[str]: def _get_file_paths(path: Path, vpt_outputs: Path | str | dict[str, Any] | None) -> tuple[Path, Path, Path]: - """ - Gets the MERSCOPE file paths when vpt_outputs is provided + """Gets the MERSCOPE file paths when vpt_outputs is provided. That is, (i) the file of transcript per cell, (ii) the cell metadata file, and (iii) the cell boundary file """ @@ -58,9 +59,9 @@ def _get_file_paths(path: Path, vpt_outputs: Path | str | dict[str, Any] | None) ] valid_boundaries = [path for path in plausible_boundaries if path.exists()] - assert ( - valid_boundaries - ), f"Boundary file not found - expected to find one of these files: {', '.join(map(str, plausible_boundaries))}" + assert valid_boundaries, ( + f"Boundary file not found - expected to find one of these files: {', '.join(map(str, plausible_boundaries))}" + ) return ( vpt_outputs / MerscopeKeys.COUNTS_FILE, @@ -95,8 +96,7 @@ def merscope( imread_kwargs: Mapping[str, Any] = MappingProxyType({}), image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *MERSCOPE* data from Vizgen. + """Read *MERSCOPE* data from Vizgen. This function reads the following files: @@ -160,9 +160,9 @@ def merscope( assert isinstance(image_models_kwargs, dict) image_models_kwargs["scale_factors"] = [2, 2, 2, 2] - assert ( - backend is None or backend in SUPPORTED_BACKENDS - ), f"Backend '{backend} not supported. Should be one of: {', '.join(SUPPORTED_BACKENDS)}" + assert backend is None or backend in SUPPORTED_BACKENDS, ( + f"Backend '{backend} not supported. Should be one of: {', '.join(SUPPORTED_BACKENDS)}" + ) path = Path(path).absolute() count_path, obs_path, boundaries_path = _get_file_paths(path, vpt_outputs) @@ -234,7 +234,7 @@ def _get_reader(backend: str | None) -> Callable: # type: ignore[type-arg] if backend is not None: return _rioxarray_load_merscope if backend == "rioxarray" else _dask_image_load_merscope try: - import rioxarray # noqa: F401 + import rioxarray return _rioxarray_load_merscope except ModuleNotFoundError: @@ -255,7 +255,7 @@ def _rioxarray_load_merscope( except ModuleNotFoundError: raise ModuleNotFoundError( "Using rioxarray backend requires to install the rioxarray library (`pip install rioxarray`)" - ) + ) from None from rasterio.errors import NotGeoreferencedWarning warnings.simplefilter("ignore", category=NotGeoreferencedWarning) diff --git a/src/spatialdata_io/readers/seqfish.py b/src/spatialdata_io/readers/seqfish.py index 27431c09..276f27b5 100644 --- a/src/spatialdata_io/readers/seqfish.py +++ b/src/spatialdata_io/readers/seqfish.py @@ -3,10 +3,9 @@ import os import re import xml.etree.ElementTree as ET -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad import numpy as np @@ -26,6 +25,9 @@ from spatialdata_io._constants._constants import SeqfishKeys as SK from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["seqfish"] @@ -41,8 +43,7 @@ def seqfish( imread_kwargs: Mapping[str, Any] = MappingProxyType({}), raster_models_scale_factors: list[float] | None = None, ) -> SpatialData: - """ - Read *seqfish* formatted dataset. + """Read *seqfish* formatted dataset. This function reads the following files: @@ -200,7 +201,6 @@ def get_transcript_file(roi: str) -> str: points = {} if load_points: for x in rois_str: - # prepare data name = f"{os.path.splitext(get_transcript_file(x))[0]}" p = pd.read_csv(path / get_transcript_file(x), delimiter=",") @@ -217,7 +217,7 @@ def get_transcript_file(roi: str) -> str: shapes = {} if cells_as_circles: - for x, adata in zip(rois_str, tables.values()): + for x, adata in zip(rois_str, tables.values(), strict=False): shapes[f"{os.path.splitext(get_cell_file(x))[0]}"] = ShapesModel.parse( adata.obsm[SK.SPATIAL_KEY], geometry=0, diff --git a/src/spatialdata_io/readers/steinbock.py b/src/spatialdata_io/readers/steinbock.py index b3ea707c..fc91ec82 100644 --- a/src/spatialdata_io/readers/steinbock.py +++ b/src/spatialdata_io/readers/steinbock.py @@ -1,15 +1,12 @@ from __future__ import annotations import os -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import anndata as ad from dask_image.imread import imread -from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage -from spatial_image import SpatialImage from spatialdata import SpatialData from spatialdata._logging import logger from spatialdata.models import Image2DModel, Labels2DModel, TableModel @@ -17,6 +14,12 @@ from spatialdata_io._constants._constants import SteinbockKeys +if TYPE_CHECKING: + from collections.abc import Mapping + + from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage + from spatial_image import SpatialImage + __all__ = ["steinbock"] @@ -26,8 +29,7 @@ def steinbock( imread_kwargs: Mapping[str, Any] = MappingProxyType({}), image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read a *Steinbock* output into a SpatialData object. + """Read a *Steinbock* output into a SpatialData object. .. seealso:: @@ -58,8 +60,7 @@ def steinbock( labels = {} if len(set(samples).difference(set(samples_labels))): logger.warning( - f"Samples {set(samples).difference(set(samples_labels))} have images but no labels. " - "They will be ignored." + f"Samples {set(samples).difference(set(samples_labels))} have images but no labels. They will be ignored." ) for sample in samples: images[f"{sample}_image"] = _get_images( diff --git a/src/spatialdata_io/readers/stereoseq.py b/src/spatialdata_io/readers/stereoseq.py index c2af7951..e4c83af4 100644 --- a/src/spatialdata_io/readers/stereoseq.py +++ b/src/spatialdata_io/readers/stereoseq.py @@ -2,10 +2,9 @@ import os import re -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import anndata as ad import h5py @@ -23,6 +22,9 @@ from spatialdata_io._docs import inject_docs from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["stereoseq"] @@ -35,8 +37,7 @@ def stereoseq( imread_kwargs: Mapping[str, Any] = MappingProxyType({}), image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *Stereo-seq* formatted dataset. + """Read *Stereo-seq* formatted dataset. Parameters ---------- @@ -311,7 +312,7 @@ def stereoseq( y_coords = df_coords.filter(regex="y_") polygons = [] for (x_index, x_row), (y_index, y_row) in tqdm( - zip(x_coords.iterrows(), y_coords.iterrows()), desc="creating polygons", total=len(df_coords) + zip(x_coords.iterrows(), y_coords.iterrows(), strict=False), desc="creating polygons", total=len(df_coords) ): assert x_index == y_index # the polygonal cells coordinates are stored as offsets from the centroids, so let's add the centroids diff --git a/src/spatialdata_io/readers/visium.py b/src/spatialdata_io/readers/visium.py index da7a8037..76a41898 100644 --- a/src/spatialdata_io/readers/visium.py +++ b/src/spatialdata_io/readers/visium.py @@ -3,10 +3,9 @@ import json import os import re -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd @@ -22,6 +21,9 @@ from spatialdata_io._docs import inject_docs from spatialdata_io.readers._utils._utils import _read_counts +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = ["visium"] @@ -38,8 +40,7 @@ def visium( image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), **kwargs: Any, ) -> SpatialData: - """ - Read *10x Genomics* Visium formatted dataset. + """Read *10x Genomics* Visium formatted dataset. This function reads the following files: diff --git a/src/spatialdata_io/readers/visium_hd.py b/src/spatialdata_io/readers/visium_hd.py index 723a2857..6a5f3bf6 100644 --- a/src/spatialdata_io/readers/visium_hd.py +++ b/src/spatialdata_io/readers/visium_hd.py @@ -3,10 +3,9 @@ import json import re import warnings -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import h5py import numpy as np @@ -15,17 +14,14 @@ from dask_image.imread import imread from geopandas import GeoDataFrame from imageio import imread as imread2 -from multiscale_spatial_image import MultiscaleSpatialImage from numpy.random import default_rng from skimage.transform import ProjectiveTransform, warp -from spatial_image import SpatialImage from spatialdata import ( SpatialData, get_extent, rasterize_bins, rasterize_bins_link_table_to_labels, ) -from spatialdata._types import ArrayLike from spatialdata.models import Image2DModel, ShapesModel, TableModel from spatialdata.transformations import Affine, Identity, Scale, set_transformation from xarray import DataArray @@ -33,6 +29,13 @@ from spatialdata_io._constants._constants import VisiumHDKeys from spatialdata_io._docs import inject_docs +if TYPE_CHECKING: + from collections.abc import Mapping + + from multiscale_spatial_image import MultiscaleSpatialImage + from spatial_image import SpatialImage + from spatialdata._types import ArrayLike + RNG = default_rng(0) @@ -51,8 +54,7 @@ def visium_hd( image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), anndata_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read *10x Genomics* Visium HD formatted dataset. + """Read *10x Genomics* Visium HD formatted dataset. .. seealso:: @@ -479,9 +481,9 @@ def _load_image( if data.shape[-1] == 3: # HE image in RGB format data = data.transpose(2, 0, 1) else: - assert data.shape[0] == min( - data.shape - ), "When the image is not in RGB, the first dimension should be the number of channels." + assert data.shape[0] == min(data.shape), ( + "When the image is not in RGB, the first dimension should be the number of channels." + ) image = DataArray(data, dims=("c", "y", "x")) parsed = Image2DModel.parse( @@ -510,8 +512,7 @@ def _projective_matrix_is_affine(projective_matrix: ArrayLike) -> bool: def _decompose_projective_matrix(projective_matrix: ArrayLike) -> tuple[ArrayLike, ArrayLike]: - """ - Decompose a projective transformation matrix into an affine transformation and a projective shift. + """Decompose a projective transformation matrix into an affine transformation and a projective shift. Parameters ---------- @@ -551,8 +552,7 @@ def _parse_metadata(path: Path, filename_prefix: str) -> tuple[dict[str, Any], d def _get_transform_matrices(metadata: dict[str, Any], hd_layout: dict[str, Any]) -> dict[str, ArrayLike]: - """ - Gets 4 projective transformation matrices, describing how to align the CytAssist, spots and microscope coordinates. + """Gets 4 projective transformation matrices, describing how to align the CytAssist, spots and microscope coordinates. Parameters ---------- diff --git a/src/spatialdata_io/readers/xenium.py b/src/spatialdata_io/readers/xenium.py index ff036067..5940ae3d 100644 --- a/src/spatialdata_io/readers/xenium.py +++ b/src/spatialdata_io/readers/xenium.py @@ -7,10 +7,9 @@ import tempfile import warnings import zipfile -from collections.abc import Mapping from pathlib import Path from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any import dask.array as da import numpy as np @@ -19,7 +18,6 @@ import pyarrow.parquet as pq import tifffile import zarr -from anndata import AnnData from dask.dataframe import read_parquet from dask_image.imread import imread from geopandas import GeoDataFrame @@ -28,7 +26,6 @@ from shapely import Polygon from spatialdata import SpatialData from spatialdata._core.query.relational_query import get_element_instances -from spatialdata._types import ArrayLike from spatialdata.models import ( Image2DModel, Labels2DModel, @@ -45,6 +42,12 @@ from spatialdata_io.readers._utils._read_10x_h5 import _read_10x_h5 from spatialdata_io.readers._utils._utils import _initialize_raster_models_kwargs +if TYPE_CHECKING: + from collections.abc import Mapping + + from anndata import AnnData + from spatialdata._types import ArrayLike + __all__ = ["xenium", "xenium_aligned_image", "xenium_explorer_selection"] @@ -68,8 +71,7 @@ def xenium( image_models_kwargs: Mapping[str, Any] = MappingProxyType({}), labels_models_kwargs: Mapping[str, Any] = MappingProxyType({}), ) -> SpatialData: - """ - Read a *10X Genomics Xenium* dataset into a SpatialData object. + """Read a *10X Genomics Xenium* dataset into a SpatialData object. This function reads the following files: @@ -324,9 +326,9 @@ def filter(self, record: logging.LogRecord) -> bool: logger = tifffile.logger() logger.addFilter(IgnoreSpecificMessage()) image_models_kwargs = dict(image_models_kwargs) - assert ( - "c_coords" not in image_models_kwargs - ), "The channel names for the morphology focus images are handled internally" + assert "c_coords" not in image_models_kwargs, ( + "The channel names for the morphology focus images are handled internally" + ) image_models_kwargs["c_coords"] = list(channel_names.values()) images["morphology_focus"] = _get_images( morphology_focus_dir, @@ -475,8 +477,7 @@ def _get_cells_metadata_table_from_zarr( file: str, specs: dict[str, Any], ) -> AnnData: - """ - Read cells metadata from ``{xx.CELLS_ZARR}``. + """Read cells metadata from ``{xx.CELLS_ZARR}``. Read the cells summary table, which contains the z_level information for versions < 2.0.0, and also the nucleus_count for versions >= 2.0.0. @@ -602,8 +603,7 @@ def xenium_aligned_image( rgba: bool = False, c_coords: list[str] | None = None, ) -> DataTree: - """ - Read an image aligned to a Xenium dataset, with an optional alignment file. + """Read an image aligned to a Xenium dataset, with an optional alignment file. Parameters ---------- @@ -731,7 +731,6 @@ def _parse_version_of_xenium_analyzer( specs: dict[str, Any], hide_warning: bool = True, ) -> packaging.version.Version | None: - # After using xeniumranger (e.g. 3.0.1.1) to resegment data from previous versions (e.g. xenium-1.6.0.7), a new dict is added to # `specs`, named 'xenium_ranger', which contains the key 'version' and whose value specifies the version of xeniumranger used to # resegment the data (e.g. 'xenium-3.0.1.1'). @@ -782,14 +781,16 @@ def cell_id_str_from_prefix_suffix_uint32(cell_id_prefix: ArrayLike, dataset_suf cell_id_prefix_hex_shifted = ["".join([hex_shift[c] for c in x]) for x in cell_id_prefix_hex] # merge the prefix and the suffix - cell_id_str = [str(x[0]).rjust(8, "a") + f"-{x[1]}" for x in zip(cell_id_prefix_hex_shifted, dataset_suffix)] + cell_id_str = [ + str(x[0]).rjust(8, "a") + f"-{x[1]}" for x in zip(cell_id_prefix_hex_shifted, dataset_suffix, strict=False) + ] return np.array(cell_id_str) def prefix_suffix_uint32_from_cell_id_str(cell_id_str: ArrayLike) -> tuple[ArrayLike, ArrayLike]: # parse the string into the prefix and suffix - cell_id_prefix_str, dataset_suffix = zip(*[x.split("-") for x in cell_id_str]) + cell_id_prefix_str, dataset_suffix = zip(*[x.split("-") for x in cell_id_str], strict=False) dataset_suffix_int = [int(x) for x in dataset_suffix] # reverse the shifted hex conversion diff --git a/tests/_utils.py b/tests/_utils.py index 9787d1bb..ee7b4c70 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -4,8 +4,7 @@ def skip_if_below_python_version() -> pytest.mark.skipif: - """ - Decorator to skip tests if the Python version is below a specified version. + """Decorator to skip tests if the Python version is below a specified version. This decorator prevents running tests on unsupported Python versions. Update the `MIN_VERSION` constant to change the minimum Python version required for the tests. diff --git a/tests/test_basic.py b/tests/test_basic.py index fc332082..71ac4d3a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,4 +2,4 @@ def test_package_has_version() -> None: - spatialdata_io.__version__ + assert spatialdata_io.__version__ diff --git a/tests/test_macsima.py b/tests/test_macsima.py index cf7ab2f7..63d6ca24 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -147,7 +147,7 @@ def test_cycle_metadata(dataset: str, expected: list[str]) -> None: def test_parsing_style() -> None: with pytest.raises(ValueError): - macsima(Path("."), parsing_style="not_a_parsing_style") + macsima(Path(), parsing_style="not_a_parsing_style") @pytest.mark.parametrize( @@ -168,7 +168,7 @@ def test_mci_sort_by_channel() -> None: cycles = [2, 0, 1] mci = MultiChannelImage( data=[RNG.random((size, size), chunks=(10, 10)) for size in sizes], - metadata=[ChannelMetadata(name=c_name, cycle=cycle) for c_name, cycle in zip(c_names, cycles)], + metadata=[ChannelMetadata(name=c_name, cycle=cycle) for c_name, cycle in zip(c_names, cycles, strict=False)], ) assert mci.get_channel_names() == c_names assert [x.shape[0] for x in mci.data] == sizes