Skip to content

Commit

Permalink
better roi unions
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Jul 1, 2023
1 parent 8eba2c7 commit f2f53db
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 11 deletions.
105 changes: 105 additions & 0 deletions .github/workflows/test_dependents.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: test dependents

on:
push:
branches:
- "main"
pull_request:
branches:
- "main"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test-aicsimageio:
name: test aicsimageio
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
repository: AllenCellModeling/aicsimageio
submodules: true
- uses: actions/checkout@v3
with:
path: ome-types
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"

- name: Install aicsimageio
run: |
python -m pip install --upgrade pip
python -m pip install .[test]
python -m pip install bioformats_jar
- uses: actions/cache@v3
id: cache
with:
path: aicsimageio/tests/resources
key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }}

- name: Download Test Resources
if: steps.cache.outputs.cache-hit != 'true'
run: python scripts/download_test_resources.py --debug

- name: Install ome-types
run: pip install .
working-directory: ome-types

- name: Run Tests
run: |
pytest --color=yes -k "not REMOTE" --ignore test_known_errors_without_cleaning \
aicsimageio/tests/readers/test_ome_tiff_reader.py \
aicsimageio/tests/readers/extra_readers/test_bioformats_reader.py \
aicsimageio/tests/readers/extra_readers/test_ome_zarr_reader.py
# test-paquo:
# name: test paquo
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# with:
# repository: bayer-science-for-a-better-life/paquo
# fetch-depth: 0
# - uses: actions/checkout@v3
# with:
# path: ome-types
# fetch-depth: 0

# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: "3.11"

# - name: Install aicsimageio
# run: |
# python -m pip install --upgrade pip
# python -m pip install -e .[dev]

# - name: Install ome-types
# run: pip install .
# working-directory: ome-types

# - name: Restore qupath cache
# uses: actions/cache@v3
# env:
# CACHE_NUMBER: 0
# with:
# path: ./qupath/download
# key: ${{ runner.os }}-qupath-v${{ env.CACHE_NUMBER }}

# - name: Install qupath and set PAQUO_QUPATH_DIR
# shell: bash
# run: |
# python -c "import os; os.makedirs('qupath/download', exist_ok=True)"
# python -c "import os; os.makedirs('qupath/apps', exist_ok=True)"
# python -m paquo get_qupath --install-path ./qupath/apps --download-path ./qupath/download ${{ env.QUPATH_VERSION }} | grep -v "^#" | sed "s/^/PAQUO_QUPATH_DIR=/" >> $GITHUB_ENV

# - name: Test with pytest
# run: |
# pytest paquo/tests
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,4 @@ src/ome_types/_version.py
docs/source/_autosummary
.benchmarks/
_build/
qupath/
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ docs = [
"sphinx-rtd-theme==1.1.1",
"ipython",
]
test = ["pytest", "pytest-cov", "pytest-benchmark", "xmlschema"]
test = ["pytest", "pytest-cov", "xmlschema"]

