Skip to content

Commit

Permalink
Merge branch 'v2' into main (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
ketozhang authored Oct 7, 2024
2 parents 7799b25 + a9e9c60 commit 5ab9a1c
Show file tree
Hide file tree
Showing 18 changed files with 492 additions and 103 deletions.
12 changes: 2 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ venv.bak/
# Rope project settings
.ropeproject

# ruff
.ruff_cache/

# mkdocs documentation
/site

Expand Down
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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'
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion asdf_pydantic/__version__.py

This file was deleted.

2 changes: 1 addition & 1 deletion asdf_pydantic/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions asdf_pydantic/examples/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions asdf_pydantic/examples/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
76 changes: 53 additions & 23 deletions asdf_pydantic/model.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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 = {}
Expand All @@ -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)
62 changes: 62 additions & 0 deletions asdf_pydantic/schema.py
Original file line number Diff line number Diff line change
@@ -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
38 changes: 32 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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"]

Expand All @@ -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",
Expand All @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions tests/convert_to_asdf_yaml_tree_test.py
Original file line number Diff line number Diff line change
@@ -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}}
}
Empty file added tests/examples/__init__.py
Empty file.
Loading

0 comments on commit 5ab9a1c

Please sign in to comment.