Add custom exceptions, simplify generation template

This commit is contained in:
2025-07-12 02:37:54 +03:00
parent ed5f975e87
commit 1994eaab0d
5 changed files with 99 additions and 51 deletions

View File

@@ -0,0 +1,22 @@
from typing import Any, Optional
class MissingFieldException(Exception):
def __init__(self, required_field: str, required_types: str):
message = f"No required field provided: {required_field} with type {required_types}"
super().__init__(message)
self.required_field = required_field
self.required_types = required_types
class FieldValidationException(Exception):
def __init__(self,
required_field: str,
required_types: str,
provided: Any,
exceptions: Optional[list[Exception]] = None):
message = f"Required field {required_field} with type {required_types}, provided: {provided}"
super().__init__(message)
self.required_field = required_field
self.required_types = required_types
self.exceptions = exceptions or []

View File

@@ -17,7 +17,6 @@ class RenderData:
argname: str
constrs: list[tuple[str, bool]] # typecall / use lookup table
typename: str
is_union: bool
is_optional: bool
allow_none: bool
default_option: Optional[str]
@@ -85,7 +84,6 @@ class SchemaInflatorGenerator:
argname,
out_argtypes,
repr(argtype),
len(argtypes) > 1,
has_default,
allow_none,
default_option

View File

@@ -1,65 +1,42 @@
{% set ns = namespace(retry_indent=0) %}
from typing import Any
from megasniff.exceptions import MissingFieldException, FieldValidationException
def inflate(from_data: dict[str, Any]):
from_data_keys = from_data.keys()
{% for conv in conversions %}
{% if not conv.is_optional or conv.default_option is not none%}
if '{{conv.argname}}' not in from_data_keys:
{% if not conv.is_optional %}
raise ValueError(f"No required field provided: {{conv.argname}} with type {{conv.typename | replace('"', "'")}}")
{% endif %}
{% if conv.default_option is not none %}
from_data['{{conv.argname}}'] = {{conv.default_option}}
{% endif%}
{% endif %}
{%endfor%}
{% for conv in conversions %}
conv_data = from_data['{{conv.argname}}']
if conv_data is None:
{% if not conv.allow_none %}
raise ValueError(f"Field {{conv.argname}} required type {{conv.typename | replace('"', "'")}}, null provided")
{% if conv.is_optional %}
{{conv.argname}} = {{conv.default_option}}
{% else %}
{{conv.argname}} = None
raise MissingFieldException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}")
{% endif %}
else:
{% if conv.is_union %}
{% set ns.retry_indent = 0 %}
{% for union_type, is_builtin in conv.constrs %}
{{ ' ' * ns.retry_indent }}try:
{% if is_builtin %}
{{ ' ' * ns.retry_indent }} {{conv.argname}} = {{union_type}}(conv_data)
conv_data = from_data['{{conv.argname}}']
if conv_data is None:
{% if not conv.allow_none %}
raise FieldValidationException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}", conv_data)
{% else %}
{{ ' ' * ns.retry_indent }} {{conv.argname}} = _lookup_table['{{union_type}}'](conv_data)
{% endif %}
{{ ' ' * ns.retry_indent }}except Exception as e:
{% set ns.retry_indent = ns.retry_indent + 1 %}
{% endfor %}
{{ ' ' * ns.retry_indent }} raise e from e
{% else %}
{% if conv.constrs[0][1] %}
{% if conv.is_optional %}
if '{{conv.argname}}' not in from_data_keys:
{{conv.argname}} = None
{% endif %}
else:
{{conv.argname}} = {{conv.constrs[0][0]}}(conv_data)
{% else %}
{{conv.argname}} = {{conv.constrs[0][0]}}(conv_data)
{% endif %}
{% else %}
{% if conv.is_optional %}
if '{{conv.argname}}' not in from_data_keys:
{{conv.argname}} = None
else:
{{conv.argname}} = _lookup_table['{{conv.constrs[0][0]}}'](conv_data)
{% else %}
{{conv.argname}} = _lookup_table['{{conv.constrs[0][0]}}'](conv_data)
{% endif %}
{% endif %}
{% endif %}
{% set ns.retry_indent = 0 %}
all_conv_exceptions = []
{% for union_type, is_builtin in conv.constrs %}
{{ ' ' * ns.retry_indent }}try:
{% if is_builtin %}
{{ ' ' * ns.retry_indent }} {{conv.argname}} = {{union_type}}(conv_data)
{% else %}
{{ ' ' * ns.retry_indent }} {{conv.argname}} = _lookup_table['{{union_type}}'](conv_data)
{% endif %}
{{ ' ' * ns.retry_indent }}except Exception as e:
{{ ' ' * ns.retry_indent }} all_conv_exceptions.append(e)
{% set ns.retry_indent = ns.retry_indent + 1 %}
{% endfor %}
{{ ' ' * ns.retry_indent }}raise FieldValidationException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}", conv_data, all_conv_exceptions)
{% endfor %}
return _tgt_type({% for conv in conversions %}{{conv.argname}}={{conv.argname}}, {% endfor %})

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from src.megasniff import SchemaInflatorGenerator
@@ -32,3 +33,14 @@ def test_circular():
a = fn({'b': {'a': None}})
return isinstance(a.b, CircB)
def test_optional():
@dataclass
class C:
a: Optional[int] = None
infl = SchemaInflatorGenerator()
fn = infl.schema_to_generator(C)
c = fn({})
assert c.a is None

39
tests/test_exceptions.py Normal file
View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass
import pytest
from megasniff import SchemaInflatorGenerator
from megasniff.exceptions import MissingFieldException, FieldValidationException
def test_missing_field():
@dataclass
class A:
a: int
infl = SchemaInflatorGenerator()
fn = infl.schema_to_generator(A)
with pytest.raises(MissingFieldException):
fn({})
def test_null():
@dataclass
class A:
a: int
infl = SchemaInflatorGenerator()
fn = infl.schema_to_generator(A)
with pytest.raises(FieldValidationException):
fn({'a': None})
def test_invalid_field():
@dataclass
class A:
a: float | int | None
infl = SchemaInflatorGenerator()
fn = infl.schema_to_generator(A)
with pytest.raises(FieldValidationException):
fn({'a': {}})