Skip to content

Commit

Permalink
add serialization
Browse files Browse the repository at this point in the history
Rate limit · GitHub

Access has been restricted

You have triggered a rate limit.

Please wait a few minutes before you try again;
in some cases this may take up to an hour.

tlambert03 committed Jun 28, 2023
1 parent acbe2e5 commit f6c9752
Showing 24 changed files with 308 additions and 224 deletions.
18 changes: 0 additions & 18 deletions convert.py

This file was deleted.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -117,6 +117,7 @@ ignore = [
"C901", # Function is too complex
"RUF009", # Do not perform function calls in default arguments
]
exclude = ['src/_ome_autogen.py']

[tool.ruff.per-file-ignores]
"tests/*.py" = ["D", "S"]
3 changes: 0 additions & 3 deletions src/ome_autogen/_class_type.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,3 @@ def is_model(self, obj: Any) -> bool:
return True

return False



3 changes: 2 additions & 1 deletion src/ome_autogen/_config.py
Original file line number Diff line number Diff line change
@@ -68,7 +68,8 @@ def get_config(
# 'Experimenters', 'ExperimenterGroups', 'Instruments', 'Images',
# 'StructuredAnnotations', 'ROIs', 'BinaryOnly']
# Pixels ['BinDataBlocks', 'TiffDataBlocks', 'MetadataOnly']
# Instrument ['GenericExcitationSource', 'LightEmittingDiode', 'Filament', 'Arc', 'Laser']
# Instrument ['GenericExcitationSource', 'LightEmittingDiode', 'Filament', 'Arc',
# 'Laser']
# BinaryFile ['External', 'BinData']
# StructuredAnnotations ['XMLAnnotation', 'FileAnnotation', 'ListAnnotation',
# 'LongAnnotation', 'DoubleAnnotation', 'CommentAnnotation',
2 changes: 1 addition & 1 deletion src/ome_autogen/_util.py
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ def get_plural_names(schema: Path | str = SCHEMA_FILE) -> dict[str, str]:
camel_snake_registry: dict[str, str] = {}


def camel_to_snake(name: str, **kwargs) -> str:
def camel_to_snake(name: str) -> str:
name = name.lstrip("@") # remove leading @ from "@any_element"
result = CAMEL_SNAKE_OVERRIDES.get(name)
if not result:
4 changes: 1 addition & 3 deletions src/ome_types/_convenience.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Union, cast
from warnings import warn

from typing_extensions import Protocol

from .model import OME

if TYPE_CHECKING:
4 changes: 2 additions & 2 deletions src/ome_types/util.py
Original file line number Diff line number Diff line change
@@ -74,13 +74,13 @@ def collect_ids(value: Any) -> dict[LSID, OMEType]:
CAMEL_REGEX = re.compile(r"(?<!^)(?=[A-Z])")


@lru_cache()
@lru_cache
def camel_to_snake(name: str) -> str:
"""Return a snake_case version of a camelCase string."""
return model._camel_to_snake.get(name, CAMEL_REGEX.sub("_", name).lower())


@lru_cache()
@lru_cache
def norm_key(key: str) -> str:
"""Return a normalized key."""
return key.split("}")[-1]
4 changes: 2 additions & 2 deletions src/ome_types2/__init__.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
except PackageNotFoundError:
__version__ = "unknown"

from ome_types2._conversion import from_tiff, from_xml, to_dict
from ome_types2._conversion import from_tiff, from_xml, to_dict, to_xml
from ome_types2.model import OME

__all__ = ["__version__", "OME", "from_xml", "from_tiff", "to_dict"]
__all__ = ["__version__", "OME", "from_xml", "from_tiff", "to_dict", "to_xml"]
26 changes: 23 additions & 3 deletions src/ome_types2/_conversion.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@
from xml.etree import ElementTree as ET

from xsdata.formats.dataclass.parsers.config import ParserConfig
from xsdata_pydantic_basemodel.bindings import XmlParser
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata_pydantic_basemodel.bindings import XmlParser, XmlSerializer

if TYPE_CHECKING:
from typing import TypedDict
@@ -24,7 +25,8 @@ class ParserKwargs(TypedDict, total=False):
handler: type[XmlHandler]


OME_2016_06 = r"{http://www.openmicroscopy.org/Schemas/OME/2016-06}OME"
OME_2016_06_URI = "http://www.openmicroscopy.org/Schemas/OME/2016-06"
OME_2016_06_NS = f"{{{OME_2016_06_URI}}}OME"


def _get_ome(xml: str | bytes) -> type[OME]:
@@ -33,7 +35,7 @@ def _get_ome(xml: str | bytes) -> type[OME]:
else:
root = ET.fromstring(xml) # noqa: S314

if root.tag == OME_2016_06:
if root.tag == OME_2016_06_NS:
from ome_types2.model import OME

return OME
@@ -70,6 +72,24 @@ def from_xml(
return parser.parse(xml, OME_type)


def to_xml(
ome: OME,
ignore_defaults: bool = True,
indent: int = 2,
include_schema_location: bool = True,
) -> str:
config = SerializerConfig(
pretty_print=indent > 0,
pretty_print_indent=" " * indent,
ignore_default_attributes=ignore_defaults,
)
if include_schema_location:
config.schema_location = f"{OME_2016_06_URI} {OME_2016_06_URI}/ome.xsd"

serializer = XmlSerializer(config=config)
return serializer.render(ome, ns_map={None: OME_2016_06_URI})


def from_tiff(
path: Path | str,
*,
121 changes: 57 additions & 64 deletions src/ome_types2/_mixins/_base_type.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import contextlib
import weakref
from datetime import datetime
from enum import Enum
from textwrap import indent
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Sequence, Set
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Sequence, Set, Type, cast

from pydantic import BaseModel, validator

@@ -18,22 +17,14 @@ def __init__(self, name: str) -> None:
self.name = name

def __repr__(self) -> str:
return f"{__name__}.{self.name}.{id(self)}"
return f"{__name__}.{self.name}.{id(self):x}"


def quantity_property(field: str) -> property:
"""Create property that returns a ``pint.Quantity`` combining value and unit."""

def quantity(self: Any) -> Optional["pint.Quantity"]:
from ome_types._units import ureg

value = getattr(self, field)
if value is None:
return None
unit = getattr(self, f"{field}_unit").value.replace(" ", "_")
return ureg.Quantity(value, unit)

return property(quantity)
# Default value to support automatic numbering for id field values.
_AUTO_SEQUENCE = Sentinel("AUTO_SEQUENCE")
_COUNTERS: dict[Type["OMEType"], int] = {}
_UNIT_FIELD = "{}_unit"
_QUANTITY_FIELD = "{}_quantity"


class OMEType(BaseModel):
@@ -47,35 +38,31 @@ class OMEType(BaseModel):
support.
"""

# Default value to support automatic numbering for id field values.
_AUTO_SEQUENCE = Sentinel("AUTO_SEQUENCE")
# pydantic BaseModel configuration.
# see: https://pydantic-docs.helpmanual.io/usage/model_config/
class Config:
arbitrary_types_allowed = False
validate_assignment = True
underscore_attrs_are_private = True
use_enum_values = False
validate_all = True

# allow use with weakref
__slots__: ClassVar[Set[str]] = {"__weakref__"} # type: ignore

def __init__(__pydantic_self__, **data: Any) -> None:
if "id" in __pydantic_self__.__fields__:
data.setdefault("id", OMEType._AUTO_SEQUENCE)
data.setdefault("id", _AUTO_SEQUENCE)
super().__init__(**data)

def __init_subclass__(cls) -> None:
"""Add some properties to subclasses with units.
"""Add `*_quantity` property for fields that have both a value and a unit.
It adds ``*_quantity`` property for fields that have both a value and a
unit, where ``*_quantity`` is a pint ``Quantity``
where `*_quantity` is a pint `Quantity`.
"""
_clsdir = set(cls.__fields__)
for field in _clsdir:
if f"{field}_unit" in _clsdir:
setattr(cls, f"{field}_quantity", quantity_property(field))

# pydantic BaseModel configuration.
# see: https://pydantic-docs.helpmanual.io/usage/model_config/
class Config:
arbitrary_types_allowed = False
validate_assignment = True
underscore_attrs_are_private = True
use_enum_values = False
validate_all = True
for field in cls.__fields__:
if _UNIT_FIELD.format(field) in cls.__fields__:
setattr(cls, _QUANTITY_FIELD.format(field), _quantity_property(field))

def __repr__(self) -> str:
name = self.__class__.__qualname__
@@ -107,43 +94,49 @@ def __repr__(self) -> str:
return f"{name}({body})"

@validator("id", pre=True, always=True, check_fields=False)
def validate_id(cls, value: Any) -> str:
@classmethod
def validate_id(cls, value: Any) -> Any:
"""Pydantic validator for ID fields in OME models.
If no value is provided, this validator provides and integer ID, and stores the
maximum previously-seen value on the class.
"""
# get the required LSID field from the annotation
id_field = cls.__fields__.get("id")
if not id_field:
current_count = _COUNTERS.setdefault(cls, 0)
if isinstance(value, str):
# parse the id and update the counter
v_id = value.rsplit(":", 1)[-1]
with contextlib.suppress(ValueError):
_COUNTERS[cls] = max(current_count, int(v_id))
return value

# Store the highest seen value on the class._max_id attribute.
if not hasattr(cls, "_max_id"):
cls._max_id = 0 # type: ignore [misc]
cls.__annotations__["_max_id"] = ClassVar[int]
if value is OMEType._AUTO_SEQUENCE:
value = cls._max_id + 1
if isinstance(value, int):
v_id = value
id_string = id_field.type_.__name__[:-2]
value = f"{id_string}:{value}"
else:
value = str(value)
v_id = value.rsplit(":", 1)[-1]
with contextlib.suppress(ValueError):
v_id = int(v_id)
cls._max_id = max(cls._max_id, v_id)
return id_field.type_(value)
_COUNTERS[cls] = max(current_count, value)
return f"{cls.__name__}:{value}"

def __getstate__(self: Any) -> Dict[str, Any]:
"""Support pickle of our weakref references."""
state = super().__getstate__()
state["__private_attribute_values__"].pop("_ref", None)
return state
if value is _AUTO_SEQUENCE:
# just increment the counter
_COUNTERS[cls] += 1
return f"{cls.__name__}:{_COUNTERS[cls]}"

@classmethod
def snake_name(cls) -> str:
from .model import _camel_to_snake
raise ValueError(f"Invalid ID value: {value!r}")

# @classmethod
# def snake_name(cls) -> str:
# from .model import _camel_to_snake

return _camel_to_snake[cls.__name__]
# return _camel_to_snake[cls.__name__]


def _quantity_property(field_name: str) -> property:
"""Create property that returns a ``pint.Quantity`` combining value and unit."""
from ome_types2._units import ureg

def quantity(self: Any) -> Optional["pint.Quantity"]:
value = getattr(self, field_name)
if value is None:
return None

unit = cast("Enum", getattr(self, _UNIT_FIELD.format(field_name)))
return ureg.Quantity(value, unit.value.replace(" ", "_"))

return property(quantity)
8 changes: 7 additions & 1 deletion src/ome_types2/_mixins/_reference.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import weakref
from typing import Optional
from typing import Any, Dict, Optional

from ._base_type import OMEType

@@ -12,3 +12,9 @@ def ref(self) -> "OMEType | None":
if self._ref is None:
raise ValueError("references not yet resolved on root OME object")
return self._ref()

def __getstate__(self: Any) -> Dict[str, Any]:
"""Support pickle of our weakref references."""
state = super().__getstate__()
state["__private_attribute_values__"].pop("_ref", None)
return state
9 changes: 9 additions & 0 deletions src/ome_types2/_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pint

ureg: pint.UnitRegistry = pint.UnitRegistry(auto_reduce_dimensions=True)
ureg.define("reference_frame = [_reference_frame]")
ureg.define("@alias grade = gradian")
ureg.define("@alias astronomical_unit = ua")
ureg.define("line = inch / 12")
ureg.define("millitorr = torr / 1000 = mTorr")
ureg.define("@alias torr = Torr")
Empty file.
2 changes: 1 addition & 1 deletion tests/v1/test_model.py
Original file line number Diff line number Diff line change
@@ -6,10 +6,10 @@
from xml.etree import ElementTree

import pytest
import util
from pydantic import ValidationError
from xmlschema.validators.exceptions import XMLSchemaValidationError

import util
from ome_types import from_tiff, from_xml, model, to_xml
from ome_types._xmlschema import NS_OME, URI_OME, get_schema, to_xml_element

24 changes: 0 additions & 24 deletions tests/v2/test_from_xml.py

This file was deleted.

Rate limit · GitHub

Access has been restricted

You have triggered a rate limit.

Please wait a few minutes before you try again;
in some cases this may take up to an hour.

0 comments on commit f6c9752

Please sign in to comment.