# https://hatch.pypa.io/latest/plugins/build-hook/custom/
[tool.hatch.build.targets.wheel.hooks.custom]
Expand Down Expand Up @@ -148,7 +148,6 @@ target-version = ['py38']
# https://docs.pytest.org/en/6.2.x/customize.html
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--benchmark-disable"
testpaths = ["tests"]
filterwarnings = [
"error",
Expand Down
20 changes: 18 additions & 2 deletions src/ome_autogen/_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,36 @@ def field_type(self, attr: Attr, parents: list[str]) -> str:
attr.name = self.appinfo.plurals.get(attr.name, f"{attr.name}s")
if self._is_color_attr(attr):
return "Color"
elif self._is_union_attr(attr):
return "ShapeUnion"

return super().field_type(attr, parents)

@classmethod
def build_import_patterns(cls) -> dict[str, dict]:
patterns = super().build_import_patterns()
patterns.update({"ome_types.model._color": {"Color": [": Color ="]}})
patterns.update(
{
"ome_types.model._color": {"Color": [": Color ="]},
"ome_types.model._roi_union": {"ShapeUnion": [": ShapeUnion ="]},
}
)
return {key: patterns[key] for key in sorted(patterns)}

def field_default_value(self, attr: Attr, ns_map: dict | None = None) -> str:
if attr.tag == "Attribute" and attr.name == "ID":
return repr(AUTO_SEQUENCE)
if self._is_color_attr(attr):
return "Color"
if self._is_union_attr(attr):
return "ShapeUnion"
return super().field_default_value(attr, ns_map)

def format_arguments(self, kwargs: dict, indent: int = 0) -> str:
# keep default_factory at the front
if kwargs.get("default") == "Color":
# keep default_factory at the front
kwargs = {"default_factory": kwargs.pop("default"), **kwargs}
if kwargs.get("default") == "ShapeUnion":
kwargs = {"default_factory": kwargs.pop("default"), **kwargs}
return super().format_arguments(kwargs, indent)

Expand All @@ -81,3 +93,7 @@ def constant_name(self, name: str, class_name: str) -> str:
def _is_color_attr(self, attr: Attr) -> bool:
# special logic to find Color types, for which we use our own type.
return attr.name == "Color" and attr.types[0].datatype == DataType.INT

def _is_union_attr(self, attr: Attr) -> bool:
# special logic to find Color types, for which we use our own type.
return attr.name == "Union" and attr.types[0].substituted
130 changes: 130 additions & 0 deletions src/ome_types/model/_roi_union.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from contextlib import suppress
from typing import Iterable, List, MutableSequence, Type, Union, overload

from pydantic import Field, ValidationError, validator

from ome_types._mixins._base_type import OMEType
from ome_types.model.ome_2016_06.ellipse import Ellipse
from ome_types.model.ome_2016_06.label import Label
from ome_types.model.ome_2016_06.line import Line
from ome_types.model.ome_2016_06.mask import Mask
from ome_types.model.ome_2016_06.point import Point
from ome_types.model.ome_2016_06.polygon import Polygon
from ome_types.model.ome_2016_06.polyline import Polyline
from ome_types.model.ome_2016_06.rectangle import Rectangle

_ShapeCls = (Rectangle, Mask, Point, Ellipse, Line, Polyline, Polygon, Label)
ShapeType = Union[Rectangle, Mask, Point, Ellipse, Line, Polyline, Polygon, Label]
_KINDS: dict[str, Type[ShapeType]] = {
"rectangle": Rectangle,
"mask": Mask,
"point": Point,
"ellipse": Ellipse,
"line": Line,
"polyline": Polyline,
"polygon": Polygon,
"label": Label,
}


class ShapeUnion(OMEType, MutableSequence[ShapeType]): # type: ignore[misc]
# NOTE: in reality, this is List[ShapeGroupType]... but
# for some reason that messes up xsdata data binding
__root__: List[object] = Field(
default_factory=list,
metadata={
"type": "Elements",
"choices": (
{
"name": "Label",
"type": Label,
},
{
"name": "Polygon",
"type": Polygon,
},
{
"name": "Polyline",
"type": Polyline,
},
{
"name": "Line",
"type": Line,
},
{
"name": "Ellipse",
"type": Ellipse,
},
{
"name": "Point",
"type": Point,
},
{
"name": "Mask",
"type": Mask,
},
{
"name": "Rectangle",
"type": Rectangle,
},
),
},
)

@validator("__root__", each_item=True)
def _validate_shapes(cls, v: ShapeType) -> ShapeType:
if isinstance(v, _ShapeCls):
return v
if isinstance(v, dict):
# NOTE: this is here to preserve the v1 behavior of passing a dict like
# {"kind": "label", "x": 0, "y": 0}
# to create a label rather than a point
if "kind" in v:
kind = v.pop("kind").lower()
return _KINDS[kind](**v)

for cls_ in _ShapeCls:
with suppress(ValidationError):
return cls_(**v)
raise ValueError(f"Invalid shape: {v}")

def __repr__(self) -> str:
return repr(self.__root__)

def __delitem__(self, _idx: int | slice) -> None:
del self.__root__[_idx]

@overload
def __getitem__(self, _idx: int) -> ShapeType:
...

@overload
def __getitem__(self, _idx: slice) -> List[ShapeType]:
...

def __getitem__(self, _idx: int | slice) -> ShapeType | List[ShapeType]:
return self.__root__[_idx] # type: ignore[return-value]

def __len__(self) -> int:
return super().__len__()

@overload
def __setitem__(self, _idx: int, _val: ShapeType) -> None:
...

@overload
def __setitem__(self, _idx: slice, _val: Iterable[ShapeType]) -> None:
...

def __setitem__(
self, _idx: int | slice, _val: ShapeType | Iterable[ShapeType]
) -> None:
self.__root__[_idx] = _val

def insert(self, index: int, value: ShapeType) -> None:
self.__root__.insert(index, value)

# for some reason, without overloading this... append() adds things to the
# beginning of the list instead of the end
def append(self, value: ShapeType) -> None:
self.__root__.append(value)
1 change: 0 additions & 1 deletion src/ome_types/model/ome.py

This file was deleted.

Loading

0 comments on commit f2f53db

Please sign in to comment.