From 1994eaab0de9cdcde09dcd3b5026244d469e4a02 Mon Sep 17 00:00:00 2001 From: nikto_b Date: Sat, 12 Jul 2025 02:37:54 +0300 Subject: [PATCH] Add custom exceptions, simplify generation template --- src/megasniff/exceptions.py | 22 ++++++++ src/megasniff/inflator.py | 2 - src/megasniff/templates/inflator.jinja2 | 75 +++++++++---------------- tests/test_basic.py | 12 ++++ tests/test_exceptions.py | 39 +++++++++++++ 5 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 src/megasniff/exceptions.py create mode 100644 tests/test_exceptions.py diff --git a/src/megasniff/exceptions.py b/src/megasniff/exceptions.py new file mode 100644 index 0000000..90d90f6 --- /dev/null +++ b/src/megasniff/exceptions.py @@ -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 [] diff --git a/src/megasniff/inflator.py b/src/megasniff/inflator.py index 9faba36..1cde799 100644 --- a/src/megasniff/inflator.py +++ b/src/megasniff/inflator.py @@ -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 diff --git a/src/megasniff/templates/inflator.jinja2 b/src/megasniff/templates/inflator.jinja2 index b3ac531..19605e4 100644 --- a/src/megasniff/templates/inflator.jinja2 +++ b/src/megasniff/templates/inflator.jinja2 @@ -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 %}) diff --git a/tests/test_basic.py b/tests/test_basic.py index 61eff88..db7202c 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -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 diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..9783578 --- /dev/null +++ b/tests/test_exceptions.py @@ -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': {}})