diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1f7c94..d9286a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,23 +20,15 @@ jobs: steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v3 - name: Install Python dependencies run: | python -m pip install --upgrade pip python -m pip install hatch - hatch env create - echo "ASDF_PYDANTIC_VERSION=$(hatch version)" >> $GITHUB_ENV - name: Build run: hatch build - - name: Create GitHub Release (prerelease only) - if: github.event.push - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag: ${{ env.ASDF_PYDANTIC_VERSION }} - files: dist/* - generate_release_notes: true - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index cf46964..6a73c32 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ venv.bak/ # Rope project settings .ropeproject +# ruff +.ruff_cache/ + # mkdocs documentation /site diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dffcef..7212142 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +default_install_hook_types: [pre-commit, pre-push, commit-msg] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -9,6 +10,11 @@ repos: - id: check-yaml - id: check-added-large-files + - repo: https://github.com/rhysd/actionlint + rev: v1.7.3 + hooks: + - id: actionlint + - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: 'v0.6.3' @@ -21,6 +27,12 @@ repos: hooks: - id: black + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v3.4.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + ci: # autofix_commit_msg: | # [pre-commit.ci] auto fixes from pre-commit.com hooks diff --git a/asdf_pydantic/__version__.py b/asdf_pydantic/__version__.py deleted file mode 100644 index 6849410..0000000 --- a/asdf_pydantic/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.1.0" diff --git a/asdf_pydantic/converter.py b/asdf_pydantic/converter.py index 395bad5..7100044 100644 --- a/asdf_pydantic/converter.py +++ b/asdf_pydantic/converter.py @@ -30,7 +30,7 @@ def add_models( cls, *model_classes: Type[AsdfPydanticModel] ) -> "AsdfPydanticConverter": for model_class in model_classes: - cls._tag_to_class[model_class._tag] = model_class + cls._tag_to_class[model_class.get_tag_uri()] = model_class return cls() @property diff --git a/asdf_pydantic/examples/extensions.py b/asdf_pydantic/examples/extensions.py index aa2a8db..b79d4d9 100644 --- a/asdf_pydantic/examples/extensions.py +++ b/asdf_pydantic/examples/extensions.py @@ -4,9 +4,9 @@ from asdf_pydantic.converter import AsdfPydanticConverter from asdf_pydantic.examples.shapes import AsdfRectangle -from asdf_pydantic.examples.tree import AsdfNode +from asdf_pydantic.examples.tree import AsdfTreeNode -AsdfPydanticConverter.add_models(AsdfRectangle, AsdfNode) +AsdfPydanticConverter.add_models(AsdfRectangle, AsdfTreeNode) class ExampleExtension(Extension): diff --git a/asdf_pydantic/examples/tree.py b/asdf_pydantic/examples/tree.py index 8518bb1..d695f86 100644 --- a/asdf_pydantic/examples/tree.py +++ b/asdf_pydantic/examples/tree.py @@ -11,7 +11,7 @@ class Node(BaseModel): child: Optional[Node] = None -class AsdfNode(Node, AsdfPydanticModel): - _tag = "asdf://asdf-pydantic/examples/tags/node-1.0.0" +class AsdfTreeNode(Node, AsdfPydanticModel): + _tag = "asdf://asdf-pydantic/examples/tags/tree-node-1.0.0" - child: Optional[Union[Node, AsdfNode]] = None + child: Optional[Union[AsdfTreeNode, Node]] = None diff --git a/asdf_pydantic/model.py b/asdf_pydantic/model.py index 638ed77..b7c3f42 100644 --- a/asdf_pydantic/model.py +++ b/asdf_pydantic/model.py @@ -1,8 +1,11 @@ -import textwrap from typing import ClassVar import yaml -from pydantic import BaseModel +from asdf.extension import TagDefinition +from pydantic import BaseModel, ConfigDict +from typing_extensions import deprecated + +from asdf_pydantic.schema import DEFAULT_ASDF_SCHEMA_REF_TEMPLATE, GenerateAsdfSchema class AsdfPydanticModel(BaseModel): @@ -14,10 +17,8 @@ class AsdfPydanticModel(BaseModel): AsdfPydanticModel object with py:meth`AsdfPydanticModel.parse_obj()`. """ - _tag: ClassVar[str] - - class Config: - arbitrary_types_allowed = True + _tag: ClassVar[str | TagDefinition] + model_config = ConfigDict(arbitrary_types_allowed=True) def asdf_yaml_tree(self) -> dict: d = {} @@ -42,28 +43,57 @@ def asdf_yaml_tree(self) -> dict: return d @classmethod + def get_tag_definition(cls): + if isinstance(cls._tag, str): + return TagDefinition( # TODO: Add title and description + cls._tag, + schema_uris=[f"{cls._tag}/schema"], + ) + return cls._tag + + @classmethod + def get_tag_uri(cls): + if isinstance(cls._tag, TagDefinition): + return cls._tag.tag_uri + else: + return cls._tag + + @classmethod + def model_asdf_schema( + cls, + by_alias: bool = True, + ref_template: str = DEFAULT_ASDF_SCHEMA_REF_TEMPLATE, + schema_generator: type[GenerateAsdfSchema] = GenerateAsdfSchema, + ): + """Get the ASDF schema definition for this model.""" + # Implementation follows closely with the `BaseModel.model_json_schema` + schema_generator_instance = schema_generator( + by_alias=by_alias, ref_template=ref_template, tag_uri=cls.get_tag_uri() + ) + json_schema = schema_generator_instance.generate(cls.__pydantic_core_schema__) + + return f"%YAML 1.1\n---\n{yaml.safe_dump(json_schema, sort_keys=False)}" + + @classmethod + @deprecated( + "The `schema_asdf` method is deprecated; use `model_asdf_schema` instead." + ) def schema_asdf( - cls, *, metaschema: str = "http://stsci.edu/schemas/asdf/asdf-schema-1.0.0" + cls, + *, + metaschema: str = GenerateAsdfSchema.schema_dialect, + **kwargs, ) -> str: """Get the ASDF schema definition for this model. Parameters ---------- metaschema, optional - A metaschema URI, by default "http://stsci.edu/schemas/asdf/asdf-schema-1.0.0". - See https://asdf.readthedocs.io/en/stable/asdf/extending/schemas.html#anatomy-of-a-schema - for more options. + A metaschema URI """ # noqa: E501 - # TODO: Function signature should follow BaseModel.schema() or - # BaseModel.schema_json() - header = textwrap.dedent( - f""" - %YAML 1.1 - --- - $schema: {metaschema} - id: {cls._tag} - - """ - ) - body = yaml.dump(cls.schema()) - return header + body + if metaschema != GenerateAsdfSchema.schema_dialect: + raise NotImplementedError( + f"Only {GenerateAsdfSchema.schema_dialect} is supported as metaschema." + ) + + return cls.model_asdf_schema(**kwargs) diff --git a/asdf_pydantic/schema.py b/asdf_pydantic/schema.py new file mode 100644 index 0000000..38a2821 --- /dev/null +++ b/asdf_pydantic/schema.py @@ -0,0 +1,62 @@ +from typing import Optional + +from pydantic.json_schema import GenerateJsonSchema + +DEFAULT_ASDF_SCHEMA_REF_TEMPLATE = "#/definitions/{model}" +DESIRED_ASDF_SCHEMA_KEY_ORDER = ( + "$schema", + "id", + "title", + "type", + "properties", + "allOf", + "anyOf", + "required", + "definitions", +) + + +class GenerateAsdfSchema(GenerateJsonSchema): + """Generates ASDF-compatible schema from Pydantic's default JSON schema generator. + + ```{caution} Experimental + This schema generator is not complete. It currently creates JSON 2020-12 + schema (despite `$schema` says it's `asdf-schema-1.0.0`) which are not + compatible with ASDF. + ``` + """ + + # HACK: When we can support tree models, then not all schema should have tag + schema_dialect = "http://stsci.edu/schemas/asdf/asdf-schema-1.0.0" + + def __init__( + self, + by_alias: bool = True, + ref_template: str = DEFAULT_ASDF_SCHEMA_REF_TEMPLATE, + tag_uri: Optional[str] = None, + ): + super().__init__(by_alias=by_alias, ref_template=ref_template) + self.tag_uri = tag_uri + + def generate(self, schema, mode="validation"): + json_schema = super().generate(schema, mode) # noqa: F841 + + if self.tag_uri: + json_schema["$schema"] = self.schema_dialect + json_schema["id"] = f"{self.tag_uri}/schema" + + # TODO: Convert jsonschema 2020-12 to ASDF schema + if "$defs" in json_schema: + json_schema["definitions"] = json_schema.pop("$defs") + + # Order keys + json_schema = { + **{ + key: json_schema[key] + for key in DESIRED_ASDF_SCHEMA_KEY_ORDER + if key in json_schema + }, + **json_schema, # Rest of the keys not in order list + } + + return json_schema diff --git a/pyproject.toml b/pyproject.toml index 599115d..a5393e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "asdf-pydantic" description = 'Create ASDF tags with pydantic models' readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license-files = {paths = ["LICENSE"]} keywords = [] authors = [ @@ -15,15 +15,16 @@ authors = [ classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ - "asdf>=2", - "pydantic>=1,<2", + "asdf>=3", + "pydantic>=2", + "numpy>=1.25", ] dynamic = ["version"] @@ -36,10 +37,11 @@ Source = "https://github.com/ketozhang/asdf-pydantic" 'asdf.extensions' = { asdf_pydantic_extension = 'asdf_pydantic.examples.extensions:get_extensions' } [tool.hatch.version] -path = "asdf_pydantic/__version__.py" +source = "vcs" # Default Environment [tool.hatch.envs.default] +installer = "uv" dependencies = [ "ipython", "pytest", @@ -53,6 +55,30 @@ dependencies = [ test = "pytest {args}" test-cov = "test --cov-report=term-missing --cov-config=pyproject.toml --cov=asdf_pydantic --cov=tests {args}" +[tool.hatch.envs.test] +template = "default" +matrix-name-format = "{variable}={value}" + +[tool.hatch.envs.test.scripts] +test = "pytest {args}" + +[[tool.hatch.envs.test.matrix]] +# Only test with numpy v1 on Python 3.9 +python = ["3.9"] +numpy-version = ["1"] + +[[tool.hatch.envs.test.matrix]] +python = ["3.10", "3.11", "3.12"] +numpy-version = ["1", "2"] + + +[tool.hatch.envs.test.overrides] +matrix.numpy-version.dependencies = [ + { value = "numpy>=1,<2", if = ["1"] }, + { value = "numpy>=2,<3", if = ["2"] }, + { value = "astropy>=6.1", if = ["2"] }, +] + [tool.hatch.envs.docs] dependencies = [ "sphinx", diff --git a/tests/convert_to_asdf_yaml_tree_test.py b/tests/convert_to_asdf_yaml_tree_test.py index 073bd7e..37825e1 100644 --- a/tests/convert_to_asdf_yaml_tree_test.py +++ b/tests/convert_to_asdf_yaml_tree_test.py @@ -1,25 +1,25 @@ from __future__ import annotations -from asdf_pydantic.examples.tree import AsdfNode, Node +from asdf_pydantic.examples.tree import AsdfTreeNode, Node def test_sanity(): - AsdfNode().asdf_yaml_tree() == {"child": None} + AsdfTreeNode().asdf_yaml_tree() == {"child": None} def test_should_not_convert_given_child_is_AsdfNode(): - AsdfNode(child=AsdfNode()).asdf_yaml_tree() == {"child": AsdfNode()} + AsdfTreeNode(child=AsdfTreeNode()).asdf_yaml_tree() == {"child": AsdfTreeNode()} def test_should_convert_given_child_is_Node(): - AsdfNode(child=Node()).asdf_yaml_tree() == {"child": {"child": None}} + AsdfTreeNode(child=Node()).asdf_yaml_tree() == {"child": {"child": None}} def test_given_mix_child_is_mix_of_AsdfNode_and_Node(): - assert AsdfNode(child=AsdfNode(child=Node())).asdf_yaml_tree() == { - "child": AsdfNode(child=Node()) + assert AsdfTreeNode(child=AsdfTreeNode(child=Node())).asdf_yaml_tree() == { + "child": AsdfTreeNode(child=Node()) } - assert AsdfNode(child=Node(child=AsdfNode())).asdf_yaml_tree() == { - "child": {"child": AsdfNode()} + assert AsdfTreeNode(child=Node(child=AsdfTreeNode())).asdf_yaml_tree() == { + "child": {"child": {"child": None}} } diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/examples/test_node.py b/tests/examples/test_node.py new file mode 100644 index 0000000..09cdb6d --- /dev/null +++ b/tests/examples/test_node.py @@ -0,0 +1,127 @@ +"""Tests example asdf-pydantic model for nodes of a graph/tree.""" + +from __future__ import annotations + +import textwrap +from unittest.mock import MagicMock, patch + +import asdf +import asdf.exceptions +import asdf.schema +import pytest +import yaml +from asdf.extension import Extension + +from asdf_pydantic import AsdfPydanticConverter, AsdfPydanticModel + + +class AsdfNode(AsdfPydanticModel): + """Model for a node in a graph/tree. + + Nodes introduce self-referential types. Notice the type of the `child` + attribute it the node type itself. The ASDF schema for this model will require + self-referencing syntax. We assumes this form is valid for ASDF schemas: + + ```yaml + --- + type: object + anyOf: + - $ref: "#/definitions/AsdfNode" + definitions: + AsdfNode: + type: object + properties: + name: + type: string + child: + anyOf: + - $ref: "#/definitions/AsdfNode" + - type: null + ``` + + The self-reference happens in ``definitions[AsdfNode].properties.child.anyOf[0]`` + where the `$ref` is a special JSONSchema syntax that referes to the value, + `#/definitions/AsdfNode`. This value is a json path where `#` denotes "this + schema". + """ + + _tag = "asdf://asdf-pydantic/examples/tags/node-2.0.0" + + name: str + child: AsdfNode | None = None + + +@pytest.fixture() +def asdf_extension(): + """Registers an ASDF extension containing models for this test.""" + AsdfPydanticConverter.add_models(AsdfNode) + + class TestExtension(Extension): + extension_uri = "asdf://asdf-pydantic/examples/extensions/test-1.0.0" + + converters = [AsdfPydanticConverter()] # type: ignore + tags = [AsdfNode.get_tag_definition()] # type: ignore + + with asdf.config_context() as asdf_config: + asdf_config.add_resource_mapping( + { + yaml.safe_load(AsdfNode.model_asdf_schema())[ + "id" + ]: AsdfNode.model_asdf_schema() + } + ) + asdf_config.add_extension(TestExtension()) + yield asdf_config + + +@pytest.mark.usefixtures("asdf_extension") +def test_check_schema(): + """Tests the model schema is correct.""" + schema = yaml.safe_load(AsdfNode.model_asdf_schema()) + asdf.schema.check_schema(schema) + + +@pytest.mark.usefixtures("asdf_extension") +def test_can_write_valid_asdf_file(tmp_path): + """Tests using the model to write an ASDF file validates its own schema.""" + af = asdf.AsdfFile() + af["root"] = AsdfNode(name="foo", child=None) + af.validate() + af.write_to(tmp_path / "test.asdf") + + with asdf.open(tmp_path / "test.asdf") as af: + assert af.tree + + +@pytest.mark.usefixtures("asdf_extension") +@patch.object(AsdfNode, "model_validate", MagicMock()) # Ignore pydantic validation +def test_errors_reading_invalid_asdf_file(tmp_path): + """Tests ASDF validation fails when ASDF file does not match the schema.""" + content = """\ + #ASDF 1.0.0 + #ASDF_STANDARD 1.5.0 + %YAML 1.1 + %TAG ! tag:stsci.edu:asdf/ + --- !core/asdf-1.1.0 + asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf', + name: asdf, version: 3.4.0} + history: + extensions: + - !core/extension_metadata-1.0.0 + extension_class: asdf.extension._manifest.ManifestExtension + extension_uri: asdf://asdf-format.org/core/extensions/core-1.5.0 + manifest_software: !core/software-1.0.0 {name: asdf_standard, version: 1.1.1} + software: !core/software-1.0.0 {name: asdf, version: 3.4.0} + - !core/extension_metadata-1.0.0 {extension_class: tests.examples.test_node.setup_module..TestExtension, + extension_uri: 'asdf://asdf-pydantic/examples/extensions/test-1.0.0'} + root: ! + name: foo + child: 1 + ... + """ + with open(tmp_path / "test.asdf", "w") as f: + f.write(textwrap.dedent(content)) + + with pytest.raises(asdf.exceptions.ValidationError): + with asdf.open(tmp_path / "test.asdf") as af: + assert af.tree diff --git a/tests/examples/test_rectangle.py b/tests/examples/test_rectangle.py new file mode 100644 index 0000000..a95a2ba --- /dev/null +++ b/tests/examples/test_rectangle.py @@ -0,0 +1,98 @@ +import textwrap +from unittest.mock import MagicMock, patch + +import asdf +import asdf.schema +import pytest +import yaml +from asdf.extension import Extension + +from asdf_pydantic import AsdfPydanticConverter, AsdfPydanticModel + + +class AsdfRectangle(AsdfPydanticModel): + _tag = "asdf://asdf-pydantic/examples/tags/rectangle-1.0.0" + width: float + height: float + + +@pytest.fixture() +def asdf_extension(): + """Registers an ASDF extension containing models for this test.""" + AsdfPydanticConverter.add_models(AsdfRectangle) + + class TestExtension(Extension): + extension_uri = "asdf://asdf-pydantic/examples/extensions/test-1.0.0" + + converters = [AsdfPydanticConverter()] # type: ignore + tags = [AsdfRectangle.get_tag_definition()] # type: ignore + + with asdf.config_context() as asdf_config: + asdf_config.add_resource_mapping( + { + yaml.safe_load(AsdfRectangle.model_asdf_schema())[ + "id" + ]: AsdfRectangle.model_asdf_schema() + } + ) + asdf_config.add_extension(TestExtension()) + yield asdf_config + + +@pytest.mark.usefixtures("asdf_extension") +def test_check_schema(): + """Tests the model schema is correct.""" + schema = yaml.safe_load(AsdfRectangle.model_asdf_schema()) + asdf.schema.check_schema(schema) + + +@pytest.mark.usefixtures("asdf_extension") +def test_can_write_valid_asdf_file(tmp_path): + """Tests using the model to write an ASDF file validates its own schema.""" + af = asdf.AsdfFile() + af["root"] = AsdfRectangle(width=42, height=10) + af.validate() + af.write_to(tmp_path / "test.asdf") + + with asdf.open(tmp_path / "test.asdf") as af: + assert af.tree + + +@pytest.mark.usefixtures("asdf_extension") +@patch.object( + AsdfRectangle, "model_validate", MagicMock() +) # Ignore pydantic validation +def test_errors_reading_invalid_asdf_file(tmp_path): + """Tests validation fails when ASDF file does not match the schema.""" + content = """\ + #ASDF 1.0.0 + #ASDF_STANDARD 1.5.0 + %YAML 1.1 + %TAG ! tag:stsci.edu:asdf/ + --- !core/asdf-1.1.0 + asdf_library: !core/software-1.0.0 { + author: The ASDF Developers, + homepage: 'http://github.com/asdf-format/asdf', + name: asdf, + version: 2.14.3} + history: + extensions: + - !core/extension_metadata-1.0.0 + extension_class: asdf.extension.BuiltinExtension + software: !core/software-1.0.0 { + name: asdf, + version: 2.14.3} + - !core/extension_metadata-1.0.0 { + extension_class: mypackage.shapes.ShapesExtension, + extension_uri: 'asdf://asdf-pydantic/shapes/extensions/shapes-1.0.0'} + rect: ! + height: "10" + width: "42" + ... + """ + with open(tmp_path / "test.asdf", "w") as f: + f.write(textwrap.dedent(content)) + + with pytest.raises(asdf.exceptions.ValidationError): + with asdf.open(tmp_path / "test.asdf") as af: + assert af.tree diff --git a/tests/patterns/tree_node_test.py b/tests/patterns/tree_node_test.py deleted file mode 100644 index 28c3054..0000000 --- a/tests/patterns/tree_node_test.py +++ /dev/null @@ -1,39 +0,0 @@ -from tempfile import NamedTemporaryFile - -import asdf - -from asdf_pydantic.examples.extensions import ExampleExtension -from asdf_pydantic.examples.tree import AsdfNode, Node - - -def setup_module(): - asdf.get_config().add_extension(ExampleExtension()) - - -def test_asdf_node_root_is_AsdfNode(): - node = AsdfNode() - af = asdf.AsdfFile({"node": node}) - with NamedTemporaryFile() as tempfile: - af.write_to(tempfile.name) - with asdf.open(tempfile.name) as ff: - assert isinstance(ff["node"], AsdfNode) - - -def test_asdf_node_child_is_AsdfNode(): - node = AsdfNode(child=AsdfNode()) - af = asdf.AsdfFile({"node": node}) - with NamedTemporaryFile() as tempfile: - af.write_to(tempfile.name) - - with asdf.open(tempfile.name) as ff: - assert isinstance(ff["node"].child, AsdfNode) - - -def test_regular_node_child_is_dict(): - node = AsdfNode(child=Node()) - af = asdf.AsdfFile({"node": node}) - with NamedTemporaryFile() as tempfile: - af.write_to(tempfile.name) - - with asdf.open(tempfile.name) as ff: - assert isinstance(ff["node"].child, Node) diff --git a/tests/patterns/union_type_test.py b/tests/patterns/union_type_test.py index 557c100..f10d27c 100644 --- a/tests/patterns/union_type_test.py +++ b/tests/patterns/union_type_test.py @@ -15,7 +15,7 @@ class UnionObject(AsdfPydanticModel): # Order of type matters int_or_str: Union[int, str] = 0 datetime_or_time: Union[datetime, Time] = datetime(2023, 1, 1) - collection: Union[tuple, list] = tuple() + collection: Union[list, set] = [] anything: Optional[Any] = None @@ -76,12 +76,12 @@ def list_converts_to_list(tmp_path): assert isinstance(af["obj"].collection, list) -def test_tuple_convert_to_tuple(tmp_path): - af = asdf.AsdfFile({"obj": UnionObject(collection=(1, 1, 1))}) +def test_set_convert_to_set(tmp_path): + af = asdf.AsdfFile({"obj": UnionObject(collection=set())}) af.write_to(tmp_path / "test.asdf") with asdf.open(tmp_path / "test.asdf") as af: - assert isinstance(af["obj"].collection, tuple) + assert isinstance(af["obj"].collection, set) @pytest.mark.parametrize( @@ -92,7 +92,7 @@ def test_tuple_convert_to_tuple(tmp_path): "asdf", dict(key="value"), [1, 2, 3], - (1, 2, 3), + set([1, 2, 3]), {1, 2, 3}, Time("2023-01-01T00:00:00", format="isot", scale="utc"), datetime(2023, 1, 1, 1, 0, 0), diff --git a/tests/schema_validation_test.py b/tests/schema_validation_test.py index 041dfe2..a5365b5 100644 --- a/tests/schema_validation_test.py +++ b/tests/schema_validation_test.py @@ -5,10 +5,10 @@ import pytest import yaml from asdf.extension import Extension -from asdf_pydantic import AsdfPydanticConverter +from asdf_pydantic import AsdfPydanticConverter from asdf_pydantic.examples.shapes import AsdfRectangle -from asdf_pydantic.examples.tree import AsdfNode +from asdf_pydantic.examples.tree import AsdfTreeNode def setup_module(): @@ -28,7 +28,7 @@ class TestExtension(Extension): asdf.get_config().add_resource_mapping( { "asdf://asdf-pydantic/shapes/schemas/rectangle-1.0.0": ( - AsdfRectangle.schema_asdf().encode("utf-8") + AsdfRectangle.model_asdf_schema().encode("utf-8") ) } ) @@ -130,9 +130,9 @@ def test_validate_fail_on_bad_yaml_file(): def test_given_child_field_contains_asdf_object_then_schema_has_child_tag(): from asdf.schema import check_schema - schema = yaml.safe_load(AsdfNode.schema_asdf()) # type: ignore + schema = yaml.safe_load(AsdfTreeNode.model_asdf_schema()) # type: ignore check_schema(schema) child_schema = schema["definitions"]["AsdfNode"]["properties"]["child"] - assert {"tag": AsdfNode._tag} in child_schema["anyOf"] + assert {"tag": AsdfTreeNode._tag} in child_schema["anyOf"] diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..9f78227 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,79 @@ +import pytest +import yaml +from asdf.extension import TagDefinition + +from asdf_pydantic import AsdfPydanticModel + + +######################################################################################## +# TAGS +######################################################################################## +@pytest.mark.parametrize( + "tag", + ( + "asdf://asdf-pydantic/test/tags/", + pytest.param( + TagDefinition("asdf://asdf-pydantic/tags/test-0.0.1"), + marks=pytest.mark.xfail( + reason="Tag definition without schema URIs not supported" + ), + ), + TagDefinition( + "asdf://asdf-pydantic/tags/test-0.0.1", + schema_uris=["asdf://asdf-pydantic/test/schemas/test-0.0.1"], + ), + ), +) +def test_can_get_tag_definition(tag): + class TestModel(AsdfPydanticModel): + _tag = tag + + tag_definition = TestModel.get_tag_definition() + assert isinstance(tag_definition, TagDefinition) + assert tag_definition.schema_uris + + +@pytest.mark.parametrize( + "tag", + ( + "asdf://asdf-pydantic/test/tags/", + TagDefinition("asdf://asdf-pydantic/tags/test-0.0.1"), + TagDefinition( + "asdf://asdf-pydantic/tags/test-0.0.1", + schema_uris=["asdf://asdf-pydantic/test/schemas/test-0.0.1"], + ), + ), +) +def test_can_get_tag_uris(tag): + class TestModel(AsdfPydanticModel): + _tag = tag + + assert TestModel.get_tag_uri() + + +######################################################################################## +# GENERATED SCHEMA +######################################################################################## +def test_generated_schema_keys_in_order(): + class TestModel(AsdfPydanticModel): + _tag = "asdf://asdf-pydantic/tags/test-0.0.1" + foo: str + + assert list(yaml.safe_load(TestModel.model_asdf_schema()).keys()) == [ + "$schema", + "id", + "title", + "type", + "properties", + "required", + ] + + +def test_generated_schema_id_uses_tag_in_pattern(): + class TestModel(AsdfPydanticModel): + _tag = "asdf://asdf-pydantic/tags/test-0.0.1" + + assert ( + yaml.safe_load(TestModel.model_asdf_schema())["id"] + == "asdf://asdf-pydantic/tags/test-0.0.1/schema" + )