Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add 'kind' field to dict output for various subclasses that are otherwise identical #185

Merged
merged 25 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e6b0146
test: updating serialization tests
tlambert03 Jul 5, 2023
7656aac
Merge branch 'v2' into serialization-tests
tlambert03 Jul 5, 2023
a265733
reorder
tlambert03 Jul 5, 2023
24a1f80
Merge branch 'v2' into serialization-tests
tlambert03 Jul 6, 2023
58d6cd8
Merge branch 'v2' into serialization-tests
tlambert03 Jul 6, 2023
1b41d8b
wip
tlambert03 Jul 6, 2023
c74ee77
Merge branch 'main' into serialization-tests
tlambert03 Jul 6, 2023
6037c43
Merge branch 'main' into serialization-tests
tlambert03 Jul 7, 2023
abc6d0f
Merge branch 'main' into serialization-tests
tlambert03 Jul 7, 2023
3763169
import uri
tlambert03 Jul 7, 2023
d9850b1
partial to dict support
tlambert03 Jul 7, 2023
7b53c78
add doc
tlambert03 Jul 7, 2023
3670613
Merge branch 'main' into serialization-tests
tlambert03 Jul 8, 2023
6b7eb3b
Merge branch 'main' into serialization-tests
tlambert03 Jul 8, 2023
c34f9a7
Merge branch 'main' into serialization-tests
tlambert03 Jul 9, 2023
e6d13f8
Merge branch 'main' into serialization-tests
tlambert03 Jul 10, 2023
49467bc
fixing any_elements_validator, and adding new method injection pattern
tlambert03 Jul 10, 2023
e789d08
test: finish dict roundtrip test
tlambert03 Jul 10, 2023
c5a8e14
add type ignore
tlambert03 Jul 10, 2023
8080861
fix XML annotation from string
tlambert03 Jul 10, 2023
18ddbce
more tests and coverage
tlambert03 Jul 10, 2023
99484d6
fix pre-commit
tlambert03 Jul 10, 2023
e131fc8
fix docs
tlambert03 Jul 10, 2023
4938475
try strategy for 3.7
tlambert03 Jul 10, 2023
142021a
add comments
tlambert03 Jul 10, 2023
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
21 changes: 20 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,33 @@ jobs:

steps:
- uses: actions/checkout@v3

# we can't actually do the codegen on python3.7
# (there's an issue with jinja template ordering)
# so we build wheel before installing python 3.7
- name: Build Wheel
if: matrix.python-version == '3.7'
run: |
pip install -U pip build
python -m build --wheel

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies

