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

aas-core-works / aas-core-codegen / 17360321615

31 Aug 2025 05:35PM UTC coverage: 82.003%. First build
17360321615

Pull #547

github

web-flow
Merge deb36441d into d626e3e00
Pull Request #547: Handle list of primitives in JSON Schema, Python

5 of 12 new or added lines in 1 file covered. (41.67%)

28509 of 34766 relevant lines covered (82.0%)

4.1 hits per line

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

93.25
/aas_core_codegen/python/xmlization/_generate.py
1
"""Generate Python code for XML-ization based on the intermediate representation."""
2

3
import io
5✔
4
import textwrap
5✔
5
from typing import Tuple, Optional, List, Union
5✔
6

7
from icontract import ensure, require
5✔
8

9
from aas_core_codegen import intermediate, specific_implementations, naming
5✔
10
from aas_core_codegen.common import (
5✔
11
    Error,
12
    Stripped,
13
    assert_never,
14
    Identifier,
15
    indent_but_first_line,
16
)
17
from aas_core_codegen.python import common as python_common, naming as python_naming
5✔
18
from aas_core_codegen.python.common import (
5✔
19
    INDENT as I,
20
    INDENT2 as II,
21
    INDENT3 as III,
22
    INDENT4 as IIII,
23
)
24

25

26
def _generate_module_docstring(
5✔
27
    symbol_table: intermediate.SymbolTable,
28
    aas_module: python_common.QualifiedModuleName,
29
) -> Stripped:
30
    """Generate the docstring of the whole module."""
31
    first_cls = (
5✔
32
        symbol_table.concrete_classes[0]
33
        if len(symbol_table.concrete_classes) > 0
34
        else None
35
    )
36

37
    docstring_blocks = [
5✔
38
        Stripped(
39
            f"""\
40
Read and write AAS models as XML.
41

42
For reading, we provide different reading functions, each handling a different kind
43
of input. All the reading functions operate in one pass, *i.e.*, the source is read
44
incrementally and the complete XML is not held in memory.
45

46
We provide the following four reading functions (where ``X`` represents the name of
47
the class):
48

49
1) ``X_from_iterparse`` reads from a stream of ``(event, element)`` tuples coming from
50
   :py:func:`xml.etree.ElementTree.iterparse` with the argument
51
   ``events=["start", "end"]``. If you do not trust the source, please consider
52
   using `defusedxml.ElementTree`_.
53
2) ``X_from_stream`` reads from the given text stream.
54
3) ``X_from_file`` reads from a file on disk.
55
4) ``X_from_str`` reads from the given string.
56

57
The functions ``X_from_stream``, ``X_from_file`` and ``X_from_str`` provide
58
an extra parameter, ``has_iterparse``, which allows you to use a parsing library
59
different from :py:mod:`xml.etree.ElementTree`. For example, you can pass in
60
`defusedxml.ElementTree`_.
61

62
.. _defusedxml.ElementTree: https://pypi.org/project/defusedxml/#defusedxml-elementtree
63

64
All XML elements are expected to live in the :py:attr:`~NAMESPACE`.
65

66
For writing, use the function :py:func:`{aas_module}.xmlization.write` which
67
translates the instance of the model into an XML document and writes it in one pass
68
to the stream."""
69
        )
70
    ]
71

72
    if first_cls is not None:
5✔
73
        read_first_cls_from_file = python_naming.function_name(
5✔
74
            Identifier(f"read_{first_cls.name}_from_file")
75
        )
76

77
        first_cls_name = python_naming.class_name(first_cls.name)
5✔
78

79
        docstring_blocks.append(
1✔
80
            Stripped(
81
                f"""\
82
Here is an example usage how to de-serialize from a file:
83

84
.. code-block::
85

86
    import pathlib
87
    import xml.etree.ElementTree as ET
88

89
    import {aas_module}.xmlization as aas_xmlization
90

91
    path = pathlib.Path(...)
92
    instance = aas_xmlization.{read_first_cls_from_file}(
93
        path
94
    )
95

96
    # Do something with the ``instance``
97

98
Here is another code example where we serialize the instance:
99

100
.. code-block::
101

102
    import pathlib
103

104
    import {aas_module}.types as aas_types
105
    import {aas_module}.xmlization as aas_xmlization
106

107
    instance = {first_cls_name}(
108
       ... # some constructor arguments
109
    )
110

111
    pth = pathlib.Path(...)
112
    with pth.open("wt") as fid:
113
        aas_xmlization.write(instance, fid)"""
114
            )
115
        )
116

117
    escaped_text = "\n\n".join(docstring_blocks).replace('"""', '\\"\\"\\"')
5✔
118
    return Stripped(
5✔
119
        f"""\
120
\"\"\"
121
{escaped_text}
122
\"\"\""""
123
    )
124

125

126
def _generate_read_enum_from_element_text(
5✔
127
    enumeration: intermediate.Enumeration,
128
) -> Stripped:
129
    """Generate the reading function from an element's text for ``enumeration``."""
130
    enum_name = python_naming.enum_name(identifier=enumeration.name)
5✔
131

132
    function_name = python_naming.private_function_name(
5✔
133
        Identifier(f"read_{enumeration.name}_from_element_text")
134
    )
135

136
    enum_from_str = python_naming.function_name(
5✔
137
        Identifier(f"{enumeration.name}_from_str")
138
    )
139

140
    return Stripped(
5✔
141
        f"""\
142
def {function_name}(
143
{I}element: Element,
144
{I}iterator: Iterator[Tuple[str, Element]]
145
) -> aas_types.{enum_name}:
146
{I}\"\"\"
147
{I}Parse the text of :paramref:`element` as a literal of
148
{I}:py:class:`.types.{enum_name}`, and read the corresponding
149
{I}end element from :paramref:`iterator`.
150

151
{I}:param element: start element
152
{I}:param iterator:
153
{II}Input stream of ``(event, element)`` coming from
154
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
155
{II}``events=["start", "end"]``
156
{I}:raise: :py:class:`DeserializationException` if unexpected input
157
{I}:return: parsed value
158
{I}\"\"\"
159
{I}text = _read_text_from_element(
160
{II}element,
161
{II}iterator
162
{I})
163

164
{I}literal = aas_stringification.{enum_from_str}(text)
165
{I}if literal is None:
166
{II}raise DeserializationException(
167
{III}f"Not a valid string representation of "
168
{III}f"a literal of {enum_name}: {{text}}"
169
{II})
170

171
{I}return literal"""
172
    )
173

174

175
def _generate_read_cls_from_iterparse(
5✔
176
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass],
177
    aas_module: python_common.QualifiedModuleName,
178
) -> Stripped:
179
    """Generate the public function for the reading for a ``cls``."""
180
    function_name = python_naming.function_name(
5✔
181
        Identifier(f"{cls.name}_from_iterparse")
182
    )
183

184
    cls_name = python_naming.class_name(cls.name)
5✔
185

186
    wrapped_function_name = python_naming.function_name(
5✔
187
        Identifier(f"_read_{cls.name}_as_element")
188
    )
189

190
    return Stripped(
5✔
191
        f"""\
192
def {function_name}(
193
{I}iterator: Iterator[Tuple[str, Element]]
194
) -> aas_types.{cls_name}:
195
{I}\"\"\"
196
{I}Read an instance of :py:class:`.types.{cls_name}` from
197
{I}the :paramref:`iterator`.
198

199
{I}Example usage:
200

201
{I}.. code-block::
202

203
{I}    import pathlib
204
{I}    import xml.etree.ElementTree as ET
205

206
{I}    import {aas_module}.xmlization as aas_xmlization
207

208
{I}    path = pathlib.Path(...)
209
{I}    with path.open("rt") as fid:
210
{I}        iterator = ET.iterparse(
211
{I}            source=fid,
212
{I}            events=['start', 'end']
213
{I}        )
214
{I}        instance = aas_xmlization.{function_name}(
215
{I}            iterator
216
{I}        )
217

218
{I}    # Do something with the ``instance``
219

220
{I}:param iterator:
221
{II}Input stream of ``(event, element)`` coming from
222
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
223
{II}``events=["start", "end"]``
224
{I}:raise: :py:class:`DeserializationException` if unexpected input
225
{I}:return:
226
{II}Instance of :py:class:`.types.{cls_name}` read from
227
{II}:paramref:`iterator`
228
{I}\"\"\"
229
{I}next_event_element = next(iterator, None)
230
{I}if next_event_element is None:
231
{II}raise DeserializationException(
232
{III}# fmt: off
233
{III}"Expected the start element for {cls_name}, "
234
{III}"but got the end-of-input"
235
{III}# fmt: on
236
{II})
237

238
{I}next_event, next_element = next_event_element
239
{I}if next_event != 'start':
240
{II}raise DeserializationException(
241
{III}f"Expected the start element for {cls_name}, "
242
{III}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
243
{II})
244

245
{I}try:
246
{II}return {wrapped_function_name}(
247
{III}next_element,
248
{III}iterator
249
{II})
250
{I}except DeserializationException as exception:
251
{II}exception.path._prepend(ElementSegment(next_element))
252
{II}raise exception"""
253
    )
254

255

256
def _generate_read_cls_from_stream(
5✔
257
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass],
258
    aas_module: python_common.QualifiedModuleName,
259
) -> Stripped:
260
    """Generate the public function for the reading of a ``cls`` from a stream."""
261
    function_name = python_naming.function_name(Identifier(f"{cls.name}_from_stream"))
5✔
262

263
    from_iterparse_name = python_naming.function_name(
5✔
264
        Identifier(f"{cls.name}_from_iterparse")
265
    )
266

267
    cls_name = python_naming.class_name(cls.name)
5✔
268

269
    return Stripped(
5✔
270
        f"""\
271
def {function_name}(
272
{I}stream: TextIO,
273
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
274
) -> aas_types.{cls_name}:
275
{I}\"\"\"
276
{I}Read an instance of :py:class:`.types.{cls_name}` from
277
{I}the :paramref:`stream`.
278

279
{I}Example usage:
280

281
{I}.. code-block::
282

283
{I}    import {aas_module}.xmlization as aas_xmlization
284

285
{I}    with open_some_stream_over_network(...) as stream:
286
{I}        instance = aas_xmlization.{function_name}(
287
{I}            stream
288
{I}        )
289

290
{I}    # Do something with the ``instance``
291

292
{I}:param stream:
293
{II}representing an instance of
294
{II}:py:class:`.types.{cls_name}` in XML
295
{I}:param has_iterparse:
296
{II}Module containing ``iterparse`` function.
297

298
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
299
{II}library. If you have to deal with malicious input, consider using
300
{II}a library such as `defusedxml.ElementTree`_.
301
{I}:raise: :py:class:`DeserializationException` if unexpected input
302
{I}:return:
303
{II}Instance of :py:class:`.types.{cls_name}` read from
304
{II}:paramref:`stream`
305
{I}\"\"\"
306
{I}iterator = has_iterparse.iterparse(
307
{II}stream,
308
{II}['start', 'end']
309
{I})
310
{I}return {from_iterparse_name}(
311
{II}_with_elements_cleared_after_yield(iterator)
312
{I})"""
313
    )
314

315

316
def _generate_read_cls_from_file(
5✔
317
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass],
318
    aas_module: python_common.QualifiedModuleName,
319
) -> Stripped:
320
    """Generate the public function for the reading of a ``cls`` from a file."""
321
    function_name = python_naming.function_name(Identifier(f"{cls.name}_from_file"))
5✔
322

323
    from_iterparse_name = python_naming.function_name(
5✔
324
        Identifier(f"{cls.name}_from_iterparse")
325
    )
326

327
    cls_name = python_naming.class_name(cls.name)
5✔
328

329
    return Stripped(
5✔
330
        f"""\
331
def {function_name}(
332
{I}path: PathLike,
333
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
334
) -> aas_types.{cls_name}:
335
{I}\"\"\"
336
{I}Read an instance of :py:class:`.types.{cls_name}` from
337
{I}the :paramref:`path`.
338

339
{I}Example usage:
340

341
{I}.. code-block::
342

343
{I}    import pathlib
344
{I}    import {aas_module}.xmlization as aas_xmlization
345

346
{I}    path = pathlib.Path(...)
347
{I}    instance = aas_xmlization.{function_name}(
348
{I}        path
349
{I}    )
350

351
{I}    # Do something with the ``instance``
352

353
{I}:param path:
354
{II}to the file representing an instance of
355
{II}:py:class:`.types.{cls_name}` in XML
356
{I}:param has_iterparse:
357
{II}Module containing ``iterparse`` function.
358

359
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
360
{II}library. If you have to deal with malicious input, consider using
361
{II}a library such as `defusedxml.ElementTree`_.
362
{I}:raise: :py:class:`DeserializationException` if unexpected input
363
{I}:return:
364
{II}Instance of :py:class:`.types.{cls_name}` read from
365
{II}:paramref:`path`
366
{I}\"\"\"
367
{I}with open(os.fspath(path), "rt", encoding='utf-8') as fid:
368
{II}iterator = has_iterparse.iterparse(
369
{III}fid,
370
{III}['start', 'end']
371
{II})
372
{II}return {from_iterparse_name}(
373
{III}_with_elements_cleared_after_yield(iterator)
374
{II})"""
375
    )
376

377

378
def _generate_read_cls_from_str(
5✔
379
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass],
380
    aas_module: python_common.QualifiedModuleName,
381
) -> Stripped:
382
    """Generate the public function for the reading of a ``cls`` from a string."""
383
    function_name = python_naming.function_name(Identifier(f"{cls.name}_from_str"))
5✔
384

385
    from_iterparse_name = python_naming.function_name(
5✔
386
        Identifier(f"{cls.name}_from_iterparse")
387
    )
388

389
    cls_name = python_naming.class_name(cls.name)
5✔
390

