2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
2 changes: 1 addition & 1 deletion docs/tutorials/notebooks
61 changes: 60 additions & 1 deletion src/spatialdata/_io/_utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,16 @@
import logging
import os.path
import re
import sys
import tempfile
import traceback
import warnings
from collections.abc import Generator, Mapping, Sequence
from contextlib import contextmanager
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Any
from typing import Any, Literal

import zarr
from anndata import AnnData
Expand DownExpand Up@@ -383,3 +386,59 @@ def save_transformations(sdata: SpatialData) -> None:
stacklevel=2,
)
sdata.write_transformations()


class BadFileHandleMethod(Enum):
ERROR = "error"
WARN = "warn"


@contextmanager
def handle_read_errors(
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN],
location: str,
exc_types: tuple[type[Exception], ...],
) -> Generator[None, None, None]:
"""
Handle read errors according to parameter `on_bad_files`.

Parameters
----------
on_bad_files
Specifies what to do upon encountering an exception.
Allowed values are :

- 'error', let the exception be raised.
- 'warn', convert the exception into a warning if it is one of the expected exception types.
location
String identifying the function call where the exception happened
exc_types
A tuple of expected exception classes that should be converted into warnings.

Raises
------
If `on_bad_files="error"`, all encountered exceptions are raised.
If `on_bad_files="warn"`, any encountered exceptions not matching the `exc_types` are raised.
"""
on_bad_files = BadFileHandleMethod(on_bad_files) # str to enum
if on_bad_files == BadFileHandleMethod.WARN:
try:
yield
except exc_types as e:
# Extract the original filename and line number from the exception and
# create a warning from it.
exc_traceback = sys.exc_info()[-1]
last_frame, lineno = list(traceback.walk_tb(exc_traceback))[-1]
filename = last_frame.f_code.co_filename
# Include the location (element path) in the warning message.
message = f"{location}: {e.__class__.__name__}: {e.args[0]}"
warnings.warn_explicit(
message=message,
category=UserWarning,
filename=filename,
lineno=lineno,
)
# continue
else: # on_bad_files == BadFileHandleMethod.ERROR
# Let it raise exceptions
yield
65 changes: 40 additions & 25 deletions src/spatialdata/_io/io_table.py
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Literal

import numpy as np
import zarr
from anndata import AnnData
from anndata import read_zarr as read_anndata_zarr
from anndata._io.specs import write_elem as write_adata
from ome_zarr.format import Format
from zarr.errors import ArrayNotFoundError

from spatialdata._io._utils import BadFileHandleMethod, handle_read_errors
from spatialdata._io.format import CurrentTablesFormat, TablesFormats, _parse_version
from spatialdata._logging import logger
from spatialdata.models import TableModel


def _read_table(
zarr_store_path: str, group: zarr.Group, subgroup: zarr.Group, tables: dict[str, AnnData]
zarr_store_path: str,
group: zarr.Group,
subgroup: zarr.Group,
tables: dict[str, AnnData],
on_bad_files: Literal[BadFileHandleMethod.ERROR, BadFileHandleMethod.WARN] = BadFileHandleMethod.ERROR,
) -> dict[str, AnnData]:
"""
Read in tables in the tables Zarr.group of a SpatialData Zarr store.
Expand All@@ -30,6 +38,8 @@ def _read_table(
The subgroup containing the tables.
tables
A dictionary of tables.
on_bad_files
Specifies what to do upon encountering a bad file, e.g. corrupted, invalid or missing files.

Returns
-------
Expand All@@ -40,33 +50,38 @@ def _read_table(
f_elem = subgroup[table_name]
f_elem_store = os.path.join(zarr_store_path, f_elem.path)

tables[table_name] = read_anndata_zarr(f_elem_store)
with handle_read_errors(
on_bad_files=on_bad_files,
location=f"{subgroup.path}/{table_name}",
exc_types=(JSONDecodeError, KeyError, ValueError, ArrayNotFoundError),
):
tables[table_name] = read_anndata_zarr(f_elem_store)

f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()
f = zarr.open(f_elem_store, mode="r")
version = _parse_version(f, expect_attrs_key=False)
assert version is not None
# since have just one table format, we currently read it but do not use it; if we ever change the format
# we can rename the two _ to format and implement the per-format read logic (as we do for shapes)
_ = TablesFormats[version]
f.store.close()

# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()
# # replace with format from above
# version = "0.1"
# format = TablesFormats[version]
if TableModel.ATTRS_KEY in tables[table_name].uns:
# fill out eventual missing attributes that has been omitted because their value was None
attrs = tables[table_name].uns[TableModel.ATTRS_KEY]
if "region" not in attrs:
attrs["region"] = None
if "region_key" not in attrs:
attrs["region_key"] = None
if "instance_key" not in attrs:
attrs["instance_key"] = None
# fix type for region
if "region" in attrs and isinstance(attrs["region"], np.ndarray):
attrs["region"] = attrs["region"].tolist()

count += 1
count += 1

logger.debug(f"Found {count} elements in {subgroup}")
return tables
Expand Down
Loading