- name: Install (3.7)
if: matrix.python-version == '3.7'
run: |
whl=$(ls dist/*.whl)
python -m pip install "${whl}[test,dev]"

- name: Install
if: matrix.python-version != '3.7'
run: |
python -m pip install -U pip
python -m pip install .[test,dev]

- name: Test
run: pytest --cov --cov-report=xml

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ exclude_lines = [
source = ["ome_types", "ome_autogen"]
omit = ["src/ome_types/_autogenerated/*", "/private/var/folders/*"]


# Entry points -- REMOVE ONCE XSDATA-PYDANTIC-BASEMODEL IS SEPARATE
[project.entry-points."xsdata.plugins.class_types"]
xsdata_pydantic_basemodel = "xsdata_pydantic_basemodel.hooks.class_type"
Expand Down
5 changes: 2 additions & 3 deletions src/ome_autogen/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# pragma: no cover
from ome_autogen.main import build_model
from ome_autogen.main import build_model # pragma: no cover

build_model()
build_model() # pragma: no cover
6 changes: 4 additions & 2 deletions src/ome_autogen/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
from ome_autogen._generator import OmeGenerator
from ome_autogen._util import camel_to_snake

KindedTypes = "(Shape|ManufacturerSpec|Annotation)"


MIXIN_MODULE = "ome_types._mixins"
MIXINS: list[tuple[str, str, bool]] = [
(".*", f"{MIXIN_MODULE}._base_type.OMEType", False), # base type on every class
("OME", f"{MIXIN_MODULE}._ome.OMEMixin", True),
("Instrument", f"{MIXIN_MODULE}._instrument.InstrumentMixin", False),
("Reference", f"{MIXIN_MODULE}._reference.ReferenceMixin", True),
("BinData", f"{MIXIN_MODULE}._bin_data.BinDataMixin", True),
("Pixels", f"{MIXIN_MODULE}._pixels.PixelsMixin", True),
(KindedTypes, f"{MIXIN_MODULE}._kinded.KindMixin", True),
]

ALLOW_RESERVED_NAMES = {"type", "Type", "Union"}
Expand Down
54 changes: 50 additions & 4 deletions src/ome_autogen/_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, NamedTuple, cast
from typing import TYPE_CHECKING, Callable, Iterator, NamedTuple, cast

from xsdata.formats.dataclass.filters import Filters
from xsdata.formats.dataclass.generator import DataclassGenerator
Expand All @@ -22,6 +22,29 @@
# avoiding import to avoid build-time dependency on the ome-types package
AUTO_SEQUENCE = "__auto_sequence__"

# Methods and/or validators to add to generated classes
# (predicate, method) where predicate returns True if the method should be added
# to the given class. Note: imports for these methods are added in
# IMPORT_PATTERNS below.
ADDED_METHODS: list[tuple[Callable[[Class], bool], str]] = [
(
lambda c: c.name == "BinData",
"\n\n_v = root_validator(pre=True)(bin_data_root_validator)",
),
(
lambda c: c.name == "Value",
"\n\n_v = validator('any_elements', each_item=True)(any_elements_validator)",
),
(
lambda c: c.name == "Pixels",
"\n\n_v = root_validator(pre=True)(pixels_root_validator)",
),
(
lambda c: c.name == "XMLAnnotation",
"\n\n_v = validator('value', pre=True)(xml_value_validator)",
),
]


class Override(NamedTuple):
element_name: str # name of the attribute in the XSD
Expand Down Expand Up @@ -55,6 +78,18 @@ class Override(NamedTuple):
for o in CLASS_OVERRIDES
if o.module_name
}
IMPORT_PATTERNS.update(
{
"ome_types._mixins._util": {"new_uuid": ["default_factory=new_uuid"]},
"datetime": {"datetime": ["datetime"]},
"ome_types._mixins._validators": {
"any_elements_validator": ["any_elements_validator"],
"bin_data_root_validator": ["bin_data_root_validator"],
"pixels_root_validator": ["pixels_root_validator"],
"xml_value_validator": ["xml_value_validator"],
},
}
)


class OmeGenerator(DataclassGenerator):
Expand Down Expand Up @@ -99,7 +134,8 @@ def register(self, env: Environment) -> None:
# add our own templates dir to the search path
tpl_dir = Path(__file__).parent.joinpath("templates")
cast("FileSystemLoader", env.loader).searchpath.insert(0, str(tpl_dir))
return super().register(env)
super().register(env)
env.filters.update({"methods": self.methods})

def __init__(self, config: GeneratorConfig):
super().__init__(config)
Expand Down Expand Up @@ -190,9 +226,13 @@ def field_type(self, attr: Attr, parents: list[str]) -> str:
@classmethod
def build_import_patterns(cls) -> dict[str, dict]:
patterns = super().build_import_patterns()
patterns.setdefault("pydantic", {}).update(
{
"validator": ["validator("],
"root_validator": ["root_validator("],
}
)
patterns.update(IMPORT_PATTERNS)
patterns["ome_types._mixins._util"] = {"new_uuid": ["default_factory=new_uuid"]}
patterns["datetime"] = {"datetime": ["datetime"]}
return {key: patterns[key] for key in sorted(patterns)}

def field_default_value(self, attr: Attr, ns_map: dict | None = None) -> str:
Expand Down Expand Up @@ -221,3 +261,9 @@ def constant_name(self, name: str, class_name: str) -> str:
# use the enum names found in appinfo/xsdfu/enum
return self.appinfo.enums[class_name][name].enum
return super().constant_name(name, class_name)

def methods(self, obj: Class) -> list[str]:
for predicate, code in ADDED_METHODS:
if predicate(obj):
return [code]
return []
17 changes: 14 additions & 3 deletions src/ome_autogen/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from ome_autogen._config import get_config
from ome_autogen._transformer import OMETransformer

BLACK_LINE_LENGTH = 88
BLACK_TARGET_VERSION = "py37"
BLACK_SKIP_TRAILING_COMMA = False # use trailing commas as a reason to split lines?
OUTPUT_PACKAGE = "ome_types._autogenerated.ome_2016_06"
DO_MYPY = os.environ.get("OME_AUTOGEN_MYPY", "0") == "1" or "--mypy" in sys.argv
SRC_PATH = Path(__file__).parent.parent
Expand Down Expand Up @@ -57,13 +60,21 @@ def build_model(
def _fix_formatting(package_dir: str, ruff_ignore: list[str] = RUFF_IGNORE) -> None:
_print_gray("Running black and ruff ...")

black = ["black", package_dir, "-q", "--line-length=88"]
subprocess.check_call(black) # noqa S

ruff = ["ruff", "-q", "--fix", package_dir]
ruff.extend(f"--ignore={ignore}" for ignore in ruff_ignore)
subprocess.check_call(ruff) # noqa S

black = [
"black",
"-q",
f"--line-length={BLACK_LINE_LENGTH}",
f"--target-version={BLACK_TARGET_VERSION}",
]
if BLACK_SKIP_TRAILING_COMMA: # pragma: no cover
black.append("--skip-magic-trailing-comma")
black.extend([str(x) for x in Path(package_dir).rglob("*.py")])
subprocess.check_call(black) # noqa S


def _check_mypy(package_dir: str) -> None:
_print_gray("Running mypy ...")
Expand Down
59 changes: 59 additions & 0 deletions src/ome_autogen/templates/class.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{% set level = level|default(0) -%}
{% set help | format_docstring(level + 1) %}
{%- include "docstrings." + docstring_name + ".jinja2" -%}
{% endset -%}
{% set parent_namespace = obj.namespace if obj.namespace is not none else parent_namespace|default(None) -%}
{% set parents = parents|default([obj.name]) -%}
{% set class_name = obj.name|class_name -%}
{% set class_annotations = obj | class_annotations(class_name) -%}
{% set global_type = level == 0 and not obj.local_type -%}
{% set local_name = obj.meta_name or obj.name -%}
{% set local_name = None if class_name == local_name or not global_type else local_name -%}
{% set base_classes = obj | class_bases(class_name) | join(', ')-%}
{% set target_namespace = obj.target_namespace if global_type and module_namespace != obj.target_namespace else None %}

{{ class_annotations | join('\n') }}
class {{ class_name }}{{"({})".format(base_classes) if base_classes }}:
{%- if help %}
{{ help|indent(4, first=True) }}
{%- endif -%}
{%- if local_name or obj.is_nillable or obj.namespace is not none or target_namespace or obj.local_type %}
class Meta:
{%- if obj.local_type %}
global_type = False
{%- endif -%}
{%- if local_name %}
name = "{{ local_name }}"
{%- endif -%}
{%- if obj.is_nillable %}
nillable = True
{%- endif -%}
{%- if obj.namespace is not none %}
namespace = "{{ obj.namespace }}"
{%- endif %}
{%- if target_namespace and target_namespace != obj.namespace %}
target_namespace = "{{ target_namespace }}"
{%- endif %}
{% elif obj.attrs|length == 0 and not help %}
pass
{%- endif -%}
{%- for attr in obj.attrs %}
{%- set field_typing = attr|field_type(parents) %}
{%- set field_definition = attr|field_definition(obj.ns_map, parent_namespace, parents) %}
{{ attr.name|field_name(obj.name) }}: {{ field_typing }} = {{ field_definition }}
{%- endfor -%}
{%- for inner in obj.inner %}
{%- set tpl = "enum.jinja2" if inner.is_enumeration else "class.jinja2" -%}
{%- set inner_parents = parents + [inner.name] -%}
{%- filter indent(4) -%}
{%- with obj=inner, parents=inner_parents, level=(level + 1) -%}
{% include tpl %}
{%- endwith -%}
{%- endfilter -%}
{%- endfor -%}
{# This is the only reason we're overriding this file #}
{%- for method in obj|methods %}
{%- filter indent(4) -%}
{{ method }}
{%- endfilter -%}
{%- endfor -%}
5 changes: 4 additions & 1 deletion src/ome_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
except PackageNotFoundError: # pragma: no cover
__version__ = "unknown"

from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from ome_types.units import ureg # noqa: TCH004

from ome_types import model
from ome_types._conversion import from_tiff, from_xml, to_dict, to_xml
Expand Down
47 changes: 40 additions & 7 deletions src/ome_types/_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,15 @@ def from_xml(
validate: bool | None = None,
parser: Any = None,
parser_kwargs: ParserKwargs | None = None,
) -> OME:
) -> OME: # Not totally true, see note below
"""Generate an OME object from an XML document.

NOTE: Technically, this can return any ome-types instance, (not just OME) but it's
by far the most common type that will come out of this function, and the type
annotation will be more useful to most users. For those who pass in an xml document
that isn't just a root <OME> tag, they can cast the result to the correct type
themselves.

Parameters
----------
xml : Path | str | bytes
Expand All @@ -127,7 +133,7 @@ def from_xml(
Returns
-------
OME
The OME object parsed from the XML document.
The OME object parsed from the XML document. (See NOTE above.)
"""
if parser is not None: # pragma: no cover
warnings.warn(
Expand All @@ -144,10 +150,7 @@ def from_xml(
if isinstance(xml, Path):
xml = str(xml)

# this cast is a lie... but it's by far the most common type that will
# come out of this function, and will be more useful to most users.
# For those who pass in an xml document that isn't just a root <OME> tag,
# they can cast the result to the correct type themselves.
# this cast is a lie... see NOTE above.
OME_type = cast("type[OME]", _get_ome_type(xml))

parser_ = XmlParser(**(parser_kwargs or {}))
Expand All @@ -159,7 +162,7 @@ def from_xml(


def to_xml(
ome: OME,
ome: OMEType,
*,
# exclude_defaults takes precedence over exclude_unset
# if a value equals the default, it will be excluded
Expand All @@ -173,6 +176,36 @@ def to_xml(
canonicalize: bool = False,
validate: bool = False,
) -> str:
"""Generate an XML document from an OME object.

Parameters
----------
ome : OMEType
Instance of an ome-types model class.
exclude_defaults : bool, optional
Whether to exclude attributes that are set to their default value,
by default False.
exclude_unset : bool, optional
Whether to exclude attributes that are not explicitly set,
by default True.
indent : int, optional
Number of spaces to indent the XML document, by default 2.
include_namespace : bool | None, optional
Whether to include the OME namespace in the root element. If `None`, will
be set to the value of `canonicalize`, by default None.
include_schema_location : bool, optional
Whether to include the schema location in the root element, by default True.
canonicalize : bool, optional
Whether to canonicalize the XML output, by default False.
validate : bool, optional
Whether to validate the XML document against the OME schema, after rendering.
(In most cases, this will be redundant and unnecessary.)

Returns
-------
str
The XML document as a string.
"""
config = SerializerConfig(
pretty_print=(indent > 0) and not canonicalize, # canonicalize does it for us
pretty_print_indent=" " * indent,
Expand Down
Loading