391
    return Stripped(
5✔
392
        f"""\
393
def {function_name}(
394
{I}text: str,
395
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
396
) -> aas_types.{cls_name}:
397
{I}\"\"\"
398
{I}Read an instance of :py:class:`.types.{cls_name}` from
399
{I}the :paramref:`text`.
400

401
{I}Example usage:
402

403
{I}.. code-block::
404

405
{I}    import pathlib
406
{I}    import {aas_module}.xmlization as aas_xmlization
407

408
{I}    text = "<...>...</...>"
409
{I}    instance = aas_xmlization.{function_name}(
410
{I}        text
411
{I}    )
412

413
{I}    # Do something with the ``instance``
414

415
{I}:param text:
416
{II}representing an instance of
417
{II}:py:class:`.types.{cls_name}` in XML
418
{I}:param has_iterparse:
419
{II}Module containing ``iterparse`` function.
420

421
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
422
{II}library. If you have to deal with malicious input, consider using
423
{II}a library such as `defusedxml.ElementTree`_.
424
{I}:raise: :py:class:`DeserializationException` if unexpected input
425
{I}:return:
426
{II}Instance of :py:class:`.types.{cls_name}` read from
427
{II}:paramref:`text`
428
{I}\"\"\"
429
{I}iterator = has_iterparse.iterparse(
430
{II}io.StringIO(text),
431
{II}['start', 'end']
432
{I})
433
{I}return {from_iterparse_name}(
434
{II}_with_elements_cleared_after_yield(iterator)
435
{I})"""
436
    )
437

438

439
# fmt: off
440
@require(
5✔
441
    lambda cls:
442
    not isinstance(cls, intermediate.AbstractClass)
443
    or len(cls.concrete_descendants) > 0,
444
    "All abstract classes must have concrete descendants; otherwise we can not dispatch"
445
)
446
# fmt: on
447
def _generate_read_cls_as_element(
5✔
448
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass]
449
) -> Stripped:
450
    """Generate the read function to dispatch or read a concrete instance of ``cls``."""
451

452
    if len(cls.concrete_descendants) > 0:
5✔
453
        dispatch_map = python_naming.private_constant_name(
5✔
454
            Identifier(f"dispatch_for_{cls.name}")
455
        )
456

457
        cls_name = python_naming.class_name(cls.name)
5✔
458

459
        body = Stripped(
5✔
460
            f"""\
461
tag_wo_ns = _parse_element_tag(element)
462
read_as_sequence = {dispatch_map}.get(
463
{I}tag_wo_ns,
464
{I}None
465
)
466

467
if read_as_sequence is None:
468
{I}raise DeserializationException(
469
{II}f"Expected the element tag to be a valid model type "
470
{II}f"of a concrete instance of '{cls_name}', "
471
{II}f"but got tag {{tag_wo_ns!r}}"
472
{I})
473

474
return read_as_sequence(
475
{I}element,
476
{I}iterator
477
)"""
478
        )
479
    else:
480
        xml_cls = naming.xml_class_name(cls.name)
5✔
481
        xml_cls_literal = python_common.string_literal(xml_cls)
5✔
482

483
        read_as_sequence_function_name = python_naming.function_name(
5✔
484
            Identifier(f"_read_{cls.name}_as_sequence")
485
        )
486

487
        body = Stripped(
5✔
488
            f"""\
489
tag_wo_ns = _parse_element_tag(element)
490

491
if tag_wo_ns != {xml_cls_literal}:
492
{I}raise DeserializationException(
493
{II}f"Expected the element with the tag '{xml_cls}', "
494
{II}f"but got tag: {{tag_wo_ns}}"
495
{I})
496

497
return {read_as_sequence_function_name}(
498
{I}element,
499
{I}iterator
500
)"""
501
        )
502

503
    function_name = python_naming.function_name(
5✔
504
        Identifier(f"_read_{cls.name}_as_element")
505
    )
506

507
    cls_name = python_naming.class_name(cls.name)
5✔
508

509
    return Stripped(
5✔
510
        f"""\
511
def {function_name}(
512
{I}element: Element,
513
{I}iterator: Iterator[Tuple[str, Element]]
514
) -> aas_types.{cls_name}:
515
{I}\"\"\"
516
{I}Read an instance of :py:class:`.types.{cls_name}` from
517
{I}:paramref:`iterator`, including the end element.
518

519
{I}:param element: start element
520
{I}:param iterator:
521
{II}Input stream of ``(event, element)`` coming from
522
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
523
{II}``events=["start", "end"]``
524
{I}:raise: :py:class:`DeserializationException` if unexpected input
525
{I}:return: parsed instance
526
{I}\"\"\"
527
{I}{indent_but_first_line(body, I)}"""
528
    )
529

530

531
def _generate_read_from_iterparse(
5✔
532
    aas_module: python_common.QualifiedModuleName,
533
) -> Stripped:
534
    """Generate the general read function to parse an instance from iterparse."""
535
    function_name = "from_iterparse"
5✔
536

537
    return Stripped(
5✔
538
        f"""\
539
def {function_name}(
540
{I}iterator: Iterator[Tuple[str, Element]]
541
) -> aas_types.Class:
542
{I}\"\"\"
543
{I}Read an instance from the :paramref:`iterator`.
544

545
{I}The type of the instance is determined by the very first start element.
546

547
{I}Example usage:
548

549
{I}.. code-block::
550

551
{I}    import pathlib
552
{I}    import xml.etree.ElementTree as ET
553

554
{I}    import {aas_module}.xmlization as aas_xmlization
555

556
{I}    path = pathlib.Path(...)
557
{I}    with path.open("rt") as fid:
558
{I}        iterator = ET.iterparse(
559
{I}            source=fid,
560
{I}            events=['start', 'end']
561
{I}        )
562
{I}        instance = aas_xmlization.{function_name}(
563
{I}            iterator
564
{I}        )
565

566
{I}    # Do something with the ``instance``
567

568
{I}:param iterator:
569
{II}Input stream of ``(event, element)`` coming from
570
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
571
{II}``events=["start", "end"]``
572
{I}:raise: :py:class:`DeserializationException` if unexpected input
573
{I}:return:
574
{II}Instance of :py:class:`.types.Class` read from the :paramref:`iterator`
575
{I}\"\"\"
576
{I}next_event_element = next(iterator, None)
577
{I}if next_event_element is None:
578
{II}raise DeserializationException(
579
{III}# fmt: off
580
{III}"Expected the start element of an instance, "
581
{III}"but got the end-of-input"
582
{III}# fmt: on
583
{II})
584

585
{I}next_event, next_element = next_event_element
586
{I}if next_event != 'start':
587
{II}raise DeserializationException(
588
{III}f"Expected the start element of an instance, "
589
{III}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
590
{II})
591

592
{I}try:
593
{II}return _read_as_element(
594
{III}next_element,
595
{III}iterator
596
{II})
597
{I}except DeserializationException as exception:
598
{II}exception.path._prepend(ElementSegment(next_element))
599
{II}raise exception"""
600
    )
601

602

603
def _generate_read_from_stream(
5✔
604
    aas_module: python_common.QualifiedModuleName,
605
) -> Stripped:
606
    """Generate the general read function to parse an instance from a text stream."""
607
    function_name = python_naming.function_name(Identifier("from_stream"))
5✔
608

609
    return Stripped(
5✔
610
        f"""\
611
def {function_name}(
612
{I}stream: TextIO,
613
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
614
) -> aas_types.Class:
615
{I}\"\"\"
616
{I}Read an instance from the :paramref:`stream`.
617

618
{I}The type of the instance is determined by the very first start element.
619

620
{I}Example usage:
621

622
{I}.. code-block::
623

624
{I}    import {aas_module}.xmlization as aas_xmlization
625

626
{I}    with open_some_stream_over_network(...) as stream:
627
{I}        instance = aas_xmlization.{function_name}(
628
{I}            stream
629
{I}        )
630

631
{I}    # Do something with the ``instance``
632

633
{I}:param stream:
634
{II}representing an instance in XML
635
{I}:param has_iterparse:
636
{II}Module containing ``iterparse`` function.
637

638
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
639
{II}library. If you have to deal with malicious input, consider using
640
{II}a library such as `defusedxml.ElementTree`_.
641
{I}:raise: :py:class:`DeserializationException` if unexpected input
642
{I}:return:
643
{II}Instance read from :paramref:`stream`
644
{I}\"\"\"
645
{I}iterator = has_iterparse.iterparse(
646
{II}stream,
647
{II}['start', 'end']
648
{I})
649
{I}return from_iterparse(
650
{II}_with_elements_cleared_after_yield(iterator)
651
{I})"""
652
    )
653

654

655
def _generate_read_from_file(aas_module: python_common.QualifiedModuleName) -> Stripped:
5✔
656
    """Generate the general read function to parse an instance from a file."""
657
    function_name = python_naming.function_name(Identifier("from_file"))
5✔
658

659
    return Stripped(
5✔
660
        f"""\
661
def {function_name}(
662
{I}path: PathLike,
663
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
664
) -> aas_types.Class:
665
{I}\"\"\"
666
{I}Read an instance from the file at the :paramref:`path`.
667

668
{I}Example usage:
669

670
{I}.. code-block::
671

672
{I}    import pathlib
673
{I}    import {aas_module}.xmlization as aas_xmlization
674

675
{I}    path = pathlib.Path(...)
676
{I}    instance = aas_xmlization.{function_name}(
677
{I}        path
678
{I}    )
679

680
{I}    # Do something with the ``instance``
681

682
{I}:param path:
683
{II}to the file representing an instance in XML
684
{I}:param has_iterparse:
685
{II}Module containing ``iterparse`` function.
686

687
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
688
{II}library. If you have to deal with malicious input, consider using
689
{II}a library such as `defusedxml.ElementTree`_.
690
{I}:raise: :py:class:`DeserializationException` if unexpected input
691
{I}:return:
692
{II}Instance read from the file at :paramref:`path`
693
{I}\"\"\"
694
{I}with open(os.fspath(path), "rt", encoding='utf-8') as fid:
695
{II}iterator = has_iterparse.iterparse(
696
{III}fid,
697
{III}['start', 'end']
698
{II})
699
{II}return from_iterparse(
700
{III}_with_elements_cleared_after_yield(iterator)
701
{II})"""
702
    )
703

704

705
def _generate_read_from_str(aas_module: python_common.QualifiedModuleName) -> Stripped:
5✔
706
    """Generate the general read function to parse an instance from a string."""
707
    function_name = python_naming.function_name(Identifier("from_str"))
5✔
708

709
    return Stripped(
5✔
710
        f"""\
711
def {function_name}(
712
{I}text: str,
713
{I}has_iterparse: HasIterparse = xml.etree.ElementTree
714
) -> aas_types.Class:
715
{I}\"\"\"
716
{I}Read an instance from the :paramref:`text`.
717

718
{I}Example usage:
719

720
{I}.. code-block::
721

722
{I}    import pathlib
723
{I}    import {aas_module}.xmlization as aas_xmlization
724

725
{I}    text = "<...>...</...>"
726
{I}    instance = aas_xmlization.{function_name}(
727
{I}        text
728
{I}    )
729

730
{I}    # Do something with the ``instance``
731

732
{I}:param text:
733
{II}representing an instance in XML
734
{I}:param has_iterparse:
735
{II}Module containing ``iterparse`` function.
736

737
{II}Default is to use :py:mod:`xml.etree.ElementTree` from the standard
738
{II}library. If you have to deal with malicious input, consider using
739
{II}a library such as `defusedxml.ElementTree`_.
740
{I}:raise: :py:class:`DeserializationException` if unexpected input
741
{I}:return:
742
{II}Instance read from :paramref:`text`
743
{I}\"\"\"
744
{I}iterator = has_iterparse.iterparse(
745
{II}io.StringIO(text),
746
{II}['start', 'end']
747
{I})
748
{I}return from_iterparse(
749
{II}_with_elements_cleared_after_yield(iterator)
750
{I})"""
751
    )
752

753

754
def _generate_general_read_as_element(
5✔
755
    symbol_table: intermediate.SymbolTable,
756
) -> Stripped:
757
    """Generate the general read function to dispatch on concrete classes."""
758
    dispatch_map = python_naming.private_constant_name(Identifier("general_dispatch"))
5✔
759

760
    body = Stripped(
5✔
761
        f"""\
762
tag_wo_ns = _parse_element_tag(element)
763
read_as_sequence = {dispatch_map}.get(
764
{I}tag_wo_ns,
765
{I}None
766
)
767

768
if read_as_sequence is None:
769
{I}raise DeserializationException(
770
{II}f"Expected the element tag to be a valid model type "
771
{II}f"of a concrete instance, "
772
{II}f"but got tag {{tag_wo_ns!r}}"
773
{I})
774

775
return read_as_sequence(
776
{I}element,
777
{I}iterator
778
)"""
779
    )
780

781
    return Stripped(
5✔
782
        f"""\
783
def _read_as_element(
784
{I}element: Element,
785
{I}iterator: Iterator[Tuple[str, Element]]
786
) -> aas_types.Class:
787
{I}\"\"\"
788
{I}Read an instance from :paramref:`iterator`, including the end element.
789

790
{I}:param element: start element
791
{I}:param iterator:
792
{II}Input stream of ``(event, element)`` coming from
793
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
794
{II}``events=["start", "end"]``
795
{I}:raise: :py:class:`DeserializationException` if unexpected input
796
{I}:return: parsed instance
797
{I}\"\"\"
798
{I}{indent_but_first_line(body, I)}"""
799
    )
800

801

802
_READ_FUNCTION_BY_PRIMITIVE_TYPE = {
5✔
803
    intermediate.PrimitiveType.BOOL: "_read_bool_from_element_text",
804
    intermediate.PrimitiveType.INT: "_read_int_from_element_text",
805
    intermediate.PrimitiveType.FLOAT: "_read_float_from_element_text",
806
    intermediate.PrimitiveType.STR: "_read_str_from_element_text",
807
    intermediate.PrimitiveType.BYTEARRAY: "_read_bytes_from_element_text",
808
}
809
assert all(
5✔
810
    literal in _READ_FUNCTION_BY_PRIMITIVE_TYPE
811
    for literal in intermediate.PrimitiveType
812
)
813

