From 8d8289f2247a3163724920e9bcf469218a1f5b2b Mon Sep 17 00:00:00 2001 From: "Keto D. Zhang" Date: Sun, 25 Aug 2024 14:38:52 -0700 Subject: [PATCH 1/3] chore: Use uv in hatch --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a8598ce..b98d09f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ source = "vcs" # Default Environment [tool.hatch.envs.default] +installer = "uv" dependencies = [ "ipython", "pytest", From 5e17c499d15fc6934c7fcf1c99e61bbfa9b51823 Mon Sep 17 00:00:00 2001 From: "Keto D. Zhang" Date: Sun, 25 Aug 2024 14:46:34 -0700 Subject: [PATCH 2/3] refactor: use pydantic v2 ConfigDict --- asdf_pydantic/model.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/asdf_pydantic/model.py b/asdf_pydantic/model.py index 7de635d..4429afe 100644 --- a/asdf_pydantic/model.py +++ b/asdf_pydantic/model.py @@ -1,7 +1,7 @@ from typing import ClassVar import yaml -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from typing_extensions import deprecated from asdf_pydantic.schema import DEFAULT_ASDF_SCHEMA_REF_TEMPLATE, GenerateAsdfSchema @@ -17,9 +17,7 @@ class AsdfPydanticModel(BaseModel): """ _tag: ClassVar[str] - - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) def asdf_yaml_tree(self) -> dict: d = {} @@ -59,9 +57,7 @@ def model_asdf_schema( ) json_schema = schema_generator_instance.generate(cls.__pydantic_core_schema__) - header = "%YAML 1.1\n---\n" - - return f"{header}\n{yaml.safe_dump(json_schema)}" + return f"%YAML 1.1\n---\n{yaml.safe_dump(json_schema)}" @classmethod @deprecated( From d127085ef603a92150559fc3af3f652dd84a796b Mon Sep 17 00:00:00 2001 From: "Keto D. Zhang" Date: Sun, 25 Aug 2024 16:59:31 -0700 Subject: [PATCH 3/3] feat: set tag field with `asdf.extension.TagDefinition` --- asdf_pydantic/converter.py | 2 +- asdf_pydantic/model.py | 23 ++++++++++++++--- asdf_pydantic/schema.py | 11 ++++---- tests/test_model.py | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 tests/test_model.py 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/model.py b/asdf_pydantic/model.py index 4429afe..09663a0 100644 --- a/asdf_pydantic/model.py +++ b/asdf_pydantic/model.py @@ -1,6 +1,7 @@ from typing import ClassVar import yaml +from asdf.extension import TagDefinition from pydantic import BaseModel, ConfigDict from typing_extensions import deprecated @@ -16,7 +17,7 @@ class AsdfPydanticModel(BaseModel): AsdfPydanticModel object with py:meth`AsdfPydanticModel.parse_obj()`. """ - _tag: ClassVar[str] + _tag: ClassVar[str | TagDefinition] model_config = ConfigDict(arbitrary_types_allowed=True) def asdf_yaml_tree(self) -> dict: @@ -41,6 +42,22 @@ 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, @@ -51,9 +68,7 @@ def model_asdf_schema( """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=cls._tag, + by_alias=by_alias, ref_template=ref_template, tag_uri=cls.get_tag_uri() ) json_schema = schema_generator_instance.generate(cls.__pydantic_core_schema__) diff --git a/asdf_pydantic/schema.py b/asdf_pydantic/schema.py index 321144f..c8ad21f 100644 --- a/asdf_pydantic/schema.py +++ b/asdf_pydantic/schema.py @@ -16,25 +16,24 @@ class GenerateAsdfSchema(GenerateJsonSchema): """ # HACK: When we can support tree models, then not all schema should have tag - tag: Optional[str] 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: Optional[str] = None, + tag_uri: Optional[str] = None, ): super().__init__(by_alias=by_alias, ref_template=ref_template) - self.tag = tag + self.tag_uri = tag_uri def generate(self, schema, mode="validation"): json_schema = super().generate(schema, mode) # noqa: F841 - if self.tag: + if self.tag_uri: json_schema["$schema"] = self.schema_dialect - json_schema["id"] = self.tag - json_schema["tag"] = f"tag:{self.tag.split('://', maxsplit=2)[-1]}" + json_schema["id"] = self.tag_uri + json_schema["tag"] = f"tag:{self.tag_uri.split('://', maxsplit=2)[-1]}" # TODO: Convert jsonschema 2020-12 to ASDF schema return json_schema diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..0566ab5 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,51 @@ +import pytest +from asdf.extension import TagDefinition + +from asdf_pydantic import AsdfPydanticModel + + +def tag(request): + return request.param + + +@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()