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

aas-core-works / aas-core-codegen / 21506615699

30 Jan 2026 06:23AM UTC coverage: 82.215% (+0.1%) from 82.113%
21506615699

push

github

web-flow
Make symbol table pickle-able (#574)

We introduce ``__getstate__`` and ``__setstate__`` methods to relevant
classes in ``intermediate`` module so that the sybmol table can be
de/serialized with ``pickle`` module. Specifically, we have to
re-compute properties relying on ``id(.)`` calls, since the runtime
identifiers of instances in Python change between different runs.

This accelerates development iterations in downstream code, in particular
aas-core-testdatagen.

109 of 109 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

28660 of 34860 relevant lines covered (82.21%)

3.29 hits per line

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

75.5
/aas_core_codegen/stringify.py
1
"""Represent general entities as strings for testing or debugging."""
2

3
import collections.abc
4✔
4
import enum
4✔
5
import inspect
4✔
6
import io
4✔
7
import re
4✔
8
import textwrap
4✔
9
from typing import Sequence, Union, Any, Mapping, List, Tuple, Set
4✔
10

11
from icontract import require
4✔
12

13
from aas_core_codegen.common import assert_never, indent_but_first_line
4✔
14

15
# We have to separate Stringifiable and Sequence[Stringifiable] since recursive types
16
# are not supported in mypy, see https://github.com/python/mypy/issues/731.
17
PrimitiveStringifiable = Union[
4✔
18
    bool, int, float, str, "Entity", "Property", "PropertyEllipsis", None
19
]
20

21
Stringifiable = Union[
4✔
22
    PrimitiveStringifiable,
23
    Sequence[PrimitiveStringifiable],
24
    Sequence[Sequence[PrimitiveStringifiable]],
25
    Mapping[str, PrimitiveStringifiable],
26
    Mapping[str, Sequence[PrimitiveStringifiable]],
27
]
28

29

30
class Property:
4✔
31
    """Represent a property of an entity to be stringified."""
32

33
    def __init__(self, name: str, value: Stringifiable) -> None:
4✔
34
        self.name = name
4✔
35
        self.value = value
4✔
36

37
    def __repr__(self) -> str:
4✔
38
        return dump(self)
×
39

40

41
class PropertyEllipsis:
4✔
42
    """Represent a property whose value is not displayed."""
43

44
    def __init__(self, name: str, ignored_value: Any) -> None:
4✔
45
        """Initialize with the given values."""
46
        self.name = name
4✔
47

48
        # The ignored value is usually not used. However, it instructs mypy and
49
        # other static checkers to raise a warning if its value can not be accessed
50
        # (*e.g.*, as can happen in a refactoring).
51
        self.ignored_value = ignored_value
4✔
52

53
    def __repr__(self) -> str:
4✔
54
        return dump(self)
×
55

56

57
class Entity:
4✔
58
    """Represent a stringifiable entity which is defined by its properties.
59

60
    Think of a dictionary with assigned type identifier.
61
    """
62

63
    def __init__(
4✔
64
        self, name: str, properties: Sequence[Union[Property, PropertyEllipsis]]
65
    ) -> None:
66
        """Initialize with the given values."""
67
        self.name = name
4✔
68
        self.properties = properties
4✔
69

70
    def __repr__(self) -> str:
4✔
71
        return dump(self)
×
72

73

74
def dump(stringifiable: Stringifiable) -> str:
4✔
75
    """Produce a string representation of ``stringifiable`` for debugging or testing."""
76
    if isinstance(stringifiable, (bool, int, float)):
4✔
77
        return repr(stringifiable)
4✔
78

79
    elif isinstance(stringifiable, str):
4✔
80
        if "\n" not in stringifiable or "\r" in stringifiable or '"""' in stringifiable:
4✔
81
            return repr(stringifiable)
4✔
82

83
        # NOTE (mristin, 2022-05-18):
84
        # A multi-line string literal is much more readable when it comes to diffing.
85

86
        escaped = stringifiable.replace("\\", "\\\\")
4✔
87

88
        indented = "\n".join(f"  {line}" for line in escaped.splitlines())
4✔
89

90
        return f'textwrap.dedent("""\\\n{indented}""")'
4✔
91

92
    elif isinstance(stringifiable, Entity):
4✔
93
        if len(stringifiable.properties) == 0:
4✔
94
            return f"{stringifiable.name}()"
4✔
95

96
        writer = io.StringIO()
4✔
97
        writer.write(f"{stringifiable.name}(\n")
4✔
98

99
        for i, prop in enumerate(stringifiable.properties):
4✔
100
            if isinstance(prop, Property):
4✔
101
                value_str = dump(prop.value)
4✔
102
                indention = "  "
4✔
103
                writer.write(
4✔
104
                    f"  {prop.name}={indent_but_first_line(value_str, indention)}"
105
                )
106
            elif isinstance(prop, PropertyEllipsis):
4✔
107
                value_str = "None" if prop.ignored_value is None else "..."
4✔
108
                writer.write(f"  {prop.name}={value_str}")
4✔
109
            else:
110
                assert_never(prop)
×
111

112
            if i == len(stringifiable.properties) - 1:
4✔
113
                writer.write(")")
4✔
114
            else:
115
                writer.write(",\n")
4✔
116

117
        return writer.getvalue()
4✔
118

119
    elif isinstance(stringifiable, collections.abc.Sequence):
4✔
120
        if len(stringifiable) == 0:
4✔
121
            return "[]"
4✔
122
        else:
123
            writer = io.StringIO()
4✔
124
            writer.write("[\n")
4✔
125
            for i, value in enumerate(stringifiable):
4✔
126
                value_str = dump(value)
4✔
127
                writer.write(textwrap.indent(value_str, "  "))
4✔
128

129
                if i == len(stringifiable) - 1:
4✔
130
                    writer.write("]")
4✔
131
                else:
132
                    writer.write(",\n")
4✔
133

134
            return writer.getvalue()
4✔
135

136
    elif isinstance(stringifiable, collections.abc.Mapping):
4✔
137
        if len(stringifiable) == 0:
4✔
138
            return "{}"
4✔
139
        else:
140
            writer = io.StringIO()
4✔
141
            writer.write("{\n")
4✔
142
            for i, (key, value) in enumerate(stringifiable.items()):
4✔
143
                key_str = dump(key)
4✔
144

145
                value_str = dump(value)
4✔
146
                writer.write(textwrap.indent(f"{key_str}: {value_str}", "  "))
4✔
147

148
                if i == len(stringifiable) - 1:
4✔
149
                    writer.write("}")
4✔
150
                else:
151
                    writer.write(",\n")
×
152

153
            return writer.getvalue()
4✔
154

155
    elif stringifiable is None:
4✔
156
        return repr(None)
4✔
157

158
    elif isinstance(stringifiable, Property):
×
159
        value_str = dump(stringifiable.value)
×
160
        indention = ""
×
161
        return (
×
162
            f"Property("
163
            f"{stringifiable.name}={indent_but_first_line(value_str, indention)}"
164
            f")"
165
        )
166

167
    elif isinstance(stringifiable, PropertyEllipsis):
×
168
        value_str = "None" if stringifiable.ignored_value is None else "..."
×
169
        return f"PropertyEllipsis({stringifiable.name}={value_str})"
×
170

171
    else:
172
        assert_never(stringifiable)
×
173

174
    raise AssertionError("Should not have gotten here")
×
175

176

177
def compares_against_dict(entity: Entity, obj: object) -> bool:
4✔
178
    """
179
    Compare that the properties in the ``entity`` and ``obj.__dict__`` match.
180

181
    Mind that the dunders and "protected" properties are excluded.
182
    """
183
    entity_property_set = {prop.name for prop in entity.properties}
×
184

185
    obj_property_set = {key for key in obj.__dict__.keys() if not key.startswith("_")}
×
186

187
    return entity_property_set == obj_property_set
×
188

189

190
@require(lambda obj: hasattr(obj, "__dict__"), error=ValueError)
4✔
191
def assert_compares_against_dict(entity: Entity, obj: object) -> None:
4✔
192
    """
193
    Compare that the properties in the ``entity`` and ``obj.__dict__`` match.
194

195
    Mind that the dunders and "protected" properties are excluded.
196
    """
197
    entity_property_set = {prop.name for prop in entity.properties}
4✔
198

199
    obj_property_set = {
4✔
200
        attr
201
        for attr in dir(obj)
202
        if not attr.startswith("_") and not inspect.ismethod(getattr(obj, attr))
203
    }
204

205
    if entity_property_set != obj_property_set:
4✔
206
        diff_in_entity = sorted(entity_property_set.difference(obj_property_set))
×
207

208
        diff_in_obj = sorted(obj_property_set.difference(entity_property_set))
×
209

210
        prefix = (
×
211
            f"Expected the stringified properties "
212
            f"of {obj.__class__.__name__!r} to match the object properties, "
213
            f"but they do not.\n\n"
214
        )
215

216
        if len(diff_in_entity) > 0 and len(diff_in_obj) == 0:
×
217
            raise AssertionError(
×
218
                f"{prefix}"
219
                f"The following properties were found in the stringified entity, "
220
                f"but not in the object: {diff_in_entity}"
221
            )
222

223
        elif len(diff_in_obj) > 0 and len(diff_in_entity) == 0:
×
224
            raise AssertionError(
×
225
                f"{prefix}"
226
                f"The following properties were found in the object, "
227
                f"but not in the stringified entity: {diff_in_obj}"
228
            )
229

230
        else:
231
            raise AssertionError(
×
232
                f"{prefix}"
233
                f"The following properties were found in the stringified entity, "
234
                f"but not in the object: {diff_in_entity}\n\n"
235
                f"The following properties were found in the object, "
236
                f"but not in the stringified entity: {diff_in_obj}"
237
            )
238

239

240
def assert_all_public_types_listed_as_dumpables(
4✔
241
    dumpable: Any, types_module: Any
242
) -> None:
243
    """Make sure that all classes in ``types_modules`` are listed as dumpables."""
244

245
    dumpable_set = set()  # type: Set[str]
4✔
246

247
    for dumpable_cls in dumpable.__args__:
4✔
248
        dumpable_set.add(dumpable_cls.__name__)
4✔
249

250
    module_set = set()  # type: Set[str]
4✔
251

252
    for identifier, cls in inspect.getmembers(types_module, inspect.isclass):
4✔
253
        module_name = getattr(cls, "__module__", None)
4✔
254
        if module_name is None:
4✔
255
            continue
×
256

257
        if module_name != types_module.__name__:
4✔
258
            continue
4✔
259

260
        if identifier.startswith("_"):
4✔
UNCOV
261
            continue
×
262

263
        if issubclass(cls, enum.Enum):
4✔
264
            continue
4✔
265

266
        if inspect.isabstract(cls):
4✔
267
            continue
4✔
268

269
        if cls.__module__ == types_module.__name__:
4✔
270
            module_set.add(identifier)
4✔
271

272
    if dumpable_set != module_set:
4✔
273
        dumpable_diff = dumpable_set.difference(module_set)
×
274
        module_diff = module_set.difference(dumpable_set)
×
275

276
        raise AssertionError(
×
277
            f"The following classes were defined as dumpable, "
278
            f"but not found as concrete classes "
279
            f"in the module ``_types``: {sorted(dumpable_diff)}\n\n"
280
            f"The following classes were defined in the module ``_types``, "
281
            f"but not found in dumpables: {sorted(module_diff)}"
282
        )
283

284

285
def assert_dispatch_exhaustive(dispatch: Mapping[Any, Any], dumpable: Any) -> None:
4✔
286
    """
287
    Make sure that ``dispatch_map`` is exhaustive over all the concrete dumpables.
288

289
    We need to dispatch a class to its corresponding ``_stringify_*`` function. This is
290
    mapped in ``dispatch_map``. At the same time, we have to make a union type over all
291
    the types that can be converted to a stringified entity.
292
    """
293
    dumpable_map = {
4✔
294
        id(cls): cls for cls in dumpable.__args__ if not inspect.isabstract(cls)
295
    }
296

297
    dispatch_map = {id(cls): cls for cls in dispatch}
4✔
298

299
    dumpable_set = set(dumpable_map.keys())
4✔
300
    dispatch_set = set(dispatch_map.keys())
4✔
301

302
    if dumpable_set != dispatch_set:
4✔
303
        dumpable_diff = dumpable_set.difference(dispatch_set)
×
304
        dispatch_diff = dispatch_set.difference(dumpable_set)
×
305

306
        dumpable_diff_names = [
×
307
            dumpable_map[cls_id].__name__ for cls_id in dumpable_diff
308
        ]
309

310
        dispatch_diff_names = [
×
311
            dispatch_map[cls_id].__name__ for cls_id in dispatch_diff
312
        ]
313

314
        raise AssertionError(
×
315
            f"The following concrete classes are found in Dumpable, "
316
            f"but not in _DISPATCH: {dumpable_diff_names}.\n\n"
317
            f"The following concrete classes are found in _DISPATCH, "
318
            f"but not in Dumpable: {dispatch_diff_names}"
319
        )
320

321
    unexpected_function_names = []  # type: List[Tuple[str, str]]
4✔
322
    for cls, func in dispatch.items():
4✔
323
        cls_snake_case = re.sub(r"(?<!^)(?=[A-Z])", "_", cls.__name__).lower()
4✔
324
        expected_func_name = f"_stringify_{cls_snake_case}"
4✔
325

326
        if func.__name__ != expected_func_name:
4✔
327
            unexpected_function_names.append((cls.__name__, func.__name__))
×
328

329
    if len(unexpected_function_names):
4✔
330
        raise AssertionError(
×
331
            f"The following dispatch functions had unexpected names "
332
            f"(as a list of (class, function name)): {unexpected_function_names}"
333
        )
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

© 2026 Coveralls, Inc