814

815
def _generate_reader_and_setter(cls: intermediate.ConcreteClass) -> Stripped:
5✔
816
    """Generate the ``ReaderAndSetterFor{cls}``."""
817
    methods = []  # type: List[Stripped]
5✔
818

819
    cls_name = python_naming.class_name(cls.name)
5✔
820

821
    init_writer = io.StringIO()
5✔
822
    for i, prop in enumerate(cls.properties):
5✔
823
        prop_name = python_naming.property_name(prop.name)
5✔
824
        prop_type = python_common.generate_type(
5✔
825
            prop.type_annotation, types_module=Identifier("aas_types")
826
        )
827

828
        # NOTE (mristin, 2022-07-22):
829
        # We make all the properties optional since we switch over the properties
830
        # during the de-serialization.
831
        if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
5✔
832
            prop_type = Stripped(f"Optional[{prop_type}]")
5✔
833

834
        if i > 0:
5✔
835
            init_writer.write("\n")
5✔
836
        init_writer.write(f"self.{prop_name}: {prop_type} = None")
5✔
837

838
    methods.append(
5✔
839
        Stripped(
840
            f"""\
841
def __init__(self) -> None:
842
{I}\"\"\"Initialize with all the properties unset.\"\"\"
843
{I}{indent_but_first_line(init_writer.getvalue(), I)}"""
844
        )
845
    )
846

847
    for prop in cls.properties:
5✔
848
        type_anno = intermediate.beneath_optional(prop.type_annotation)
5✔
849

850
        prop_name = python_naming.property_name(prop.name)
5✔
851

852
        method_body: Stripped
853

854
        if isinstance(type_anno, intermediate.PrimitiveTypeAnnotation) or (
5✔
855
            isinstance(type_anno, intermediate.OurTypeAnnotation)
856
            and isinstance(type_anno.our_type, intermediate.ConstrainedPrimitive)
857
        ):
858
            primitive_type = intermediate.try_primitive_type(type_anno)
5✔
859
            assert primitive_type is not None
5✔
860

861
            read_function = _READ_FUNCTION_BY_PRIMITIVE_TYPE[primitive_type]
5✔
862

863
            method_body = Stripped(
5✔
864
                f"""\
865
self.{prop_name} = {read_function}(
866
{I}element,
867
{I}iterator
868
)"""
869
            )
870

871
        elif isinstance(type_anno, intermediate.OurTypeAnnotation):
5✔
872
            our_type = type_anno.our_type
5✔
873
            if isinstance(our_type, intermediate.Enumeration):
5✔
874
                read_function = python_naming.private_function_name(
5✔
875
                    Identifier(f"read_{our_type.name}_from_element_text")
876
                )
877

878
                method_body = Stripped(
5✔
879
                    f"""\
880
self.{prop_name} = {read_function}(
881
{I}element,
882
{I}iterator
883
)"""
884
                )
885

886
            elif isinstance(our_type, intermediate.ConstrainedPrimitive):
5✔
887
                raise AssertionError(
×
888
                    f"Expected {intermediate.ConstrainedPrimitive.__name__} "
889
                    f"to have been handled before"
890
                )
891

892
            elif isinstance(
5✔
893
                our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
894
            ):
895
                prop_cls_name = python_naming.class_name(our_type.name)
5✔
896

897
                if len(our_type.concrete_descendants) > 0:
5✔
898
                    read_prop_cls_as_element = python_naming.function_name(
5✔
899
                        Identifier(f"_read_{our_type.name}_as_element")
900
                    )
901

902
                    method_body = Stripped(
5✔
903
                        f"""\
904
next_event_element = next(iterator, None)
905
if next_event_element is None:
906
{I}raise DeserializationException(
907
{II}"Expected a discriminator start element corresponding "
908
{II}"to {prop_cls_name}, but got end-of-input"
909
{I})
910

911
next_event, next_element = next_event_element
912
if next_event != 'start':
913
{I}raise DeserializationException(
914
{II}f"Expected a discriminator start element corresponding "
915
{II}f"to {prop_cls_name}, "
916
{II}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
917
{I})
918

919
try:
920
{I}result = {read_prop_cls_as_element}(
921
{II}next_element,
922
{II}iterator
923
{I})
924
except DeserializationException as exception:
925
{I}exception.path._prepend(ElementSegment(next_element))
926
{I}raise
927

928
_read_end_element(element, iterator)
929

930
self.{prop_name} = result"""
931
                    )
932
                else:
933
                    read_prop_cls_as_sequence = python_naming.function_name(
5✔
934
                        Identifier(f"_read_{our_type.name}_as_sequence")
935
                    )
936

937
                    method_body = Stripped(
5✔
938
                        f"""\
939
self.{prop_name} = {read_prop_cls_as_sequence}(
940
{I}element,
941
{I}iterator
942
)"""
943
                    )
944

945
        elif isinstance(type_anno, intermediate.ListTypeAnnotation):
5✔
946
            if isinstance(
5✔
947
                type_anno.items, intermediate.OurTypeAnnotation
948
            ) and isinstance(
949
                type_anno.items.our_type,
950
                (intermediate.AbstractClass, intermediate.ConcreteClass),
951
            ):
952
                read_item_cls_as_element = python_naming.function_name(
5✔
953
                    Identifier(f"_read_{type_anno.items.our_type.name}_as_element")
954
                )
955

956
                items_type = python_common.generate_type(
5✔
957
                    type_anno.items, types_module=Identifier("aas_types")
958
                )
NEW
959
            elif isinstance(type_anno.items, intermediate.PrimitiveTypeAnnotation):
×
NEW
960
                read_item_cls_as_element = Identifier(
×
961
                    _READ_FUNCTION_BY_PRIMITIVE_TYPE[type_anno.items.a_type]
962
                )
NEW
963
                items_type = Stripped(str(type_anno.items.a_type))
×
964
            else:
965
                raise AssertionError(
×
966
                    "(mristin, 2022-10-09) We handle only lists of classes and primitive types"
967
                    "in the XML de-serialization at the moment. The meta-model does not contain "
968
                    "any other lists, so we wanted to keep the code as simple as "
969
                    "possible, and avoid unrolling. Please contact the developers "
970
                    "if you need this feature."
971
                )
972

973
            method_body = Stripped(
5✔
974
                f"""\
975
if element.text is not None and len(element.text.strip()) != 0:
976
{I}raise DeserializationException(
977
{II}f"Expected only item elements and whitespace text, "
978
{II}f"but got text: {{element.text!r}}"
979
{I})
980

981
result: List[
982
{I}{items_type}
983
] = []
984

985
item_i = 0
986

987
while True:
988
{I}next_event_element = next(iterator, None)
989
{I}if next_event_element is None:
990
{II}raise DeserializationException(
991
{III}"Expected one or more items from a list or the end element, "
992
{III}"but got end-of-input"
993
{II})
994

995
{I}next_event, next_element = next_event_element
996
{I}if next_event == 'end' and next_element.tag == element.tag:
997
{II}# We reached the end of the list.
998
{II}break
999

1000
{I}if next_event != 'start':
1001
{II}raise DeserializationException(
1002
{III}"Expected a start element corresponding to an item, "
1003
{III}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
1004
{II})
1005

1006
{I}try:
1007
{II}item = {read_item_cls_as_element}(
1008
{III}next_element,
1009
{III}iterator
1010
{II})
1011
{I}except DeserializationException as exception:
1012
{II}exception.path._prepend(IndexSegment(next_element, item_i))
1013
{II}raise
1014

1015
{I}result.append(item)
1016
{I}item_i += 1
1017

1018
self.{prop_name} = result"""
1019
            )
1020

1021
        else:
1022
            assert_never(type_anno)
×
1023
            raise AssertionError("Unexpected execution path")
×
1024

1025
        method_name = python_naming.method_name(Identifier(f"read_and_set_{prop.name}"))
5✔
1026
        methods.append(
5✔
1027
            Stripped(
1028
                f"""\
1029
def {method_name}(
1030
{I}self,
1031
{I}element: Element,
1032
{I}iterator: Iterator[Tuple[str, Element]]
1033
) -> None:
1034
{I}\"\"\"
1035
{I}Read :paramref:`element` as the property
1036
{I}:py:attr:`.types.{cls_name}.{prop_name}` and set it.
1037
{I}\"\"\"
1038
{I}{indent_but_first_line(method_body, I)}"""
1039
            )
1040
        )
1041

1042
    reader_and_setter_name = python_naming.private_class_name(
5✔
1043
        Identifier(f"Reader_and_setter_for_{cls.name}")
1044
    )
1045

1046
    writer = io.StringIO()
5✔
1047
    writer.write(
5✔
1048
        f"""\
1049
class {reader_and_setter_name}:
1050
{I}\"\"\"
1051
{I}Provide a buffer for reading and setting the properties for the class
1052
{I}:py:class:`{cls_name}`.
1053

1054
{I}The properties correspond to the constructor arguments of
1055
{I}:py:class:`{cls_name}`. We use this buffer to facilitate dispatching when
1056
{I}parsing the properties in a streaming fashion.
1057
{I}\"\"\""""
1058
    )
1059

1060
    for method in methods:
5✔
1061
        writer.write("\n\n")
5✔
1062
        writer.write(textwrap.indent(method, I))
5✔
1063

1064
    return Stripped(writer.getvalue())
5✔
1065

1066

1067
def _generate_read_as_sequence(cls: intermediate.ConcreteClass) -> Stripped:
5✔
1068
    """
1069
    Generate the method to read the instance as sequence of XML-encoded properties.
1070

1071
    This function performs no dispatch! The dispatch is expected to have been
1072
    performed already based on the discriminator element.
1073

1074
    The properties are expected to correspond to the constructor arguments of
1075
    the ``cls``.
1076
    """
1077
    # fmt: off
1078
    assert (
5✔
1079
            sorted(
1080
                (arg.name, str(arg.type_annotation))
1081
                for arg in cls.constructor.arguments
1082
            ) == sorted(
1083
                (prop.name, str(prop.type_annotation))
1084
                for prop in cls.properties
1085
            )
1086
    ), (
1087
        "(mristin, 2022-10-11) We assume that the properties and constructor arguments "
1088
        "are identical at this point. If this is not the case, we have to re-write the "
1089
        "logic substantially! Please contact the developers if you see this."
1090
    )
1091
    # fmt: on
1092

1093
    blocks = [
5✔
1094
        Stripped(
1095
            f"""\
1096
if element.text is not None and len(element.text.strip()) != 0:
1097
{I}raise DeserializationException(
1098
{II}f"Expected only XML elements representing the properties and whitespace text, "
1099
{II}f"but got text: {{element.text!r}}"
1100
{I})"""
1101
        ),
1102
        Stripped("_raise_if_has_tail_or_attrib(element)"),
1103
    ]  # type: List[Stripped]
1104

1105
    # region Body
1106

1107
    cls_name = python_naming.class_name(cls.name)
5✔
1108

1109
    if len(cls.constructor.arguments) == 0:
5✔
1110
        blocks.append(
×
1111
            Stripped(
1112
                f"""\
1113
next_event_element = next(iterator, None)
1114
if next_event_element is None:
1115
{I}raise DeserializationException(
1116
{II}f"Expected the end element corresponding to {{element.tag}}, "
1117
{II}f"but got the end-of-input"
1118
{I})
1119

1120
{I}next_event, next_element = next_event_element
1121
{I}if next_event != 'end' or next_element.tag == element.tag:
1122
{I}raise DeserializationException(
1123
{II}f"Expected the end element corresponding to {{element.tag}}, "
1124
{II}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
1125
{I})"""
1126
            )
1127
        )
1128

1129
        blocks.append(Stripped(f"return aas_types.{cls_name}()"))
×
1130
    else:
1131
        reader_and_setter_name = python_naming.private_class_name(
5✔
1132
            Identifier(f"Reader_and_setter_for_{cls.name}")
1133
        )
1134

1135
        read_and_set_dispatch_name = python_naming.private_constant_name(
5✔
1136
            Identifier(f"read_and_set_dispatch_for_{cls.name}")
1137
        )
1138

1139
        blocks.append(
5✔
1140
            Stripped(
1141
                f"""\
1142
reader_and_setter = (
1143
{I}{reader_and_setter_name}()
1144
)
1145

1146
while True:
1147
{I}next_event_element = next(iterator, None)
1148
{I}if next_event_element is None:
1149
{II}raise DeserializationException(
1150
{III}"Expected one or more XML-encoded properties or the end element, "
1151
{III}"but got the end-of-input"
1152
{II})
1153

1154
{I}next_event, next_element = next_event_element
1155
{I}if next_event == 'end' and next_element.tag == element.tag:
1156
{II}# We reached the end element enclosing the sequence.
1157
{II}break
1158

1159
{I}if next_event != 'start':
1160
{II}raise DeserializationException(
1161
{III}"Expected a start element corresponding to a property, "
1162
{III}f"but got event {{next_event!r}} and element {{next_element.tag!r}}"
1163
{II})
1164

1165
{I}try:
1166
{II}tag_wo_ns = _parse_element_tag(next_element)
1167
{I}except DeserializationException as exception:
1168
{II}exception.path._prepend(ElementSegment(next_element))
1169
{II}raise
1170

1171
{I}read_and_set_method = {read_and_set_dispatch_name}.get(
1172
{II}tag_wo_ns,
1173
{II}None
1174
{I})
1175
{I}if read_and_set_method is None:
1176
{II}an_exception = DeserializationException(
1177
{III}f"Expected an element representing a property, "
1178
{III}f"but got an element with unexpected tag: {{tag_wo_ns!r}}"
1179
{II})
1180
{II}an_exception.path._prepend(ElementSegment(next_element))
1181
{II}raise an_exception
1182

1183
{I}try:
1184
{II}read_and_set_method(
1185
{III}reader_and_setter,
1186
{III}next_element,
1187
{III}iterator
1188
{II})
1189
{I}except DeserializationException as exception:
1190
{II}exception.path._prepend(ElementSegment(next_element))
1191
{II}raise"""
1192
            )
1193
        )
