• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bogdandm / json2python-models / 11498135606

pending completion
11498135606

Pull #59

github

web-flow
Merge 2387ea51f into e2606e8f2
Pull Request #59: Modernize project setup and setup cron action job

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.8
/json_to_models/models/string_converters.py
1
from functools import wraps
2✔
2
from inspect import isclass
2✔
3
from typing import Any, Callable, List, Optional, Tuple
2✔
4

5
from . import ClassType
2✔
6
from ..dynamic_typing import (
2✔
7
    BaseType,
8
    DDict,
9
    DList,
10
    DOptional,
11
    DUnion,
12
    MetaData,
13
    ModelMeta,
14
    ModelPtr,
15
    StringLiteral,
16
    StringSerializable
17
)
18
from ..dynamic_typing.base import NoneType
2✔
19

20

21
def convert_strings(str_field_paths: List[str], class_type: Optional[ClassType] = None,
2✔
22
                    method: Optional[str] = None) -> Callable[[type], type]:
23
    """
24
    Decorator factory. Set up post-init method to convert strings fields values into StringSerializable types
25

26
    If field contains complex data type path should be consist of field name and dotted list of tokens:
27

28
    * `S` - string component
29
    * `O` - Optional
30
    * `L` - List
31
    * `D` - Dict
32

33
    So if field `'bar'` has type `Optional[List[List[IntString]]]` field path would be `'bar#O.L.L.S'`
34

35
    ! If type is too complex i.e. Union[List[IntString], List[List[IntString]]]
36
    you can't specify field path and such field would be ignored
37

38
    To specify name of post-init method you should provide it by class_type argument or directly by method argument:
39

40
    >>> convert_strings([...], class_type=ClassType.Attrs)
41

42
    is equivalent of
43

44
    >>> convert_strings([...], method="__attrs_post_init__")
45

46
    :param str_field_paths: Paths of StringSerializable fields (field name or field name + typing path)
47
    :param class_type: attrs | dataclass - type of decorated class
48
    :param method: post-init method name
49
    :return: Class decorator
50
    """
51
    method = {
2✔
52
        ClassType.Attrs: '__attrs_post_init__',
53
        ClassType.Dataclass: '__post_init__',
54
        None: method
55
    }.get(class_type)
56

57
    def decorator(cls: type) -> type:
2✔
58
        if hasattr(cls, method):
2✔
59
            old_fn = getattr(cls, method)
2✔
60

61
            @wraps(old_fn)
2✔
62
            def __post_init__(self, *args, **kwargs):
2✔
63
                post_init_converters(str_field_paths)(self)
2✔
64
                old_fn(self, *args, **kwargs)
2✔
65

66
            setattr(cls, method, __post_init__)
2✔
67
        else:
68
            fn = post_init_converters(str_field_paths)
2✔
69
            fn.__name__ = method
2✔
70
            setattr(cls, method, fn)
2✔
71

72
        return cls
2✔
73

74
    return decorator
2✔
75

76

77
def post_init_converters(str_fields: List[str], wrap_fn=None):
2✔
78
    """
79
    Method factory. Return post_init method to convert string into StringSerializable types
80
    To override generated __post_init__ you can call it directly:
81

82
    >>> def __post_init__(self):
83
    ...     post_init_converters(['a', 'b'])(self)
84

85
    :param str_fields: names of StringSerializable fields
86
    :return: __post_init__ method
87
    """
88

89
    def __post_init__(self):
2✔
90
        # `S` - string component
91
        # `O` - Optional
92
        # `L` - List
93
        # `D` - Dict
94
        for name in str_fields:
2✔
95
            if '#' in name:
2✔
96
                name, path_str = name.split('#')
2✔
97
                path: List[str] = path_str.split('.')
2✔
98
            else:
99
                path = ['S']
2✔
100

101
            new_value = _process_string_field_value(
2✔
102
                path=path,
103
                value=getattr(self, name),
104
                current_type=self.__annotations__[name]
105
            )
106
            setattr(self, name, new_value)
2✔
107

108
    if wrap_fn:
2✔
109
        __post_init__ = wraps(wrap_fn)(__post_init__)
×
110

111
    return __post_init__
2✔
112

113

114
def _process_string_field_value(path: List[str], value: Any, current_type: Any, optional=False) -> Any:
2✔
115
    token, *path = path
2✔
116
    if token == 'S':
2✔
117
        try:
2✔
118
            value = current_type.to_internal_value(value)
2✔
119
        except (ValueError, TypeError) as e:
2✔
120
            if not optional:
2✔
121
                raise e
×
122
        return value
2✔
123
    elif token == 'O':
2✔
124
        return _process_string_field_value(
2✔
125
            path=path,
126
            value=value,
127
            current_type=current_type.__args__[0],
128
            optional=True
129
        )
130
    elif token == 'L':
2✔
131
        t = current_type.__args__[0]
2✔
132
        return [
2✔
133
            _process_string_field_value(path, item, current_type=t, optional=optional)
134
            for item in value
135
        ]
136
    elif token == 'D':
2✔
137
        t = current_type.__args__[1]
2✔
138
        return {
2✔
139
            key: _process_string_field_value(path, item, current_type=t, optional=optional)
140
            for key, item in value.items()
141
        }
142
    else:
143
        raise ValueError(f"Unknown token {token}")
×
144

145

146
def get_string_field_paths(model: ModelMeta) -> List[Tuple[str, List[str]]]:
2✔
147
    """
148
    Return paths for convert_strings function of given model
149

150
    :return: Paths with raw names
151
    """
152
    # `S` - string component
153
    # `O` - Optional
154
    # `L` - List
155
    # `D` - Dict
156
    str_fields: List[Tuple[str, List[str]]] = []
2✔
157
    for name, t in model.type.items():
2✔
158

159
        # Walk through nested types
160
        paths: List[List[str]] = []
2✔
161
        tokens: List[Tuple[MetaData, List[str]]] = [(t, ['#'])]
2✔
162
        while tokens:
2✔
163
            tmp_type, path = tokens.pop()
2✔
164
            if isclass(tmp_type):
2✔
165
                if issubclass(tmp_type, StringSerializable):
2✔
166
                    paths.append(path + ['S'])
2✔
167
            elif isinstance(tmp_type, BaseType):
2✔
168
                cls = type(tmp_type)
2✔
169
                if cls is DOptional:
2✔
170
                    token = 'O'
2✔
171
                elif cls is DList:
2✔
172
                    token = 'L'
2✔
173
                elif cls is DDict:
2✔
174
                    token = 'D'
2✔
175
                elif cls in (DUnion, ModelPtr):
2✔
176
                    # We could not resolve Union
177
                    paths = []
2✔
178
                    break
2✔
179
                elif cls is NoneType:
×
180
                    continue
×
181
                elif cls in (StringLiteral,):
×
182
                    continue
×
183
                else:
184
                    raise TypeError(f"Unsupported meta-type for converter path {cls}")
×
185

186
                for nested_type in tmp_type:
2✔
187
                    tokens.append((nested_type, path + [token]))
2✔
188
        paths: List[str] = ["".join(p[1:]) for p in paths]
2✔
189
        if len(paths) != 1:
2✔
190
            continue
2✔
191

192
        path = paths.pop()
2✔
193
        if path == 'S':
2✔
194
            str_fields.append((name, []))
2✔
195
        else:
196
            str_fields.append((name, path))
2✔
197

198
    return str_fields
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc