Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ and this project adheres to [Semantic Versioning][].

### Fixed

## [0.3.0] - 2026-07-07

## [0.3.0] - 2026-07-08

### Added

### Changed

- Require `anndata>=0.13` (and consequently Python `>=3.12`, which anndata 0.13 requires) (#88)
- Require `zarr>=3` (anndata 0.13 no longer supports zarr <3). Removed the dead `zarr<3` (`ZARR_V2`) code branches and the dead `anndata<0.11` (`USE_EXPERIMENTAL`) code branches now that the supported floors exceed them (#88)
- Zarr writes now store `obst`/`vart` in a columnar layout (one array per node/edge attribute key) instead of a single JSON scalar. Dense numeric/boolean columns use native dtypes; ragged or complex columns fall back to per-element JSON. This fixes a `TypeError: string too large to store inside array` crash when writing large trees to zarr v3 (#86)
- `dtype` parameter in `TreeData.__init__` is now deprecated; passing it raises a `FutureWarning` and has no effect, matching anndata's removal of this argument
- `obsm`, `varm`, `layers`, `raw`, `shape`, `filename`, `filemode`, `asview`, and related parameters in `TreeData.__init__` are now keyword-only, matching the anndata 0.13.x API
Expand All @@ -30,6 +33,7 @@ and this project adheres to [Semantic Versioning][].

### Fixed

- `TreeData.__getitem__` now delegates `anndata.acc` accessor indexers (`AdRef`, `MapAcc`, `RefAcc`) to `AnnData.__getitem__` instead of routing them through `_normalize_indices`. This fixes an `IndexError: Unknown indexer` from `obs_vector`/`var_vector` (and any `var_names`/layer column lookup) under anndata ≥ 0.13 (#88)
- Fixed `UserWarning: Duplicate name: 'zarr.json'` spam when writing to a zarr `ZipStore` by staging the write in a `MemoryStore` and copying to the `ZipStore` once (#86)
- `read_zarr` now auto-detects `.zip` paths and opens them as a `ZipStore` (#86)
- Node/edge attributes explicitly set to `None` are now preserved through a zarr round-trip and kept distinct from absent attributes (#86)
Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@ maintainers = [
authors = [
{ name = "William Colgan" },
]
requires-python = ">=3.11"
requires-python = ">=3.12"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove Python 3.11 from the test matrix

With this floor bump, the hatch matrix still contains Python 3.11 at pyproject.toml:78, and .github/workflows/test.yaml reads that matrix and creates every listed environment. That schedules a hatch-test job under Python 3.11 for each PR, but an editable install of this project is invalid once requires-python is >=3.12, so the required test workflow will fail before running tests. Please update the matrix's lowest Python to 3.12 or remove the 3.11 entry along with this change.

Useful? React with 👍 / 👎.

classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"anndata>=0.11",
"anndata>=0.13",
"h5py",
"networkx",
"numpy",
"packaging",
"pandas",
"pyarrow",
"session-info2",
"zarr>=2",
"zarr>=3",
]
urls.Documentation = "https://treedata.readthedocs.io/"
urls.Homepage = "https://github.com/colganwi/treedata"
Expand Down Expand Up @@ -76,7 +75,7 @@ envs.docs.scripts.open = "python -m webbrowser -t docs/_build/html/index.html"
envs.docs.dependency-groups = [ "doc" ]
envs.hatch-test.matrix = [
# Test the lowest and highest supported Python versions with normal deps
{ deps = [ "stable" ], python = [ "3.11", "3.14" ] },
{ deps = [ "stable" ], python = [ "3.12", "3.14" ] },
# Test the newest supported Python version also with pre-release deps
{ deps = [ "pre" ], python = [ "3.14" ] },
]
Expand Down
10 changes: 1 addition & 9 deletions src/treedata/_core/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,23 @@
import json
import warnings
from collections.abc import MutableMapping
from importlib.metadata import version as get_version
from os import PathLike
from typing import Literal

import anndata as ad
import h5py
import networkx as nx
import zarr
from packaging import version

from treedata._core.treedata import TreeData

ANNDATA_VERSION = version.parse(get_version("anndata"))
USE_EXPERIMENTAL = ANNDATA_VERSION < version.parse("0.11.0")

# Sentinel distinguishing an absent attribute from an attribute explicitly set to ``None``.
_MISSING = object()


def _read_elem(elem):
"""Read an element from a store."""
if USE_EXPERIMENTAL:
return ad.experimental.read_elem(elem)
else:
return ad.io.read_elem(elem)
return ad.io.read_elem(elem)


def _dict_to_digraph(graph_dict: dict) -> nx.DiGraph:
Expand Down
5 changes: 5 additions & 0 deletions src/treedata/_core/treedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ def __repr__(self) -> str:

def __getitem__(self, index: Any) -> TreeData:
"""Returns a sliced view of the object."""
from anndata.acc import AdRef, MapAcc, RefAcc

if isinstance(index, AdRef | MapAcc | RefAcc):
# Accessor indexers retrieve an array/vector, not a TreeData view.
return super().__getitem__(index)
oidx, vidx = self._normalize_indices(index)
return TreeData(self, oidx=oidx, vidx=vidx, asview=True)

Expand Down
18 changes: 2 additions & 16 deletions src/treedata/_core/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json
import warnings
from collections.abc import MutableMapping
from importlib.metadata import version as get_version
from os import PathLike
from pathlib import Path
from typing import Any, Literal
Expand All @@ -14,15 +13,10 @@
import numpy as np
import pandas as pd
import zarr
from packaging import version

from treedata._core.aligned_mapping import AxisTrees
from treedata._core.treedata import TreeData

ANNDATA_VERSION = version.parse(get_version("anndata"))
USE_EXPERIMENTAL = ANNDATA_VERSION < version.parse("0.11.0")
ZARR_V2 = version.parse(get_version("zarr")) < version.parse("3.0.0")

# Sentinel distinguishing an absent attribute from an attribute explicitly set to ``None``.
_MISSING = object()

Expand All @@ -45,10 +39,7 @@ def _make_serializable(data: Any) -> Any:

def _write_elem(f, k, elem, *, dataset_kwargs) -> None:
"""Write an element to a storage group using anndata encoding."""
if USE_EXPERIMENTAL:
ad.experimental.write_elem(f, k, elem, dataset_kwargs=dataset_kwargs)
else:
ad.io.write_elem(f, k, elem, dataset_kwargs=dataset_kwargs)
ad.io.write_elem(f, k, elem, dataset_kwargs=dataset_kwargs)


def _digraph_to_dict(G: nx.DiGraph) -> dict:
Expand Down Expand Up @@ -229,10 +220,7 @@ def _open_zarr_group(
"""Open a Zarr group for writing and indicate ownership."""
if isinstance(store, zarr.Group):
return store, False
group_kwargs: dict[str, int] = {}
if not ZARR_V2:
group_kwargs["zarr_format"] = 3
return zarr.open_group(store, mode=mode, **group_kwargs), True
return zarr.open_group(store, mode=mode, zarr_format=3), True


def _close_zarr_group(group: zarr.Group) -> None:
Expand All @@ -246,8 +234,6 @@ def _close_zarr_group(group: zarr.Group) -> None:

def _is_zip_store(store: Any) -> bool:
"""Return True if store is or resolves to a zarr ZipStore."""
if ZARR_V2:
return False
return isinstance(store, zarr.storage.ZipStore) or (
isinstance(store, (str, PathLike)) and str(store).endswith(".zip")
)
Expand Down
Loading