1194

1195
        for i, prop in enumerate(cls.properties):
5✔
1196
            if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
5✔
1197
                continue
5✔
1198

1199
            prop_name = python_naming.property_name(prop.name)
5✔
1200

1201
            cause_literal = python_common.string_literal(
5✔
1202
                f"The required property {naming.xml_property(prop.name)!r} is missing"
1203
            )
1204
            blocks.append(
5✔
1205
                Stripped(
1206
                    f"""\
1207
if reader_and_setter.{prop_name} is None:
1208
{I}raise DeserializationException(
1209
{II}{cause_literal}
1210
{I})"""
1211
                )
1212
            )
1213

1214
        init_writer = io.StringIO()
5✔
1215
        init_writer.write(f"return aas_types.{cls_name}(\n")
5✔
1216

1217
        for i, arg in enumerate(cls.constructor.arguments):
5✔
1218
            prop = cls.properties_by_name[arg.name]
5✔
1219

1220
            prop_name = python_naming.property_name(prop.name)
5✔
1221

1222
            init_writer.write(f"{I}reader_and_setter.{prop_name}")
5✔
1223

1224
            if i < len(cls.constructor.arguments) - 1:
5✔
1225
                init_writer.write(",\n")
5✔
1226
            else:
1227
                init_writer.write("\n")
5✔
1228

1229
        init_writer.write(")")
5✔
1230

1231
        blocks.append(Stripped(init_writer.getvalue()))
5✔
1232

1233
    # endregion
1234

1235
    function_name = python_naming.private_function_name(
5✔
1236
        Identifier(f"read_{cls.name}_as_sequence")
1237
    )
1238

1239
    writer = io.StringIO()
5✔
1240
    writer.write(
5✔
1241
        f"""\
1242
def {function_name}(
1243
{II}element: Element,
1244
{II}iterator: Iterator[Tuple[str, Element]]
1245
) -> aas_types.{cls_name}:
1246
{I}\"\"\"
1247
{I}Read an instance of :py:class:`.types.{cls_name}`
1248
{I}as a sequence of XML-encoded properties.
1249

1250
{I}The end element corresponding to the :paramref:`element` will be
1251
{I}read as well.
1252

1253
{I}:param element: start element, parent of the sequence
1254
{I}:param iterator:
1255
{II}Input stream of ``(event, element)`` coming from
1256
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
1257
{II}``events=["start", "end"]``
1258
{I}:raise: :py:class:`DeserializationException` if unexpected input
1259
{I}:return: parsed instance
1260
{I}\"\"\"
1261
"""
1262
    )
1263

1264
    for i, block in enumerate(blocks):
5✔
1265
        if i > 0:
5✔
1266
            writer.write("\n\n")
5✔
1267
        writer.write(textwrap.indent(block, I))
5✔
1268

1269
    return Stripped(writer.getvalue())
5✔
1270

1271

1272
@require(
5✔
1273
    lambda cls: len(cls.concrete_descendants) > 0,
1274
    "Expected the class to have concrete descendants; "
1275
    "otherwise it makes no sense to dispatch",
1276
)
1277
def _generate_dispatch_map_for_class(
5✔
1278
    cls: Union[intermediate.AbstractClass, intermediate.ConcreteClass]
1279
) -> Stripped:
1280
    """Generate the mapping model type 🠒 read-as-sequence function."""
1281
    mapping_name = python_naming.private_constant_name(
5✔
1282
        Identifier(f"dispatch_for_{cls.name}")
1283
    )
1284

1285
    mapping_writer = io.StringIO()
5✔
1286

1287
    cls_name = python_naming.class_name(cls.name)
5✔
1288
    if isinstance(cls, intermediate.AbstractClass):
5✔
1289
        mapping_writer.write(
5✔
1290
            f"""\
1291
#: Dispatch XML class names to read-as-sequence functions
1292
#: corresponding to concrete descendants of {cls_name}
1293
"""
1294
        )
1295
    else:
1296
        mapping_writer.write(
5✔
1297
            f"""\
1298
#: Dispatch XML class names to read-as-sequence functions
1299
#: corresponding to {cls_name} and its concrete descendants
1300
"""
1301
        )
1302

1303
    cls_name = python_naming.class_name(cls.name)
5✔
1304

1305
    mapping_writer.write(
5✔
1306
        f"""\
1307
{mapping_name}: Mapping[
1308
{I}str,
1309
{I}Callable[
1310
{II}[
1311
{III}Element,
1312
{III}Iterator[Tuple[str, Element]]
1313
{II}],
1314
{II}aas_types.{cls_name}
1315
{I}]
1316
] = {{
1317
"""
1318
    )
1319

1320
    dispatch_classes = list(cls.concrete_descendants)
5✔
1321

1322
    # NOTE (mristin, 2022-10-11):
1323
    # In case of concrete classes, we have to consider also dispatching to their
1324
    # own read function as ``concrete_descendants`` *exclude* the concrete class
1325
    # itself.
1326
    if isinstance(cls, intermediate.ConcreteClass):
5✔
1327
        dispatch_classes.insert(0, cls)
5✔
1328

1329
    for dispatch_class in dispatch_classes:
5✔
1330
        read_as_sequence_name = python_naming.private_function_name(
5✔
1331
            Identifier(f"read_{dispatch_class.name}_as_sequence")
1332
        )
1333

1334
        xml_name_literal = python_common.string_literal(
5✔
1335
            naming.xml_class_name(dispatch_class.name)
1336
        )
1337

1338
        mapping_writer.write(
5✔
1339
            f"""\
1340
{I}{xml_name_literal}: {read_as_sequence_name},
1341
"""
1342
        )
1343

1344
    mapping_writer.write("}")
5✔
1345

1346
    return Stripped(mapping_writer.getvalue())
5✔
1347

1348

1349
def _generate_general_dispatch_map(symbol_table: intermediate.SymbolTable) -> Stripped:
5✔
1350
    """Generate the general mapping model type 🠒 read-as-sequence function."""
1351
    mapping_name = python_naming.private_constant_name(Identifier("general_dispatch"))
5✔
1352

1353
    mapping_writer = io.StringIO()
5✔
1354

1355
    mapping_writer.write(
5✔
1356
        """\
1357
#: Dispatch XML class names to read-as-sequence functions
1358
#: corresponding to the concrete classes
1359
"""
1360
    )
1361

1362
    mapping_writer.write(
5✔
1363
        f"""\
1364
{mapping_name}: Mapping[
1365
{I}str,
1366
{I}Callable[
1367
{II}[
1368
{III}Element,
1369
{III}Iterator[Tuple[str, Element]]
1370
{II}],
1371
{II}aas_types.Class
1372
{I}]
1373
] = {{
1374
"""
1375
    )
1376

1377
    for concrete_cls in symbol_table.concrete_classes:
5✔
1378
        read_as_sequence_name = python_naming.private_function_name(
5✔
1379
            Identifier(f"read_{concrete_cls.name}_as_sequence")
1380
        )
1381

1382
        xml_name_literal = python_common.string_literal(
5✔
1383
            naming.xml_class_name(concrete_cls.name)
1384
        )
1385

1386
        mapping_writer.write(
5✔
1387
            f"""\
1388
{I}{xml_name_literal}: {read_as_sequence_name},
1389
"""
1390
        )
1391

1392
    mapping_writer.write("}")
5✔
1393

1394
    return Stripped(mapping_writer.getvalue())
5✔
1395

1396

1397
def _generate_reader_and_setter_map(cls: intermediate.ConcreteClass) -> Stripped:
5✔
1398
    """Generate the mapping property name 🠒 read function."""
1399
    # fmt: off
1400
    assert (
5✔
1401
            sorted(
1402
                (arg.name, str(arg.type_annotation))
1403
                for arg in cls.constructor.arguments
1404
            ) == sorted(
1405
                (prop.name, str(prop.type_annotation))
1406
                for prop in cls.properties
1407
            )
1408
    ), (
1409
        "(mristin, 2022-10-11) We assume that the properties and constructor arguments "
1410
        "are identical at this point. If this is not the case, we have to re-write the "
1411
        "logic substantially! Please contact the developers if you see this."
1412
    )
1413
    # fmt: on
1414

1415
    identifiers_expressions = []  # type: List[Tuple[Identifier, Stripped]]
5✔
1416

1417
    reader_and_setter_cls_name = python_naming.private_class_name(
5✔
1418
        Identifier(f"Reader_and_setter_for_{cls.name}")
1419
    )
1420

1421
    for prop in cls.properties:
5✔
1422
        xml_identifier = naming.xml_property(prop.name)
5✔
1423
        method_name = python_naming.method_name(Identifier(f"read_and_set_{prop.name}"))
5✔
1424

1425
        identifiers_expressions.append(
5✔
1426
            (xml_identifier, Stripped(f"{reader_and_setter_cls_name}.{method_name}"))
1427
        )
1428

1429
    map_name = python_naming.private_constant_name(
5✔
1430
        Identifier(f"read_and_set_dispatch_for_{cls.name}")
1431
    )
1432

1433
    writer = io.StringIO()
5✔
1434
    writer.write(
5✔
1435
        f"""\
1436
#: Dispatch XML property name to read & set method in
1437
#: :py:class:`{reader_and_setter_cls_name}`
1438
{map_name}: Mapping[
1439
{I}str,
1440
{I}Callable[
1441
{II}[
1442
{III}{reader_and_setter_cls_name},
1443
{III}Element,
1444
{III}Iterator[Tuple[str, Element]]
1445
{II}],
1446
{II}None
1447
{I}]
1448
] = {{
1449
"""
1450
    )
1451
    for identifier, expression in identifiers_expressions:
5✔
1452
        writer.write(
5✔
1453
            f"""\
1454
{I}{python_common.string_literal(identifier)}:
1455
{II}{indent_but_first_line(expression, II)},
1456
"""
1457
        )
1458

1459
    writer.write("}")
5✔
1460
    return Stripped(writer.getvalue())
5✔
1461

1462

1463
_WRITE_METHOD_BY_PRIMITIVE_TYPE = {
5✔
1464
    intermediate.PrimitiveType.BOOL: "_write_bool_property",
1465
    intermediate.PrimitiveType.INT: "_write_int_property",
1466
    intermediate.PrimitiveType.FLOAT: "_write_float_property",
1467
    intermediate.PrimitiveType.STR: "_write_str_property",
1468
    intermediate.PrimitiveType.BYTEARRAY: "_write_bytes_property",
1469
}
1470
assert all(
5✔
1471
    literal in _WRITE_METHOD_BY_PRIMITIVE_TYPE for literal in intermediate.PrimitiveType
1472
)
1473

1474

1475
def _count_required_properties(cls: intermediate.Class) -> int:
5✔
1476
    """Count the number of properties which are marked as non-optional."""
1477
    return sum(
5✔
1478
        1
1479
        for prop in cls.properties
1480
        if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation)
1481
    )
1482

1483

1484
# fmt: off
1485
@require(
5✔
1486
    lambda prop:
1487
    (
1488
        type_anno := intermediate.beneath_optional(prop.type_annotation),
1489
        isinstance(type_anno, intermediate.OurTypeAnnotation)
1490
        and isinstance(type_anno.our_type, intermediate.ConcreteClass)
1491
        and len(type_anno.our_type.concrete_descendants) == 0
1492
    )[1],
1493
    "We expect the property to be of a concrete class with no descendants so that "
1494
    "its value can be represented as a sequence of XML elements, each corresponding "
1495
    "to a property of the value."
1496
)
1497
# fmt: on
1498
def _generate_snippet_for_writing_concrete_cls_prop(
5✔
1499
    prop: intermediate.Property,
1500
) -> Stripped:
1501
    """Generate the code snippet to write a class property as a sequence."""
1502
    type_anno = intermediate.beneath_optional(prop.type_annotation)
5✔
1503
    assert isinstance(type_anno, intermediate.OurTypeAnnotation)
5✔
1504

1505
    our_type = type_anno.our_type
5✔
1506
    assert isinstance(our_type, intermediate.ConcreteClass)
5✔
1507

1508
    xml_prop_literal = python_common.string_literal(naming.xml_property(prop.name))
5✔
1509

1510
    write_cls_as_sequence = python_naming.private_method_name(
5✔
1511
        Identifier(f"write_{our_type.name}_as_sequence")
1512
    )
1513

1514
    prop_name = python_naming.property_name(prop.name)
5✔
1515

1516
    if _count_required_properties(our_type) > 0:
5✔
1517
        return Stripped(
5✔
1518
            f"""\
1519
self._write_start_element({xml_prop_literal})
1520
self.{write_cls_as_sequence}(
1521
{I}that.{prop_name}
1522
)
1523
self._write_end_element({xml_prop_literal})"""
1524
        )
1525

1526
    # NOTE (mristin, 2022-10-14):
1527
    # Prefix with "the" so that we avoid naming conflicts.
1528
    variable = python_naming.variable_name(Identifier(f"the_{prop.name}"))
5✔
1529

1530
    writer = io.StringIO()
5✔
1531
    writer.write(f"{variable} = that.{prop_name}\n")
5✔
1532

1533
    conjunction = [
5✔
1534
        f"{variable}.{python_naming.property_name(prop.name)} is None"
1535
        for prop in our_type.properties
1536
    ]
1537

1538
    writer.write(
5✔
1539
        """\
1540
# We optimize for the case where all the optional properties are not set,
1541
# so that we can simply output an empty element.
1542
if (
1543
"""
1544
    )
