Allow constructing iflators for dict->tuple for further args unwrap
This commit is contained in:
@@ -47,6 +47,7 @@ fn({'a': 1, 'b': 2, 'c': {'field1': {}, 'field2': '1.1', 'field3': None}})
|
||||
- не проверяет типы generic-словарей, кортежей (реализация ожидается)
|
||||
- пользовательские проверки типов должны быть реализованы через наследование и проверки в конструкторе
|
||||
- опциональный `strict-mode`: выключение приведения базовых типов
|
||||
- может генерировать кортежи верхнеуровневых объектов при наличии описания схемы (полезно при развертывании аргументов)
|
||||
|
||||
----
|
||||
|
||||
@@ -101,3 +102,10 @@ class A:
|
||||
>>> {"a": [1, 1.1, "321"]}
|
||||
<<< 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]
|
||||
name = "megasniff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "Library for in-time codegened type validation"
|
||||
authors = [
|
||||
{ name = "nikto_b", email = "niktob560@yandex.ru" }
|
||||
|
||||
@@ -7,7 +7,8 @@ from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
|
||||
@@ -45,14 +46,17 @@ class SchemaInflatorGenerator:
|
||||
templateLoader: jinja2.BaseLoader
|
||||
templateEnv: jinja2.Environment
|
||||
|
||||
template: jinja2.Template
|
||||
object_template: jinja2.Template
|
||||
tuple_template: jinja2.Template
|
||||
_strict_mode: bool
|
||||
|
||||
def __init__(self,
|
||||
loader: Optional[jinja2.BaseLoader] = None,
|
||||
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
|
||||
|
||||
@@ -62,10 +66,11 @@ class SchemaInflatorGenerator:
|
||||
|
||||
self.templateLoader = loader
|
||||
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,
|
||||
schema: type,
|
||||
schema: type | Sequence[TupleSchemaItem | tuple[str, type]] | OrderedDict[str, type],
|
||||
strict_mode_override: Optional[bool] = None,
|
||||
from_type_override: Optional[type | TypeAliasType] = None
|
||||
) -> Callable[[dict[str, Any]], Any]:
|
||||
@@ -117,20 +122,46 @@ class SchemaInflatorGenerator:
|
||||
strict_mode if is_builtin else False)
|
||||
|
||||
def _schema_to_inflator(self,
|
||||
schema: type,
|
||||
schema: type | Sequence[TupleSchemaItem | tuple[str, type]] | OrderedDict[str, type],
|
||||
strict_mode_override: Optional[bool] = None,
|
||||
from_type_override: Optional[type | TypeAliasType] = None,
|
||||
*,
|
||||
_funcname='inflate',
|
||||
_namespace=None
|
||||
_namespace=None,
|
||||
) -> tuple[str, dict]:
|
||||
if strict_mode_override is not None:
|
||||
strict_mode = strict_mode_override
|
||||
else:
|
||||
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 = []
|
||||
|
||||
txt_segments = []
|
||||
@@ -143,8 +174,10 @@ class SchemaInflatorGenerator:
|
||||
if namespace.get(f'{_funcname}_tgt_type') is not None:
|
||||
return '', namespace
|
||||
|
||||
namespace[f'{_funcname}_tgt_type'] = schema
|
||||
namespace[utils.typename(schema)] = schema
|
||||
if mode == 'object':
|
||||
namespace[f'{_funcname}_tgt_type'] = schema
|
||||
namespace[utils.typename(schema)] = schema
|
||||
|
||||
if from_type_override is not None:
|
||||
namespace['_from_type'] = from_type_override
|
||||
|
||||
@@ -202,7 +235,7 @@ class SchemaInflatorGenerator:
|
||||
else:
|
||||
namespace[argt.__name__] = argt
|
||||
|
||||
convertor_functext = self.template.render(
|
||||
convertor_functext = template.render(
|
||||
funcname=_funcname,
|
||||
conversions=render_data,
|
||||
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 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:
|
||||
@@ -19,7 +28,7 @@ def get_kwargs_type_hints(obj: type) -> dict[str, Any]:
|
||||
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):
|
||||
for f in dataclasses.fields(cls):
|
||||
if f.name == field:
|
||||
@@ -32,6 +41,12 @@ def get_field_default(cls: type[Any], field: str) -> tuple[bool, Any]:
|
||||
# поле не объявлено в dataclass
|
||||
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__)
|
||||
|
||||
params = list(sig.parameters.values())[1:]
|
||||
@@ -53,6 +68,6 @@ def is_builtin_type(tp: type) -> bool:
|
||||
|
||||
|
||||
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 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