Allow constructing iflators for dict->tuple for further args unwrap #2
@@ -47,6 +47,7 @@ fn({'a': 1, 'b': 2, 'c': {'field1': {}, 'field2': '1.1', 'field3': None}})
|
|||||||
- не проверяет типы generic-словарей, кортежей (реализация ожидается)
|
- не проверяет типы generic-словарей, кортежей (реализация ожидается)
|
||||||
- пользовательские проверки типов должны быть реализованы через наследование и проверки в конструкторе
|
- пользовательские проверки типов должны быть реализованы через наследование и проверки в конструкторе
|
||||||
- опциональный `strict-mode`: выключение приведения базовых типов
|
- опциональный `strict-mode`: выключение приведения базовых типов
|
||||||
|
- может генерировать кортежи верхнеуровневых объектов при наличии описания схемы (полезно при развертывании аргументов)
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -101,3 +102,10 @@ class A:
|
|||||||
>>> {"a": [1, 1.1, "321"]}
|
>>> {"a": [1, 1.1, "321"]}
|
||||||
<<< FieldValidationException, т.к. 1.1 не является int
|
<<< FieldValidationException, т.к. 1.1 не является int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Tuple unwrap
|
||||||
|
```
|
||||||
|
fn = infl.schema_to_inflator(
|
||||||
|
(('a', int), TupleSchemaItem(Optional[list[int]], key_name='b', has_default=True, default=None)))
|
||||||
|
```
|
||||||
|
Создаёт `fn: (dict[str,Any]) -> tuple[int, Optional[list[int]]]: ...` (сигнатура остаётся `(dict[str,Any])->tuple`)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "megasniff"
|
name = "megasniff"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
description = "Library for in-time codegened type validation"
|
description = "Library for in-time codegened type validation"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "nikto_b", email = "niktob560@yandex.ru" }
|
{ name = "nikto_b", email = "niktob560@yandex.ru" }
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from collections import defaultdict
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from types import NoneType, UnionType
|
from types import NoneType, UnionType
|
||||||
from typing import Optional, get_origin, get_args, Union, Annotated, Literal, Sequence, List, Set, TypeAliasType
|
from typing import Optional, get_origin, get_args, Union, Annotated, Literal, Sequence, List, Set, TypeAliasType, \
|
||||||
|
OrderedDict
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
@@ -45,14 +46,17 @@ class SchemaInflatorGenerator:
|
|||||||
templateLoader: jinja2.BaseLoader
|
templateLoader: jinja2.BaseLoader
|
||||||
templateEnv: jinja2.Environment
|
templateEnv: jinja2.Environment
|
||||||
|
|
||||||
template: jinja2.Template
|
object_template: jinja2.Template
|
||||||
|
tuple_template: jinja2.Template
|
||||||
_strict_mode: bool
|
_strict_mode: bool
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
loader: Optional[jinja2.BaseLoader] = None,
|
loader: Optional[jinja2.BaseLoader] = None,
|
||||||
strict_mode: bool = False,
|
strict_mode: bool = False,
|
||||||
*,
|
*,
|
||||||
template_filename: str = 'inflator.jinja2'):
|
object_template_filename: str = 'inflator.jinja2',
|
||||||
|
tuple_template_filename: str = 'inflator_tuple.jinja2',
|
||||||
|
):
|
||||||
|
|
||||||
self._strict_mode = strict_mode
|
self._strict_mode = strict_mode
|
||||||
|
|
||||||
@@ -62,10 +66,11 @@ class SchemaInflatorGenerator:
|
|||||||
|
|
||||||
self.templateLoader = loader
|
self.templateLoader = loader
|
||||||
self.templateEnv = jinja2.Environment(loader=self.templateLoader)
|
self.templateEnv = jinja2.Environment(loader=self.templateLoader)
|
||||||
self.template = self.templateEnv.get_template(template_filename)
|
self.object_template = self.templateEnv.get_template(object_template_filename)
|
||||||
|
self.tuple_template = self.templateEnv.get_template(tuple_template_filename)
|
||||||
|
|
||||||
def schema_to_inflator(self,
|
def schema_to_inflator(self,
|
||||||
schema: type,
|
schema: type | Sequence[TupleSchemaItem | tuple[str, type]] | OrderedDict[str, type],
|
||||||
strict_mode_override: Optional[bool] = None,
|
strict_mode_override: Optional[bool] = None,
|
||||||
from_type_override: Optional[type | TypeAliasType] = None
|
from_type_override: Optional[type | TypeAliasType] = None
|
||||||
) -> Callable[[dict[str, Any]], Any]:
|
) -> Callable[[dict[str, Any]], Any]:
|
||||||
@@ -117,20 +122,46 @@ class SchemaInflatorGenerator:
|
|||||||
strict_mode if is_builtin else False)
|
strict_mode if is_builtin else False)
|
||||||
|
|
||||||
def _schema_to_inflator(self,
|
def _schema_to_inflator(self,
|
||||||
schema: type,
|
schema: type | Sequence[TupleSchemaItem | tuple[str, type]] | OrderedDict[str, type],
|
||||||
strict_mode_override: Optional[bool] = None,
|
strict_mode_override: Optional[bool] = None,
|
||||||
from_type_override: Optional[type | TypeAliasType] = None,
|
from_type_override: Optional[type | TypeAliasType] = None,
|
||||||
*,
|
*,
|
||||||
_funcname='inflate',
|
_funcname='inflate',
|
||||||
_namespace=None
|
_namespace=None,
|
||||||
) -> tuple[str, dict]:
|
) -> tuple[str, dict]:
|
||||||
if strict_mode_override is not None:
|
if strict_mode_override is not None:
|
||||||
strict_mode = strict_mode_override
|
strict_mode = strict_mode_override
|
||||||
else:
|
else:
|
||||||
strict_mode = self._strict_mode
|
strict_mode = self._strict_mode
|
||||||
|
|
||||||
|
template = self.object_template
|
||||||
|
mode = 'object'
|
||||||
|
if isinstance(schema, dict):
|
||||||
|
new_schema = []
|
||||||
|
for argname, argtype in schema.items():
|
||||||
|
new_schema.append((argname, argtype))
|
||||||
|
schema = new_schema
|
||||||
|
|
||||||
|
if isinstance(schema, collections.abc.Iterable):
|
||||||
|
template = self.tuple_template
|
||||||
|
mode = 'tuple'
|
||||||
|
new_schema = []
|
||||||
|
for t in schema:
|
||||||
|
if isinstance(t, TupleSchemaItem):
|
||||||
|
new_schema.append(t)
|
||||||
|
else:
|
||||||
|
new_schema.append(TupleSchemaItem(t[1], key_name=t[0]))
|
||||||
|
schema = new_schema
|
||||||
|
|
||||||
# Я это написал, оно пока работает, и я не собираюсь это упрощать, сорян
|
# Я это написал, оно пока работает, и я не собираюсь это упрощать, сорян
|
||||||
type_hints = get_kwargs_type_hints(schema)
|
if mode == 'object':
|
||||||
|
type_hints = get_kwargs_type_hints(schema)
|
||||||
|
else:
|
||||||
|
type_hints = {}
|
||||||
|
for i, t in enumerate(schema):
|
||||||
|
n = t.key_name or f'_arg_{i}'
|
||||||
|
type_hints[n] = t.schema
|
||||||
|
|
||||||
render_data = []
|
render_data = []
|
||||||
|
|
||||||
txt_segments = []
|
txt_segments = []
|
||||||
@@ -143,8 +174,10 @@ class SchemaInflatorGenerator:
|
|||||||
if namespace.get(f'{_funcname}_tgt_type') is not None:
|
if namespace.get(f'{_funcname}_tgt_type') is not None:
|
||||||
return '', namespace
|
return '', namespace
|
||||||
|
|
||||||
namespace[f'{_funcname}_tgt_type'] = schema
|
if mode == 'object':
|
||||||
namespace[utils.typename(schema)] = schema
|
namespace[f'{_funcname}_tgt_type'] = schema
|
||||||
|
namespace[utils.typename(schema)] = schema
|
||||||
|
|
||||||
if from_type_override is not None:
|
if from_type_override is not None:
|
||||||
namespace['_from_type'] = from_type_override
|
namespace['_from_type'] = from_type_override
|
||||||
|
|
||||||
@@ -202,7 +235,7 @@ class SchemaInflatorGenerator:
|
|||||||
else:
|
else:
|
||||||
namespace[argt.__name__] = argt
|
namespace[argt.__name__] = argt
|
||||||
|
|
||||||
convertor_functext = self.template.render(
|
convertor_functext = template.render(
|
||||||
funcname=_funcname,
|
funcname=_funcname,
|
||||||
conversions=render_data,
|
conversions=render_data,
|
||||||
tgt_type=utils.typename(schema),
|
tgt_type=utils.typename(schema),
|
||||||
|
|||||||
32
src/megasniff/templates/inflator_tuple.jinja2
Normal file
32
src/megasniff/templates/inflator_tuple.jinja2
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% set ns = namespace(retry_indent=0) %}
|
||||||
|
{% import "unwrap_type_data.jinja2" as unwrap_type_data %}
|
||||||
|
|
||||||
|
|
||||||
|
def {{funcname}}(from_data: {% if from_type is none %}dict[str, Any]{% else %}{{from_type}}{% endif %}) {% if tgt_type is not none %} -> tuple {% endif %}:
|
||||||
|
"""
|
||||||
|
{% for conv in conversions %}{{conv.argname}}:{{conv.typename}}, {% endfor %}
|
||||||
|
"""
|
||||||
|
from_data_keys = from_data.keys()
|
||||||
|
|
||||||
|
{% for conv in conversions %}
|
||||||
|
if '{{conv.argname}}' not in from_data_keys:
|
||||||
|
{% if conv.is_optional %}
|
||||||
|
{{conv.argname}} = {{conv.default_option}}
|
||||||
|
{% else %}
|
||||||
|
raise MissingFieldException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}")
|
||||||
|
{% endif %}
|
||||||
|
else:
|
||||||
|
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 %}
|
||||||
|
{{conv.argname}} = None
|
||||||
|
{% endif %}
|
||||||
|
else:
|
||||||
|
|
||||||
|
{{ unwrap_type_data.render_segment(conv.argname, conv.constrs, "conv_data", false) | indent(4*3) }}
|
||||||
|
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
return ({% for conv in conversions %}{{conv.argname}}, {% endfor %})
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import collections.abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
from typing import get_type_hints, Any, get_origin
|
from typing import get_type_hints, Any, get_origin, Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class TupleSchemaItem:
|
||||||
|
schema: type
|
||||||
|
key_name: str
|
||||||
|
has_default: bool = False
|
||||||
|
default: Any = None
|
||||||
|
|
||||||
|
|
||||||
def is_typed_dict_type(tp: type) -> bool:
|
def is_typed_dict_type(tp: type) -> bool:
|
||||||
@@ -19,7 +28,7 @@ def get_kwargs_type_hints(obj: type) -> dict[str, Any]:
|
|||||||
return get_type_hints(obj.__init__)
|
return get_type_hints(obj.__init__)
|
||||||
|
|
||||||
|
|
||||||
def get_field_default(cls: type[Any], field: str) -> tuple[bool, Any]:
|
def get_field_default(cls: type[Any] | Iterable[TupleSchemaItem], field: str) -> tuple[bool, Any]:
|
||||||
if dataclasses.is_dataclass(cls):
|
if dataclasses.is_dataclass(cls):
|
||||||
for f in dataclasses.fields(cls):
|
for f in dataclasses.fields(cls):
|
||||||
if f.name == field:
|
if f.name == field:
|
||||||
@@ -32,6 +41,12 @@ def get_field_default(cls: type[Any], field: str) -> tuple[bool, Any]:
|
|||||||
# поле не объявлено в dataclass
|
# поле не объявлено в dataclass
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
if isinstance(cls, collections.abc.Iterable):
|
||||||
|
for i, t in enumerate(cls):
|
||||||
|
if (t.key_name or f'_arg_{i}') == field:
|
||||||
|
return t.has_default, t.default
|
||||||
|
return False, None
|
||||||
|
|
||||||
sig = inspect.signature(cls.__init__)
|
sig = inspect.signature(cls.__init__)
|
||||||
|
|
||||||
params = list(sig.parameters.values())[1:]
|
params = list(sig.parameters.values())[1:]
|
||||||
@@ -53,6 +68,6 @@ def is_builtin_type(tp: type) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def typename(tp: type) -> str:
|
def typename(tp: type) -> str:
|
||||||
if get_origin(tp) is None:
|
if get_origin(tp) is None and hasattr(tp, '__name__'):
|
||||||
return tp.__name__
|
return tp.__name__
|
||||||
return str(tp)
|
return str(tp)
|
||||||
|
|||||||
27
tests/test_tuple_inflate.py
Normal file
27
tests/test_tuple_inflate.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.megasniff.utils import TupleSchemaItem
|
||||||
|
from src.megasniff import SchemaInflatorGenerator
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_tuple():
|
||||||
|
infl = SchemaInflatorGenerator()
|
||||||
|
|
||||||
|
fn = infl.schema_to_inflator({'a': int, 'b': float, 'c': str, 'd': list[int]})
|
||||||
|
a = fn({'a': 42, 'b': 1.1, 'c': 123, 'd': []})
|
||||||
|
|
||||||
|
assert a[0] == 42
|
||||||
|
|
||||||
|
fn = infl.schema_to_inflator((('a', int), ('b', list[int])))
|
||||||
|
a = fn({'a': 42, 'b': ['1']})
|
||||||
|
|
||||||
|
assert a[1][0] == 1
|
||||||
|
|
||||||
|
fn = infl.schema_to_inflator(
|
||||||
|
(('a', int), TupleSchemaItem(Optional[list[int]], key_name='b', has_default=True, default=None)))
|
||||||
|
a = fn({'a': 42})
|
||||||
|
|
||||||
|
assert a[1] is None
|
||||||
|
assert a[0] == 42
|
||||||
Reference in New Issue
Block a user