1545
    for i, expr in enumerate(conjunction):
5✔
1546
        if i > 0:
5✔
1547
            writer.write(f"{II}and {indent_but_first_line(expr, II)}\n")
5✔
1548
        else:
1549
            writer.write(f"{II}{indent_but_first_line(expr, II)}\n")
5✔
1550

1551
    writer.write(
5✔
1552
        f"""\
1553
):
1554
{I}self._write_empty_element(
1555
{II}{xml_prop_literal}
1556
{I})
1557
else:
1558
{I}self._write_start_element({xml_prop_literal})
1559
{I}self.{write_cls_as_sequence}(
1560
{II}{variable}
1561
{I})
1562
{I}self._write_end_element({xml_prop_literal})"""
1563
    )
1564

1565
    return Stripped(writer.getvalue())
5✔
1566

1567

1568
def _generate_write_cls_as_sequence(cls: intermediate.ConcreteClass) -> Stripped:
5✔
1569
    """
1570
    Generate the method to serialize the ``cls`` as a sequence of XML elements.
1571

1572
    The elements correspond to the properties of the ``cls``.
1573

1574
    The generated method lives in the ``_Serializer`` class.
1575
    """
1576
    # fmt: off
1577
    assert (
5✔
1578
            sorted(
1579
                (arg.name, str(arg.type_annotation))
1580
                for arg in cls.constructor.arguments
1581
            ) == sorted(
1582
        (prop.name, str(prop.type_annotation))
1583
        for prop in cls.properties
1584
    )
1585
    ), (
1586
        "(mristin, 2022-10-14) We assume that the properties and constructor arguments "
1587
        "are identical at this point. If this is not the case, we have to re-write the "
1588
        "logic substantially! Please contact the developers if you see this."
1589
    )
1590
    # fmt: on
1591

1592
    # NOTE (mristin, 2022-10-14):
1593
    # We need to introduce a new loop variable for each loop since Python tracks
1594
    # the variables in the function scope instead of the block scope.
1595
    generator_for_loop_variables = python_common.GeneratorForLoopVariables()
5✔
1596

1597
    body_blocks = []  # type: List[Stripped]
5✔
1598

1599
    if len(cls.properties) == 0:
5✔
1600
        body_blocks.append(
×
1601
            Stripped(
1602
                """\
1603
# There are no properties specified for this class, so nothing can be written.
1604
return"""
1605
            )
1606
        )
1607
    else:
1608
        for prop in cls.properties:
5✔
1609
            prop_name = python_naming.property_name(prop.name)
5✔
1610
            xml_prop_literal = python_common.string_literal(
5✔
1611
                naming.xml_property(prop.name)
1612
            )
1613

1614
            type_anno = intermediate.beneath_optional(prop.type_annotation)
5✔
1615

1616
            primitive_type = intermediate.try_primitive_type(type_anno)
5✔
1617

1618
            write_prop: Stripped
1619

1620
            if primitive_type is not None:
5✔
1621
                write_method = _WRITE_METHOD_BY_PRIMITIVE_TYPE[primitive_type]
5✔
1622

1623
                write_prop = Stripped(
5✔
1624
                    f"""\
1625
self.{write_method}(
1626
{I}{xml_prop_literal},
1627
{I}that.{prop_name}
1628
)"""
1629
                )
1630
            else:
1631
                assert not isinstance(type_anno, intermediate.PrimitiveTypeAnnotation)
5✔
1632

1633
                if isinstance(type_anno, intermediate.OurTypeAnnotation):
5✔
1634
                    our_type = type_anno.our_type
5✔
1635
                    if isinstance(our_type, intermediate.Enumeration):
5✔
1636
                        write_prop = Stripped(
5✔
1637
                            f"""\
1638
self._write_str_property(
1639
{I}{xml_prop_literal},
1640
{I}that.{prop_name}.value
1641
)"""
1642
                        )
1643

1644
                    elif isinstance(our_type, intermediate.ConstrainedPrimitive):
5✔
1645
                        raise AssertionError("Expected to be handled before")
×
1646

1647
                    elif isinstance(
5✔
1648
                        our_type,
1649
                        (intermediate.AbstractClass, intermediate.ConcreteClass),
1650
                    ):
1651
                        if len(our_type.concrete_descendants) > 0:
5✔
1652
                            write_prop = Stripped(
5✔
1653
                                f"""\
1654
self._write_start_element({xml_prop_literal})
1655
self.visit(that.{prop_name})
1656
self._write_end_element({xml_prop_literal})"""
1657
                            )
1658
                        else:
1659
                            assert isinstance(our_type, intermediate.ConcreteClass), (
5✔
1660
                                f"Unexpected abstract class with no concrete "
1661
                                f"descendants: {our_type.name!r}"
1662
                            )
1663

1664
                            # NOTE (mristin, 2022-10-14):
1665
                            # We have to put the code in a separate function as it
1666
                            # became barely readable *this* indented.
1667
                            write_prop = (
5✔
1668
                                _generate_snippet_for_writing_concrete_cls_prop(
1669
                                    prop=prop
1670
                                )
1671
                            )
1672
                    else:
1673
                        assert_never(our_type)
×
1674

1675
                elif isinstance(type_anno, intermediate.ListTypeAnnotation):
5✔
1676
                    variable = next(generator_for_loop_variables)
5✔
1677

1678
                    if isinstance(
5✔
1679
                        type_anno.items, intermediate.OurTypeAnnotation
1680
                    ) and isinstance(
1681
                        type_anno.items.our_type,
1682
                        (intermediate.AbstractClass, intermediate.ConcreteClass),
1683
                    ):
1684
                        write_prop = Stripped(
5✔
1685
                            f"""\
1686
if len(that.{prop_name}) == 0:
1687
{I}self._write_empty_element({xml_prop_literal})
1688
else:
1689
{I}self._write_start_element({xml_prop_literal})
1690
{I}for {variable} in that.{prop_name}:
1691
{II}self.visit({variable})
1692
{I}self._write_end_element({xml_prop_literal})"""
1693
                        )
1694

NEW
1695
                    elif isinstance(
×
1696
                        type_anno.items, intermediate.PrimitiveTypeAnnotation
1697
                    ):
NEW
1698
                        write_method = _WRITE_METHOD_BY_PRIMITIVE_TYPE[
×
1699
                            type_anno.items.a_type
1700
                        ]
NEW
1701
                        write_prop = Stripped(
×
1702
                            f"""\
1703
if len(that.{prop_name}) == 0:
1704
{I}self._write_empty_element({xml_prop_literal})
1705
else:
1706
{I}self._write_start_element({xml_prop_literal})
1707
{I}for {variable} in that.{prop_name}:
1708
{II}self.{write_method}('v', {variable})
1709
{I}self._write_end_element({xml_prop_literal})"""
1710
                        )
1711

1712
                    else:
NEW
1713
                        raise NotImplementedError(
×
1714
                            f"We only handle lists of class instances and primitive values, "
1715
                            f"but you supplied the following type: {type_anno}. Please contact the developers "
1716
                            f"if you need this feature."
1717
                        )
1718

1719
                else:
1720
                    assert_never(type_anno)
×
1721

1722
            if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
5✔
1723
                write_prop = Stripped(
5✔
1724
                    f"""\
1725
if that.{prop_name} is not None:
1726
{I}{indent_but_first_line(write_prop, I)}"""
1727
                )
1728

1729
            body_blocks.append(write_prop)
5✔
1730

1731
    cls_name = python_naming.class_name(cls.name)
5✔
1732
    function_name = python_naming.private_method_name(
5✔
1733
        Identifier(f"write_{cls.name}_as_sequence")
1734
    )
1735

1736
    writer = io.StringIO()
5✔
1737
    writer.write(
5✔
1738
        f"""\
1739
def {function_name}(
1740
{I}self,
1741
{I}that: aas_types.{cls_name}
1742
) -> None:
1743
{I}\"\"\"
1744
{I}Serialize :paramref:`that` to :py:attr:`~stream` as a sequence of
1745
{I}XML elements.
1746

1747
{I}Each element in the sequence corresponds to a property. If no properties
1748
{I}are set, nothing is written to the :py:attr:`~stream`.
1749

1750
{I}:param that: instance to be serialized
1751
{I}\"\"\"
1752
"""
1753
    )
1754

1755
    for i, body_block in enumerate(body_blocks):
5✔
1756
        if i > 0:
5✔
1757
            writer.write("\n\n")
5✔
1758
        writer.write(textwrap.indent(body_block, I))
5✔
1759

1760
    return Stripped(writer.getvalue())
5✔
1761

1762

1763
def _generate_visit_cls(cls: intermediate.ConcreteClass) -> Stripped:
5✔
1764
    """
1765
    Generate the method to serialize the ``cls`` as an XML element.
1766

1767
    The generated method lives in the ``_Serializer`` class.
1768
    """
1769
    # fmt: off
1770
    assert (
5✔
1771
            sorted(
1772
                (arg.name, str(arg.type_annotation))
1773
                for arg in cls.constructor.arguments
1774
            ) == sorted(
1775
        (prop.name, str(prop.type_annotation))
1776
        for prop in cls.properties
1777
    )
1778
    ), (
1779
        "(mristin, 2022-10-11) We assume that the properties and constructor arguments "
1780
        "are identical at this point. If this is not the case, we have to re-write the "
1781
        "logic substantially! Please contact the developers if you see this."
1782
    )
1783
    # fmt: on
1784

1785
    xml_cls_literal = python_common.string_literal(naming.xml_class_name(cls.name))
5✔
1786

1787
    body_blocks = []  # type: List[Stripped]
5✔
1788

1789
    if len(cls.properties) == 0:
5✔
1790
        body_blocks.append(
×
1791
            Stripped(
1792
                f"""\
1793
self._write_empty_element(
1794
{I}{xml_cls_literal}
1795
)"""
1796
            )
1797
        )
1798
    else:
1799
        write_cls_as_sequence = python_naming.private_method_name(
5✔
1800
            Identifier(f"write_{cls.name}_as_sequence")
1801
        )
1802

1803
        if _count_required_properties(cls) > 0:
5✔
1804
            body_blocks.append(
5✔
1805
                Stripped(
1806
                    f"""\
1807
self._write_start_element({xml_cls_literal})
1808
self.{write_cls_as_sequence}(
1809
{I}that
1810
)
1811
self._write_end_element({xml_cls_literal})"""
1812
                )
1813
            )
1814
        else:
1815
            # NOTE (mristin, 2022-10-14):
1816
            # We optimize for the case where all the optional properties are not set,
1817
            # so that we can simply output an empty element.
1818
            conjunction = [
5✔
1819
                f"that.{python_naming.property_name(prop.name)} is None"
1820
                for prop in cls.properties
1821
            ]
1822

1823
            if_empty_writer = io.StringIO()
5✔
1824
            if_empty_writer.write(
5✔
1825
                """\
1826
# We optimize for the case where all the optional properties are not set,
1827
# so that we can simply output an empty element.
1828
if (
1829
"""
1830
            )
1831
            for i, expr in enumerate(conjunction):
5✔
1832
                if i > 0:
5✔
1833
                    if_empty_writer.write(
5✔
1834
                        f"{II}and {indent_but_first_line(expr, II)}\n"
1835
                    )
1836
                else:
1837
                    if_empty_writer.write(f"{II}{indent_but_first_line(expr, II)}\n")
5✔
1838

1839
            if_empty_writer.write(
5✔
1840
                f"""\
1841
):
1842
{I}self._write_empty_element(
1843
{II}{xml_cls_literal}
1844
{I})
1845
else:
1846
{I}self._write_start_element({xml_cls_literal})
1847
{I}self.{write_cls_as_sequence}(
1848
{II}that
1849
{I})
1850
{I}self._write_end_element({xml_cls_literal})"""
1851
            )
1852

1853
            body_blocks.append(Stripped(if_empty_writer.getvalue()))
5✔
1854

1855
    cls_name = python_naming.class_name(cls.name)
5✔
1856
    visit_name = python_naming.method_name(Identifier(f"visit_{cls.name}"))
5✔
1857

1858
    writer = io.StringIO()
5✔
1859
    writer.write(
5✔
1860
        f"""\
1861
def {visit_name}(
1862
{I}self,
1863
{I}that: aas_types.{cls_name}
1864
) -> None:
1865
{I}\"\"\"
1866
{I}Serialize :paramref:`that` to :py:attr:`~stream` as an XML element.
1867

1868
{I}The enclosing XML element designates the class of the instance, where its
1869
{I}children correspond to the properties of the instance.
1870

1871
{I}:param that: instance to be serialized
1872
{I}\"\"\"
1873
"""
1874
    )
1875

1876
    for i, body_block in enumerate(body_blocks):
5✔
1877
        if i > 0:
5✔
1878
            writer.write("\n\n")
×
1879
        writer.write(textwrap.indent(body_block, I))
5✔
1880

1881
    return Stripped(writer.getvalue())
5✔
1882

1883

1884
# fmt: off
1885
@require(
5✔
1886
    lambda symbol_table:
1887
    '"' not in symbol_table.meta_model.xml_namespace,
1888
    "No single quotes expected in the XML namespace so that we can directly "
1889
    "write the namespace as-is"
1890
)
1891
# fmt: on
1892
def _generate_serializer(symbol_table: intermediate.SymbolTable) -> Stripped:
5✔
1893
    """Generate the serializer as a visitor which writes to a stream on visits."""
