-
Notifications
You must be signed in to change notification settings - Fork 53
feat(pydantic): add ToonPydanticModel with schema_to_toon() and from_toon() #46
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .serializer import ToonPydanticModel | ||
|
|
||
| __all__ = ["ToonPydanticModel"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TypeVar, Type | ||
|
|
||
| from pydantic import BaseModel, ValidationError | ||
| from toon_format import encode, decode | ||
|
|
||
| T = TypeVar("T", bound="ToonPydanticModel") | ||
|
|
||
|
|
||
| class ToonPydanticModel(BaseModel): | ||
| """ | ||
| Pydantic mixin that adds TOON superpowers. | ||
|
|
||
| • schema_to_toon() → TOON schema string (for LLM few-shot / system prompts) | ||
| • from_toon() → Parse TOON output directly into validated model | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why only deserialization? I would have expected also a serialization method, like |
||
| """ | ||
|
|
||
| @classmethod | ||
| def schema_to_toon(cls) -> str: | ||
| """ | ||
| Convert the model's JSON schema into compact TOON format. | ||
| Use this in your LLM prompt to save 40–60% tokens vs JSON schema. | ||
| """ | ||
| schema = cls.model_json_schema() | ||
| # Pydantic gives us full JSON schema | ||
| return encode(schema) | ||
|
|
||
| @classmethod | ||
| def from_toon(cls: Type[T], text: str) -> T: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Starting from pydantic v2 the naming convention is to prefix all methods with |
||
| """ | ||
| Parse raw TOON string (from LLM) into a fully validated Pydantic model. | ||
|
|
||
| Raises: | ||
| ValueError – If TOON parsing fails | ||
| ValidationError – If data doesn't match model | ||
| ValueError – Friendly wrapper for both | ||
| """ | ||
| if not text.strip(): | ||
| raise ValueError("Empty string cannot be parsed as TOON") | ||
|
|
||
| try: | ||
| data = decode(text.strip()) | ||
| return cls.model_validate(data) | ||
| except ValidationError as e: | ||
| raise e # Let Pydantic's rich error surface (best UX) | ||
| except Exception as e: | ||
| raise ValueError(f"Failed to parse TOON into {cls.__name__}: {e}") from e | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import pytest | ||
| from pydantic import BaseModel, ValidationError | ||
|
|
||
| from toon_format.pydantic import ToonPydanticModel | ||
|
|
||
|
|
||
| class User(ToonPydanticModel): | ||
| name: str | ||
| age: int | ||
| email: str | None = None | ||
|
|
||
|
|
||
| def test_schema_to_toon(): | ||
| schema = User.schema_to_toon() | ||
| assert "name:str" in schema | ||
| assert "age:int" in schema | ||
| assert "email:" in schema # optional field | ||
|
|
||
|
|
||
| def test_from_toon_success(): | ||
| toon = "name:Ansar\nage:25\nemail:null" | ||
| user = User.from_toon(toon) | ||
| assert user.name == "Ansar" | ||
| assert user.age == 25 | ||
| assert user.email is None | ||
|
|
||
|
|
||
| def test_from_toon_validation_error(): | ||
| toon = "name:Ansar\nage:twenty-five" # wrong type | ||
| with pytest.raises(ValidationError): | ||
| User.from_toon(toon) | ||
|
|
||
|
|
||
| def test_from_toon_empty_string(): | ||
| with pytest.raises(ValueError, match="Empty string"): | ||
| User.from_toon("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we want to introduce a dependency from
poetry, I would use the standard[project.optional-dependencies]in this case. Is there any particular reason to use poetry?