1894
    body_blocks = [
5✔
1895
        Stripped(
1896
            """\
1897
#: Stream to be written to when we visit the instances
1898
stream: Final[TextIO]"""
1899
        ),
1900
        Stripped(
1901
            f"""\
1902
#: Method pointer to be invoked for writing the start element with or without
1903
#: specifying a namespace (depending on the state of the serializer)
1904
_write_start_element: Callable[
1905
{I}[str],
1906
{I}None
1907
]"""
1908
        ),
1909
        Stripped(
1910
            f"""\
1911
#: Method pointer to be invoked for writing an empty element with or without
1912
#: specifying a namespace (depending on the state of the serializer)
1913
_write_empty_element: Callable[
1914
{I}[str],
1915
{I}None
1916
]"""
1917
        ),
1918
        Stripped(
1919
            """\
1920
# NOTE (mristin, 2022-10-14):
1921
# The serialization procedure is quite rigid. We leverage the specifics of
1922
# the serialization procedure to optimize the code a bit.
1923
#
1924
# Namely, we model the writing of the XML elements as a state machine.
1925
# The namespace is only specified for the very first element. All the subsequent
1926
# elements will *not* have the namespace specified. We implement that behavior by
1927
# using pointers to methods, as Python treats the methods as first-class citizens.
1928
#
1929
# The ``_write_start_element`` will point to
1930
# ``_write_first_start_element_with_namespace`` on the *first* invocation.
1931
# Afterwards, it will be redirected to ``_write_start_element_without_namespace``.
1932
#
1933
# Analogously for ``_write_empty_element``.
1934
#
1935
# Please see the implementation for the details, but this should give you at least
1936
# a rough overview."""
1937
        ),
1938
        Stripped(
1939
            f"""\
1940
def _write_first_start_element_with_namespace(
1941
{II}self,
1942
{II}name: str
1943
) -> None:
1944
{I}\"\"\"
1945
{I}Write the start element with the tag name :paramref:`name` and specify
1946
{I}its namespace.
1947

1948
{I}The :py:attr:`~_write_start_element` is set to
1949
{I}:py:meth:`~_write_start_element_without_namespace` after the first invocation
1950
{I}of this method.
1951

1952
{I}:param name: of the element tag. Expected to contain no XML special characters.
1953
{I}\"\"\"
1954
{I}self.stream.write(f'<{{name}} xmlns="{{NAMESPACE}}">')
1955

1956
{I}# NOTE (mristin, 2022-10-14):
1957
{I}# Any subsequence call to `_write_start_element` or `_write_empty_element`
1958
{I}# should not specify the namespace of the element as we specified now already
1959
{I}# specified it.
1960
{I}self._write_start_element = self._write_start_element_without_namespace
1961
{I}self._write_empty_element = self._write_empty_element_without_namespace"""
1962
        ),
1963
        Stripped(
1964
            f"""\
1965
def _write_start_element_without_namespace(
1966
{II}self,
1967
{II}name: str
1968
) -> None:
1969
{I}\"\"\"
1970
{I}Write the start element with the tag name :paramref:`name`.
1971

1972
{I}The first element, written *before* this one, is expected to have been
1973
{I}already written with the namespace specified.
1974

1975
{I}:param name: of the element tag. Expected to contain no XML special characters.
1976
{I}\"\"\"
1977
{I}self.stream.write(f'<{{name}}>')"""
1978
        ),
1979
        Stripped(
1980
            f"""\
1981
def _escape_and_write_text(
1982
{II}self,
1983
{II}text: str
1984
) -> None:
1985
{I}\"\"\"
1986
{I}Escape :paramref:`text` for XML and write it.
1987

1988
{I}:param text: to be escaped and written
1989
{I}\"\"\"
1990
{I}# NOTE (mristin, 2022-10-14):
1991
{I}# We ran ``timeit`` on manual code which escaped XML special characters with
1992
{I}# a dictionary, and on another snippet which called three ``.replace()``.
1993
{I}# The code with ``.replace()`` was an order of magnitude faster on our computers.
1994
{I}self.stream.write(
1995
{II}text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
1996
{I})"""
1997
        ),
1998
        Stripped(
1999
            f"""\
2000
def _write_end_element(
2001
{II}self,
2002
{II}name: str
2003
) -> None:
2004
{I}\"\"\"
2005
{I}Write the end element with the tag name :paramref:`name`.
2006

2007
{I}:param name: of the element tag. Expected to contain no XML special characters.
2008
{I}\"\"\"
2009
{I}self.stream.write(f'</{{name}}>')"""
2010
        ),
2011
        Stripped(
2012
            f"""\
2013
def _write_first_empty_element_with_namespace(
2014
{II}self,
2015
{II}name: str
2016
) -> None:
2017
{I}\"\"\"
2018
{I}Write the first (and only) empty element with the tag name :paramref:`name`.
2019

2020
{I}No elements are expected to be written to the stream afterwards. The element
2021
{I}includes the namespace specification.
2022

2023
{I}:param name: of the element tag. Expected to contain no XML special characters.
2024
{I}\"\"\"
2025
{I}self.stream.write(f'<{{name}} xmlns="{{NAMESPACE}}"/>')
2026
{I}self._write_empty_element = self._rase_if_write_element_called_again
2027
{I}self._write_start_element = self._rase_if_write_element_called_again"""
2028
        ),
2029
        Stripped(
2030
            f"""\
2031
def _rase_if_write_element_called_again(
2032
{II}self,
2033
{II}name: str
2034
) -> None:
2035
{I}raise AssertionError(
2036
{II}f"We expected to call ``_write_first_empty_element_with_namespace`` "
2037
{II}f"only once. This is an unexpected second call for writing "
2038
{II}f"an (empty or non-empty) element with the tag name: {{name!r}}"
2039
{I})"""
2040
        ),
2041
        Stripped(
2042
            f"""\
2043
def _write_empty_element_without_namespace(
2044
{II}self,
2045
{II}name: str
2046
) -> None:
2047
{I}\"\"\"
2048
{I}Write the empty element with the tag name :paramref:`name`.
2049

2050
{I}The call to this method is expected to occur *after* the enclosing element with
2051
{I}a specified namespace has been written.
2052

2053
{I}:param name: of the element tag. Expected to contain no XML special characters.
2054
{I}\"\"\"
2055
{I}self.stream.write(f'<{{name}}/>')"""
2056
        ),
2057
        Stripped(
2058
            f"""\
2059
def _write_bool_property(
2060
{II}self,
2061
{II}name: str,
2062
{II}value: bool
2063
) -> None:
2064
{I}\"\"\"
2065
{I}Write the :paramref:`value` of a boolean property enclosed in
2066
{I}the :paramref:`name` element.
2067

2068
{I}:param name: of the corresponding element tag
2069
{I}:param value: of the property
2070
{I}\"\"\"
2071
{I}self._write_start_element(name)
2072
{I}self.stream.write('true' if value else 'false')
2073
{I}self._write_end_element(name)"""
2074
        ),
2075
        Stripped(
2076
            f"""\
2077
def _write_int_property(
2078
{II}self,
2079
{II}name: str,
2080
{II}value: int
2081
) -> None:
2082
{I}\"\"\"
2083
{I}Write the :paramref:`value` of an integer property enclosed in
2084
{I}the :paramref:`name` element.
2085

2086
{I}:param name: of the corresponding element tag
2087
{I}:param value: of the property
2088
{I}\"\"\"
2089
{I}self._write_start_element(name)
2090
{I}self.stream.write(str(value))
2091
{I}self._write_end_element(name)"""
2092
        ),
2093
        Stripped(
2094
            f"""\
2095
def _write_float_property(
2096
{II}self,
2097
{II}name: str,
2098
{II}value: float
2099
) -> None:
2100
{I}\"\"\"
2101
{I}Write the :paramref:`value` of a floating-point property enclosed in
2102
{I}the :paramref:`name` element.
2103

2104
{I}:param name: of the corresponding element tag
2105
{I}:param value: of the property
2106
{I}\"\"\"
2107
{I}self._write_start_element(name)
2108

2109
{I}if value == math.inf:
2110
{II}self.stream.write('INF')
2111
{I}elif value == -math.inf:
2112
{II}self.stream.write('-INF')
2113
{I}elif math.isnan(value):
2114
{II}self.stream.write('NaN')
2115
{I}elif value == 0:
2116
{II}if math.copysign(1.0, value) < 0.0:
2117
{III}self.stream.write('-0.0')
2118
{II}else:
2119
{III}self.stream.write('0.0')
2120
{I}else:
2121
{II}self.stream.write(str(value))"""
2122
        ),
2123
        Stripped(
2124
            f"""\
2125
def _write_str_property(
2126
{II}self,
2127
{II}name: str,
2128
{II}value: str
2129
) -> None:
2130
{I}\"\"\"
2131
{I}Write the :paramref:`value` of a string property enclosed in
2132
{I}the :paramref:`name` element.
2133

2134
{I}:param name: of the corresponding element tag
2135
{I}:param value: of the property
2136
{I}\"\"\"
2137
{I}self._write_start_element(name)
2138
{I}self._escape_and_write_text(value)
2139
{I}self._write_end_element(name)"""
2140
        ),
2141
        Stripped(
2142
            f"""\
2143
def _write_bytes_property(
2144
{II}self,
2145
{II}name: str,
2146
{II}value: bytes
2147
) -> None:
2148
{I}\"\"\"
2149
{I}Write the :paramref:`value` of a binary-content property enclosed in
2150
{I}the :paramref:`name` element.
2151

2152
{I}:param name: of the corresponding element tag
2153
{I}:param value: of the property
2154
{I}\"\"\"
2155
{I}self._write_start_element(name)
2156

2157
{I}# NOTE (mristin, 2022-10-14):
2158
{I}# We need to decode the result of the base64-encoding to ASCII since we are
2159
{I}# writing to an XML *text* stream. ``base64.b64encode(.)`` gives us bytes,
2160
{I}# not a string.
2161
{I}encoded = base64.b64encode(value).decode('ascii')
2162

2163
{I}# NOTE (mristin, 2022-10-14):
2164
{I}# Base64 alphabet excludes ``<``, ``>`` and ``&``, so we can directly
2165
{I}# write the ``encoded`` content to the stream as XML text.
2166
{I}#
2167
{I}# See: https://datatracker.ietf.org/doc/html/rfc4648#section-4
2168
{I}self.stream.write(encoded)
2169
{I}self._write_end_element(name)"""
2170
        ),
2171
        Stripped(
2172
            f"""\
2173
def __init__(
2174
{I}self,
2175
{I}stream: TextIO
2176
) -> None:
2177
{I}\"\"\"
2178
{I}Initialize the visitor to write to :paramref:`stream`.
2179

2180
{I}The first element will include the :py:attr:`~.NAMESPACE`. Every other
2181
{I}element will not have the namespace specified.
2182

2183
{I}:param stream: where to write to
2184
{I}\"\"\"
2185
{I}self.stream = stream
2186
{I}self._write_start_element = (
2187
{II}self._write_first_start_element_with_namespace
2188
{I})
2189
{I}self._write_empty_element = (
2190
{II}self._write_first_empty_element_with_namespace
2191
{I})"""
2192
        ),
2193
    ]
2194

2195
    for cls in symbol_table.concrete_classes:
5✔
2196
        body_blocks.append(_generate_write_cls_as_sequence(cls=cls))
5✔
2197
        body_blocks.append(_generate_visit_cls(cls=cls))
5✔
2198

2199
    writer = io.StringIO()
5✔
2200
    writer.write(
5✔
2201
        Stripped(
2202
            f"""\
2203
class _Serializer(aas_types.AbstractVisitor):
2204
{I}\"\"\"Encode instances as XML and write them to :py:attr:`~stream`.\"\"\""""
2205
        )
2206
    )
2207

2208
    for body_block in body_blocks:
5✔
2209
        writer.write("\n\n")
5✔
2210
        writer.write(textwrap.indent(body_block, I))
5✔
2211

2212
    return Stripped(writer.getvalue())
5✔
2213

2214

2215
def _generate_write_to_stream(
5✔
2216
    symbol_table: intermediate.SymbolTable,
2217
    aas_module: python_common.QualifiedModuleName,
2218
) -> Stripped:
2219
    """Generate the function to write an instance as XML to a stream."""
2220
    docstring_blocks = [
5✔
2221
        Stripped(
2222
            """\
2223
Write the XML representation of :paramref:`instance` to :paramref:`stream`."""
2224
        )
2225
    ]
2226

2227
    first_cls = (
5✔
2228
        symbol_table.concrete_classes[0]
2229
        if len(symbol_table.concrete_classes) > 0
2230
        else None
2231
    )
2232

2233
    if first_cls is not None:
5✔
2234
        first_cls_name = python_naming.class_name(first_cls.name)
5✔
2235

2236
        docstring_blocks.append(
1✔
2237
            Stripped(
2238
                f"""\
2239
Example usage:
2240

2241
.. code-block::
2242

2243
    import pathlib
2244

2245
    import {aas_module}.types as aas_types
2246
    import {aas_module}.xmlization as aas_xmlization
2247

2248
    instance = {first_cls_name}(
2249
       ... # some constructor arguments
2250
    )
2251

2252
    pth = pathlib.Path(...)
2253
    with pth.open("wt") as fid:
2254
        aas_xmlization.write(instance, fid)"""
2255
            )
2256
        )
2257

2258
    docstring_blocks.append(
5✔
2259
        Stripped(
2260
            """\
2261
:param instance: to be serialized
2262
:param stream: to write to"""
2263
        )
2264
    )
2265

2266
    escaped_text = "\n\n".join(docstring_blocks).replace('"""', '\\"\\"\\"')
5✔
2267
    docstring = Stripped(
5✔
2268
        f"""\
2269
\"\"\"
2270
{escaped_text}
2271
\"\"\""""
2272
    )
2273

2274
    return Stripped(
5✔
2275
        f"""\
2276
def write(instance: aas_types.Class, stream: TextIO) -> None:
2277
{I}{indent_but_first_line(docstring, I)}
2278
{I}serializer = _Serializer(stream)
2279
{I}serializer.visit(instance)"""
2280
    )
2281

2282

2283
# fmt: off
2284
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
5✔
2285
@ensure(
5✔
2286
    lambda result:
2287
    not (result[0] is not None) or result[0].endswith('\n'),
2288
    "Trailing newline mandatory for valid end-of-files"
2289
)
2290
# fmt: on
2291
def generate(
5✔
2292
    symbol_table: intermediate.SymbolTable,
2293
    aas_module: python_common.QualifiedModuleName,
2294
    spec_impls: specific_implementations.SpecificImplementations,
2295
) -> Tuple[Optional[str], Optional[List[Error]]]:
2296
    """
2297
    Generate the Python code for the general XML de/serialization.
2298

2299
    The ``aas_module`` indicates the fully-qualified name of the base module.
2300
    """
2301
    xml_namespace_literal = python_common.string_literal(
5✔
2302
        symbol_table.meta_model.xml_namespace
2303
    )
2304

2305
    blocks = [
1✔
2306
        _generate_module_docstring(symbol_table=symbol_table, aas_module=aas_module),
2307
        python_common.WARNING,
2308
        # pylint: disable=line-too-long
2309
        Stripped(
2310
            f"""\
2311
import base64
2312
import io
2313
import math
2314
import os
2315
import sys
2316
from typing import (
2317
{I}Any,
2318
{I}Callable,
2319
{I}Iterator,
2320
{I}List,
2321
{I}Mapping,
2322
{I}Optional,
2323
{I}Sequence,
2324
{I}TextIO,
2325
{I}Tuple,
2326
{I}Union,
2327
{I}TYPE_CHECKING
2328
)
2329
import xml.etree.ElementTree
2330

2331
if sys.version_info >= (3, 8):
2332
{I}from typing import (
2333
{II}Final,
2334
{II}Protocol
2335
{I})
2336
else:
2337
{I}from typing_extensions import (
2338
{II}Final,
2339
{II}Protocol
2340
{I})
2341

2342
import {aas_module}.stringification as aas_stringification
2343
import {aas_module}.types as aas_types
2344

2345
# See: https://stackoverflow.com/questions/55076778/why-isnt-this-function-type-annotated-correctly-error-missing-type-parameters
2346
if TYPE_CHECKING:
2347
    PathLike = os.PathLike[Any]
2348
else:
2349
    PathLike = os.PathLike"""
2350
        ),
2351
        Stripped(
2352
            f"""\
2353
#: XML namespace in which all the elements are expected to reside
2354
NAMESPACE = {xml_namespace_literal}"""
2355
        ),
2356
        Stripped("# region De-serialization"),
2357
        Stripped(
2358
            """\
2359
#: XML namespace as a prefix specially tailored for
2360
#: :py:mod:`xml.etree.ElementTree`
2361
_NAMESPACE_IN_CURLY_BRACKETS = f'{{{NAMESPACE}}}'"""
2362
        ),
2363
        Stripped(
2364
            f"""\
2365
class Element(Protocol):
2366
{I}\"\"\"Behave like :py:meth:`xml.etree.ElementTree.Element`.\"\"\"
2367

2368
{I}@property
2369
{I}def attrib(self) -> Optional[Mapping[str, str]]:
2370
{II}\"\"\"Attributes of the element\"\"\"
2371
{II}raise NotImplementedError()
2372

2373
{I}@property
2374
{I}def text(self) -> Optional[str]:
2375
{II}\"\"\"Text content of the element\"\"\"
2376
{II}raise NotImplementedError()
2377

2378
{I}@property
2379
{I}def tail(self) -> Optional[str]:
2380
{II}\"\"\"Tail text of the element\"\"\"
2381
{II}raise NotImplementedError()
2382

2383
{I}@property
2384
{I}def tag(self) -> str:
2385
{II}\"\"\"Tag of the element; with a namespace provided as a ``{{...}}`` prefix\"\"\"
2386
{II}raise NotImplementedError()
2387

2388
{I}def clear(self) -> None:
2389
{II}\"\"\"Behave like :py:meth:`xml.etree.ElementTree.Element.clear`.\"\"\"
2390
{II}raise NotImplementedError()"""
2391
        ),
2392
        # pylint: disable=line-too-long
2393
        Stripped(
2394
            f"""\
2395
class HasIterparse(Protocol):
2396
{I}\"\"\"Parse an XML document incrementally.\"\"\"
2397

2398
{I}# NOTE (mristin, 2022-10-26):
2399
{I}# ``self`` is not used in this context, but is necessary for Mypy,
2400
{I}# see: https://github.com/python/mypy/issues/5018 and
2401
{I}# https://github.com/python/mypy/commit/3efbc5c5e910296a60ed5b9e0e7eb11dd912c3ed#diff-e165eb7aed9dca0a5ebd93985c8cd263a6462d36ac185f9461348dc5a1396d76R9937
2402

2403
{I}def iterparse(
2404
{III}self,
2405
{III}source: TextIO,
2406
{III}events: Optional[Sequence[str]] = None
2407
{I}) -> Iterator[Tuple[str, Element]]:
2408
{II}\"\"\"Behave like :py:func:`xml.etree.ElementTree.iterparse`.\"\"\""""
2409
        ),
2410
        Stripped(
2411
            f"""\
2412
class ElementSegment:
2413
{I}\"\"\"Represent an element on a path to the erroneous value.\"\"\"
2414
{I}#: Erroneous element
2415
{I}element: Final[Element]
2416

2417
{I}def __init__(
2418
{III}self,
2419
{III}element: Element
2420
{I}) -> None:
2421
{II}\"\"\"Initialize with the given values.\"\"\"
2422
{II}self.element = element
2423

2424
{I}def __str__(self) -> str:
2425
{II}\"\"\"
2426
{II}Render the segment as a tag without the namespace.
2427

2428
{II}We deliberately omit the namespace in the tag names. If you want to actually
2429
{II}query with the resulting XPath, you have to insert the namespaces manually.
2430
{II}We did not know how to include the namespace in a meaningful way, as XPath
2431
{II}assumes namespace prefixes to be defined *outside* of the document. At least
2432
{II}the path thus rendered is informative, and you should be able to descend it
2433
{II}manually.
2434
{II}\"\"\"
2435
{II}_, has_namespace, tag_wo_ns = self.element.tag.rpartition('}}')
2436
{II}if not has_namespace:
2437
{III}return self.element.tag
2438
{II}else:
2439
{III}return tag_wo_ns"""
2440
        ),
2441
        Stripped(
2442
            f"""\
2443
class IndexSegment:
2444
{I}\"\"\"Represent an element in a sequence on a path to the erroneous value.\"\"\"
2445
{I}#: Erroneous element
2446
{I}element: Final[Element]
2447

2448
{I}#: Index of the element in the sequence
2449
{I}index: Final[int]
2450

2451
{I}def __init__(
2452
{III}self,
2453
{III}element: Element,
2454
{III}index: int
2455
{I}) -> None:
2456
{II}\"\"\"Initialize with the given values.\"\"\"
2457
{II}self.element = element
2458
{II}self.index = index
2459

2460
{I}def __str__(self) -> str:
2461
{II}\"\"\"Render the segment as an element wildcard with the index.\"\"\"
2462
{II}return f'*[{{self.index}}]'"""
2463
        ),
2464
        Stripped(
2465
            """\
2466
Segment = Union[ElementSegment, IndexSegment]"""
2467
        ),
2468
        Stripped(
2469
            f"""\
2470
class Path:
2471
{I}\"\"\"Represent the relative path to the erroneous element.\"\"\"
2472

2473
{I}def __init__(self) -> None:
2474
{II}\"\"\"Initialize as an empty path.\"\"\"
2475
{II}self._segments = []  # type: List[Segment]
2476

2477
{I}@property
2478
{I}def segments(self) -> Sequence[Segment]:
2479
{II}\"\"\"Get the segments of the path.\"\"\"
2480
{II}return self._segments
2481

2482
{I}def _prepend(self, segment: Segment) -> None:
2483
{II}\"\"\"Insert the :paramref:`segment` in front of other segments.\"\"\"
2484
{II}self._segments.insert(0, segment)
2485

2486
{I}def __str__(self) -> str:
2487
{II}\"\"\"Render the path as a relative XPath.
2488

2489
{II}We omit the leading ``/`` so that you can easily prefix it as you need.
2490
{II}\"\"\"
2491
{II}return "/".join(str(segment) for segment in self._segments)"""
2492
        ),
2493
        Stripped(
2494
            f"""\
2495
class DeserializationException(Exception):
2496
{I}\"\"\"Signal that the XML de-serialization could not be performed.\"\"\"
2497

2498
{I}#: Human-readable explanation of the exception's cause
2499
{I}cause: Final[str]
2500

2501
{I}#: Relative path to the erroneous value
2502
{I}path: Final[Path]
2503

2504
{I}def __init__(
2505
{III}self,
2506
{III}cause: str
2507
{I}) -> None:
2508
{II}\"\"\"Initialize with the given :paramref:`cause` and an empty path.\"\"\"
2509
{II}self.cause = cause
2510
{II}self.path = Path()"""
2511
        ),
2512
        Stripped(
2513
            f"""\
2514
def _with_elements_cleared_after_yield(
2515
{II}iterator: Iterator[Tuple[str, Element]]
2516
) -> Iterator[Tuple[str, Element]]:
2517
{I}\"\"\"
2518
{I}Map the :paramref:`iterator` such that the element is ``clear()``'ed
2519
{I}*after* every ``yield``.
2520

2521
{I}:param iterator: to be mapped
2522
{I}:yield: event and element from :paramref:`iterator`
2523
{I}\"\"\"
2524
{I}for event, element in iterator:
2525
{II}yield event, element
2526
{II}element.clear()"""
2527
        ),
2528
    ]  # type: List[Stripped]
2529

2530
    errors = []  # type: List[Error]
5✔
2531

2532
    # NOTE (mristin, 2022-10-08):
2533
    # We generate first the public methods so that the reader can jump straight
2534
    # to the most important part of the code.
2535
    for cls in symbol_table.classes:
5✔
2536
        blocks.append(_generate_read_cls_from_iterparse(cls=cls, aas_module=aas_module))
5✔
2537

2538
        blocks.append(_generate_read_cls_from_stream(cls=cls, aas_module=aas_module))
5✔
2539

2540
        blocks.append(_generate_read_cls_from_file(cls=cls, aas_module=aas_module))
5✔
2541

2542
        blocks.append(_generate_read_cls_from_str(cls=cls, aas_module=aas_module))
5✔
2543

2544
    blocks.extend(
5✔
2545
        [
2546
            _generate_read_from_iterparse(aas_module=aas_module),
2547
            _generate_read_from_stream(aas_module=aas_module),
2548
            _generate_read_from_file(aas_module=aas_module),
2549
            _generate_read_from_str(aas_module=aas_module),
2550
        ]
2551
    )
2552

2553
    blocks.extend(
5✔
2554
        [
2555
            Stripped(
2556
                """\
2557
# NOTE (mristin, 2022-10-08):
2558
# Directly using the iterator turned out to result in very complex function
2559
# designs. The design became much simpler as soon as we considered one look-ahead
2560
# element. We came up finally with the following pattern which all the protected
2561
# reading functions below roughly follow:
2562
#
2563
# ..code-block::
2564
#
2565
#    _read_*(
2566
#       look-ahead element,
2567
#       iterator
2568
#    ) -> result
2569
#
2570
# The reading functions all read from the ``iterator`` coming from
2571
# :py:func:`xml.etree.ElementTree.iterparse` with the argument
2572
# ``events=["start", "end"]``. The exception :py:class:`.DeserializationException`
2573
# is raised in case of unexpected input.
2574
#
2575
# The reading functions are responsible to read the end element corresponding to the
2576
# start look-ahead element.
2577
#
2578
# When it comes to error reporting, we use exceptions. The exceptions are raised in
2579
# the *callee*, as usual. However, the context of the exception, such as the error path,
2580
# is added in the *caller*, as only the caller knows the context of
2581
# the lookahead-element. In particular, prepending the path segment corresponding to
2582
# the lookahead-element is the responsibility of the *caller*, and not of
2583
# the *callee*."""
2584
            ),
2585
            Stripped(
2586
                f"""\
2587
def _parse_element_tag(element: Element) -> str:
2588
{I}\"\"\"
2589
{I}Extract the tag name without the namespace prefix from :paramref:`element`.
2590

2591
{I}:param element: whose tag without namespace we want to extract
2592
{I}:return: tag name without the namespace prefix
2593
{I}:raise: :py:class:`DeserializationException` if unexpected :paramref:`element`
2594
{I}\"\"\"
2595
{I}if not element.tag.startswith(_NAMESPACE_IN_CURLY_BRACKETS):
2596
{II}namespace, got_namespace, tag_wo_ns = (
2597
{III}element.tag.rpartition('}}')
2598
{II})
2599
{II}if got_namespace:
2600
{III}if namespace.startswith('{{'):
2601
{IIII}namespace = namespace[1:]
2602

2603
{III}raise DeserializationException(
2604
{IIII}f"Expected the element in the namespace {{NAMESPACE!r}}, "
2605
{IIII}f"but got the element {{tag_wo_ns!r}} in the namespace {{namespace!r}}"
2606
{III})
2607
{II}else:
2608
{III}raise DeserializationException(
2609
{IIII}f"Expected the element in the namespace {{NAMESPACE!r}}, "
2610
{IIII}f"but got the element {{tag_wo_ns!r}} without the namespace prefix"
2611
{III})
2612

2613
{I}return element.tag[len(_NAMESPACE_IN_CURLY_BRACKETS):]"""
2614
            ),
2615
            Stripped(
2616
                f"""\
2617
def _raise_if_has_tail_or_attrib(
2618
{II}element: Element
2619
) -> None:
2620
{I}\"\"\"
2621
{I}Check that :paramref:`element` has no trailing text and no attributes.
2622

2623
{I}:param element: to be verified
2624
{I}:raise:
2625
{II}:py:class:`.DeserializationException` if trailing text or attributes;
2626
{II}conforming to the convention about handling error paths,
2627
{II}the exception path is left empty.
2628
{I}\"\"\"
2629
{I}if element.tail is not None and len(element.tail.strip()) != 0:
2630
{II}raise DeserializationException(
2631
{III}f"Expected no trailing text, but got: {{element.tail!r}}"
2632
{II})
2633

2634
{I}if element.attrib is not None and len(element.attrib) > 0:
2635
{II}raise DeserializationException(
2636
{III}f"Expected no attributes, but got: {{element.attrib}}"
2637
{II})"""
2638
            ),
2639
            Stripped(
2640
                f"""\
2641
def _read_end_element(
2642
{II}element: Element,
2643
{II}iterator: Iterator[Tuple[str, Element]]
2644
) -> Element:
2645
{I}\"\"\"
2646
{I}Read the end element corresponding to the start :paramref:`element`
2647
{I}from :paramref:`iterator`.
2648

2649
{I}:param element: corresponding start element
2650
{I}:param iterator:
2651
{II}Input stream of ``(event, element)`` coming from
2652
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2653
{II}``events=["start", "end"]``
2654
{I}:raise: :py:class:`DeserializationException` if unexpected input
2655
{I}\"\"\"
2656
{I}next_event_element = next(iterator, None)
2657
{I}if next_event_element is None:
2658
{II}raise DeserializationException(
2659
{III}f"Expected the end element for {{element.tag}}, "
2660
{III}f"but got the end-of-input"
2661
{II})
2662

2663
{I}next_event, next_element = next_event_element
2664
{I}if next_event != "end" or next_element.tag != element.tag:
2665
{II}raise DeserializationException(
2666
{III}f"Expected the end element for {{element.tag!r}}, "
2667
{III}f"but got the event {{next_event!r}} and element {{next_element.tag!r}}"
2668
{II})
2669

2670
{I}_raise_if_has_tail_or_attrib(next_element)
2671

2672
{I}return next_element"""
2673
            ),
2674
            Stripped(
2675
                f"""\
2676
def _read_text_from_element(
2677
{I}element: Element,
2678
{I}iterator: Iterator[Tuple[str, Element]]
2679
) -> str:
2680
{I}\"\"\"
2681
{I}Extract the text from the :paramref:`element`, and read
2682
{I}the end element from :paramref:`iterator`.
2683

2684
{I}The :paramref:`element` is expected to contain text. Otherwise,
2685
{I}it is considered as unexpected input.
2686

2687
{I}:param element: start element enclosing the text
2688
{I}:param iterator:
2689
{II}Input stream of ``(event, element)`` coming from
2690
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2691
{II}``events=["start", "end"]``
2692
{I}:raise: :py:class:`DeserializationException` if unexpected input
2693
{I}\"\"\"
2694
{I}_raise_if_has_tail_or_attrib(element)
2695

2696
{I}text = element.text
2697

2698
{I}end_element = _read_end_element(
2699
{II}element,
2700
{II}iterator,
2701
{I})
2702

2703
{I}if text is None:
2704
{II}if end_element.text is None:
2705
{III}raise DeserializationException(
2706
{IIII}"Expected an element with text, but got an element with no text."
2707
{III})
2708

2709
{II}text = end_element.text
2710

2711
{I}return text"""
2712
            ),
2713
            Stripped(
2714
                f"""\
2715
_XS_BOOLEAN_LITERAL_SET = {{
2716
{I}"1",
2717
{I}"true",
2718
{I}"0",
2719
{I}"false",
2720
}}"""
2721
            ),
2722
            Stripped(
2723
                f"""\
2724
def _read_bool_from_element_text(
2725
{I}element: Element,
2726
{I}iterator: Iterator[Tuple[str, Element]]
2727
) -> bool:
2728
{I}\"\"\"
2729
{I}Parse the text of :paramref:`element` as a boolean, and
2730
{I}read the corresponding end element from :paramref:`iterator`.
2731

2732
{I}:param element: start element
2733
{I}:param iterator:
2734
{II}Input stream of ``(event, element)`` coming from
2735
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2736
{II}``events=["start", "end"]``
2737
{I}:raise: :py:class:`DeserializationException` if unexpected input
2738
{I}:return: parsed value
2739
{I}\"\"\"
2740
{I}text = _read_text_from_element(
2741
{II}element,
2742
{II}iterator
2743
{I})
2744

2745
{I}if text not in _XS_BOOLEAN_LITERAL_SET:
2746
{II}raise DeserializationException(
2747
{III}f"Expected a boolean, "
2748
{III}f"but got an element with text: {{text!r}}"
2749
{II})
2750

2751
{I}return text in ('1', 'true')"""
2752
            ),
2753
            Stripped(
2754
                f"""\
2755
def _read_int_from_element_text(
2756
{I}element: Element,
2757
{I}iterator: Iterator[Tuple[str, Element]]
2758
) -> int:
2759
{I}\"\"\"
2760
{I}Parse the text of :paramref:`element` as an integer, and
2761
{I}read the corresponding end element from :paramref:`iterator`.
2762

2763
{I}:param element: start element
2764
{I}:param iterator:
2765
{II}Input stream of ``(event, element)`` coming from
2766
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2767
{II}``events=["start", "end"]``
2768
{I}:raise: :py:class:`DeserializationException` if unexpected input
2769
{I}:return: parsed value
2770
{I}\"\"\"
2771
{I}text = _read_text_from_element(
2772
{II}element,
2773
{II}iterator
2774
{I})
2775

2776
{I}try:
2777
{II}value = int(text)
2778
{I}except ValueError:
2779
{II}# pylint: disable=raise-missing-from
2780
{II}raise DeserializationException(
2781
{III}f"Expected an integer, "
2782
{III}f"but got an element with text: {{text!r}}"
2783
{II})
2784

2785
{I}return value"""
2786
            ),
2787
            Stripped(
2788
                f"""\
2789
_TEXT_TO_XS_DOUBLE_LITERALS = {{
2790
{I}"NaN": math.nan,
2791
{I}"INF": math.inf,
2792
{I}"-INF": -math.inf,
2793
}}"""
2794
            ),
2795
            Stripped(
2796
                f"""\
2797
def _read_float_from_element_text(
2798
{I}element: Element,
2799
{I}iterator: Iterator[Tuple[str, Element]]
2800
) -> float:
2801
{I}\"\"\"
2802
{I}Parse the text of :paramref:`element` as a floating-point number, and
2803
{I}read the corresponding end element from :paramref:`iterator`.
2804

2805
{I}:param element: start element
2806
{I}:param iterator:
2807
{II}Input stream of ``(event, element)`` coming from
2808
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2809
{II}``events=["start", "end"]``
2810
{I}:raise: :py:class:`DeserializationException` if unexpected input
2811
{I}:return: parsed value
2812
{I}\"\"\"
2813
{I}text = _read_text_from_element(
2814
{II}element,
2815
{II}iterator
2816
{I})
2817

2818
{I}value = _TEXT_TO_XS_DOUBLE_LITERALS.get(text, None)
2819
{I}if value is None:
2820
{II}try:
2821
{III}value = float(text)
2822
{II}except ValueError:
2823
{III}# pylint: disable=raise-missing-from
2824
{III}raise DeserializationException(
2825
{IIII}f"Expected a floating-point number, "
2826
{IIII}f"but got an element with text: {{text!r}}"
2827
{III})
2828

2829
{I}return value"""
2830
            ),
2831
            Stripped(
2832
                f"""\
2833
def _read_str_from_element_text(
2834
{I}element: Element,
2835
{I}iterator: Iterator[Tuple[str, Element]]
2836
) -> str:
2837
{I}\"\"\"
2838
{I}Parse the text of :paramref:`element` as a string, and
2839
{I}read the corresponding end element from :paramref:`iterator`.
2840

2841
{I}If there is no text, empty string is returned.
2842

2843
{I}:param element: start element
2844
{I}:param iterator:
2845
{II}Input stream of ``(event, element)`` coming from
2846
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2847
{II}``events=["start", "end"]``
2848
{I}:raise: :py:class:`DeserializationException` if unexpected input
2849
{I}:return: parsed value
2850
{I}\"\"\"
2851
{I}# NOTE (mristin, 2022-10-26):
2852
{I}# We do not use ``_read_text_from_element`` as that function expects
2853
{I}# the ``element`` to contain *some* text. In contrast, this function
2854
{I}# can also deal with empty text, in which case it returns an empty string.
2855

2856
{I}text = element.text
2857

2858
{I}end_element = _read_end_element(
2859
{II}element,
2860
{II}iterator
2861
{I})
2862

2863
{I}if text is None:
2864
{II}text = end_element.text
2865

2866
{I}_raise_if_has_tail_or_attrib(element)
2867
{I}result = (
2868
{II}text
2869
{II}if text is not None
2870
{II}else ""
2871
{I})
2872

2873
{I}return result"""
2874
            ),
2875
            Stripped(
2876
                f"""\
2877
def _read_bytes_from_element_text(
2878
{I}element: Element,
2879
{I}iterator: Iterator[Tuple[str, Element]]
2880
) -> bytes:
2881
{I}\"\"\"
2882
{I}Parse the text of :paramref:`element` as base64-encoded bytes, and
2883
{I}read the corresponding end element from :paramref:`iterator`.
2884

2885
{I}:param element: look-ahead element
2886
{I}:param iterator:
2887
{II}Input stream of ``(event, element)`` coming from
2888
{II}:py:func:`xml.etree.ElementTree.iterparse` with the argument
2889
{II}``events=["start", "end"]``
2890
{I}:raise: :py:class:`DeserializationException` if unexpected input
2891
{I}:return: parsed value
2892
{I}\"\"\"
2893
{I}text = _read_text_from_element(
2894
{II}element,
2895
{II}iterator
2896
{I})
2897

2898
{I}try:
2899
{II}value = base64.b64decode(text)
2900
{I}except Exception:
2901
{II}# pylint: disable=raise-missing-from
2902
{II}raise DeserializationException(
2903
{III}f"Expected a text as base64-encoded bytes, "
2904
{III}f"but got an element with text: {{text!r}}"
2905
{II})
2906

2907
{I}return value"""
2908
            ),
2909
        ]
2910
    )
2911

2912
    for our_type in symbol_table.our_types:
5✔
2913
        if isinstance(our_type, intermediate.Enumeration):
5✔
2914
            blocks.append(_generate_read_enum_from_element_text(enumeration=our_type))
5✔
2915
        elif isinstance(our_type, intermediate.ConstrainedPrimitive):
5✔
2916
            continue
5✔
2917
        elif isinstance(our_type, intermediate.AbstractClass):
5✔
2918
            blocks.append(_generate_read_cls_as_element(cls=our_type))
5✔
2919

2920
        elif isinstance(our_type, intermediate.ConcreteClass):
5✔
2921
            if our_type.is_implementation_specific:
5✔
2922
                implementation_key = specific_implementations.ImplementationKey(
×
2923
                    f"Xmlization/read_{our_type.name}.py"
2924
                )
2925

2926
                implementation = spec_impls.get(implementation_key, None)
×
2927
                if implementation is None:
×
2928
                    errors.append(
×
2929
                        Error(
2930
                            our_type.parsed.node,
2931
                            f"The xmlization snippet is missing "
2932
                            f"for the implementation-specific "
2933
                            f"class {our_type.name}: {implementation_key}",
2934
                        )
2935
                    )
2936
                    continue
×
2937
            else:
2938
                blocks.extend(
5✔
2939
                    [
2940
                        _generate_reader_and_setter(cls=our_type),
2941
                        _generate_read_as_sequence(cls=our_type),
2942
                    ]
2943
                )
2944

2945
                blocks.append(_generate_read_cls_as_element(cls=our_type))
5✔
2946

2947
        else:
2948
            assert_never(our_type)
×
2949

2950
    blocks.append(_generate_general_read_as_element(symbol_table=symbol_table))
5✔
2951

2952
    for cls in symbol_table.classes:
5✔
2953
        if isinstance(cls, intermediate.AbstractClass):
5✔
2954
            blocks.append(_generate_dispatch_map_for_class(cls=cls))
5✔
2955
        elif isinstance(cls, intermediate.ConcreteClass):
5✔
2956
            if len(cls.concrete_descendants) > 0:
5✔
2957
                blocks.append(_generate_dispatch_map_for_class(cls=cls))
5✔
2958

2959
            if not cls.is_implementation_specific:
5✔
2960
                blocks.append(_generate_reader_and_setter_map(cls=cls))
5✔
2961

2962
        else:
2963
            assert_never(cls)
×
2964

2965
    blocks.append(_generate_general_dispatch_map(symbol_table=symbol_table))
5✔
2966

2967
    blocks.append(Stripped("# endregion"))
5✔
2968

2969
    blocks.append(Stripped("# region Serialization"))
5✔
2970

2971
    blocks.append(_generate_serializer(symbol_table=symbol_table))
5✔
2972

2973
    blocks.append(
5✔
2974
        _generate_write_to_stream(symbol_table=symbol_table, aas_module=aas_module)
2975
    )
2976

2977
    blocks.append(
5✔
2978
        Stripped(
2979
            f"""\
2980
def to_str(that: aas_types.Class) -> str:
2981
{I}\"\"\"
2982
{I}Serialize :paramref:`that` to an XML-encoded text.
2983

2984
{I}:param that: instance to be serialized
2985
{I}:return: :paramref:`that` serialized to XML serialized to text
2986
{I}\"\"\"
2987
{I}writer = io.StringIO()
2988
{I}write(that, writer)
2989
{I}return writer.getvalue()"""
2990
        )
2991
    )
2992

2993
    blocks.append(Stripped("# endregion"))
5✔
2994

2995
    writer = io.StringIO()
5✔
2996
    for i, block in enumerate(blocks):
5✔
2997
        if i > 0:
5✔
2998
            writer.write("\n\n\n")
5✔
2999

3000
        writer.write(block)
5✔
3001

3002
    writer.write("\n")
5✔
3003

3004
    return writer.getvalue(), None
5✔
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