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

aas-core-works / aas-core-codegen / 25155643253

30 Apr 2026 08:32AM UTC coverage: 83.973% (-0.03%) from 83.998%
25155643253

push

github

web-flow
Infer schema constraints for values (#618)

So far, we inferred the constraints on the class properties. As we want
to support lists of primitives, lists of constrained primitives *etc.*,
we need to infer the constraints at each atomic value of a type
annotation, not only at the property level.

We adapt the inference in this change to achieve that. It caused only
minor changes in the generated code -- since we do not limit ourselves
to properties only, and need a more general logic, some of the generated
artefacts had to slightly change.

378 of 430 new or added lines in 6 files covered. (87.91%)

13 existing lines in 3 files now uncovered.

29310 of 34904 relevant lines covered (83.97%)

3.36 hits per line

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

90.32
/aas_core_codegen/infer_for_schema/_inline.py
1
"""Merge constrained primitives as property constraints."""
2
import collections
4✔
3
import itertools
4✔
4
from typing import (
4✔
5
    Tuple,
6
    Optional,
7
    List,
8
    Mapping,
9
    MutableMapping,
10
    Sequence,
11
    Set,
12
    Union,
13
    Iterator,
14
)
15

16
from icontract import ensure
4✔
17

18
from aas_core_codegen import intermediate
4✔
19
from aas_core_codegen.common import Error, assert_never
4✔
20
from aas_core_codegen.infer_for_schema import (
4✔
21
    _len as infer_for_schema_len,
22
    _pattern as infer_for_schema_pattern,
23
    _set as infer_for_schema_set,
24
)
25
from aas_core_codegen.infer_for_schema._types import (
4✔
26
    Constraints,
27
    LenConstraint,
28
    PatternConstraint,
29
    SetOfPrimitivesConstraint,
30
    SetOfEnumerationLiteralsConstraint,
31
    ConstraintsByValue,
32
    MutableConstraintsByValue,
33
)
34

35

36
def _min_or_none(that: Optional[int], other: Optional[int]) -> Optional[int]:
4✔
37
    """Compute the minimum or return None if both values are None."""
38
    if that is not None and other is not None:
4✔
39
        return min(that, other)
4✔
40

41
    elif that is None and other is not None:
4✔
42
        return other
4✔
43

44
    elif that is not None and other is None:
4✔
45
        return that
4✔
46

NEW
47
    elif that is None and other is None:
×
NEW
48
        return None
×
49

50
    else:
NEW
51
        raise AssertionError("Unhandled execution path")
×
52

53

54
def _max_or_none(that: Optional[int], other: Optional[int]) -> Optional[int]:
4✔
55
    """Compute the maximum or return None if both values are None."""
56
    if that is not None and other is not None:
4✔
57
        return max(that, other)
4✔
58

59
    elif that is None and other is not None:
4✔
60
        return other
4✔
61

62
    elif that is not None and other is None:
4✔
63
        return that
4✔
64

NEW
65
    elif that is None and other is None:
×
NEW
66
        return None
×
67

68
    else:
NEW
69
        raise AssertionError("Unhandled execution path")
×
70

71

72
def _merge_len_constraints(
4✔
73
    that: Optional[LenConstraint], other: Optional[LenConstraint]
74
) -> Optional[LenConstraint]:
75
    if that is not None and other is not None:
4✔
76
        return LenConstraint(
4✔
77
            min_value=_max_or_none(that.min_value, other.min_value),
78
            max_value=_min_or_none(that.max_value, other.max_value),
79
        )
80

81
    elif that is not None and other is None:
4✔
82
        return that
4✔
83

84
    elif that is None and other is not None:
4✔
85
        return other
4✔
86

87
    elif that is None and other is None:
4✔
88
        return None
4✔
89

90
    else:
NEW
91
        raise AssertionError("Unhandled execution path")
×
92

93

94
def _merge_pattern_constraints(
4✔
95
    that: Optional[Sequence[PatternConstraint]],
96
    other: Optional[Sequence[PatternConstraint]],
97
) -> Optional[Sequence[PatternConstraint]]:
98
    if that is not None and other is not None:
4✔
99
        observed_pattern_set: Set[str] = set()
4✔
100

101
        result: List[PatternConstraint] = []
4✔
102

103
        for pattern_constraint in itertools.chain(that, other):
4✔
104
            if pattern_constraint.pattern not in observed_pattern_set:
4✔
105
                result.append(pattern_constraint)
4✔
106
                observed_pattern_set.add(pattern_constraint.pattern)
4✔
107

108
        return result
4✔
109

110
    elif that is not None and other is None:
4✔
111
        return that
4✔
112

113
    elif that is None and other is not None:
4✔
114
        return other
4✔
115

116
    elif that is None and other is None:
4✔
117
        return None
4✔
118

119
    else:
NEW
120
        raise AssertionError("Unhandled execution path")
×
121

122

123
def _merge_set_of_primitives_constraints(
4✔
124
    that: Optional[SetOfPrimitivesConstraint],
125
    other: Optional[SetOfPrimitivesConstraint],
126
) -> Optional[SetOfPrimitivesConstraint]:
127
    if that is not None and other is not None:
4✔
128
        if that.a_type != other.a_type:
4✔
NEW
129
            raise ValueError(
×
130
                "Constraints on sets of primitives of different primitive types "
131
                f"can not be merged together; that primitive type is {that.a_type}, "
132
                f"other primitive type is {other.a_type}."
133
            )
134

135
        value_histo: MutableMapping[
4✔
136
            Union[bool, int, float, str, bytearray], int
137
        ] = collections.OrderedDict()
138

139
        literal_by_value: MutableMapping[
4✔
140
            Union[bool, int, float, str, bytearray], intermediate.PrimitiveSetLiteral
141
        ] = {}
142

143
        for literal in itertools.chain(that.literals, other.literals):
4✔
144
            if literal.value not in value_histo:
4✔
145
                value_histo[literal.value] = 1
4✔
146
            else:
147
                value_histo[literal.value] += 1
4✔
148

149
            literal_by_value[literal.value] = literal
4✔
150

151
        return SetOfPrimitivesConstraint(
4✔
152
            # NOTE (mristin):
153
            # We simply pick one of the types as we assert before that that.a_type
154
            # and other.a_type must be the same.
155
            a_type=that.a_type,
156
            # NOTE (mristin):
157
            # We need to compute the intersection, so we only pick the literals which we
158
            # observed twice, once in each list of literals.
159
            literals=[
160
                literal_by_value[value]
161
                for value, count in value_histo.items()
162
                if count == 2
163
            ],
164
        )
165

166
    elif that is not None and other is None:
4✔
167
        return that
4✔
168

169
    elif that is None and other is not None:
4✔
170
        return other
4✔
171

172
    elif that is None and other is None:
4✔
173
        return None
4✔
174

175
    else:
NEW
176
        raise AssertionError("Unhandled execution path")
×
177

178

179
def _merge_set_of_enumeration_literals_constraints(
4✔
180
    that: Optional[SetOfEnumerationLiteralsConstraint],
181
    other: Optional[SetOfEnumerationLiteralsConstraint],
182
) -> Optional[SetOfEnumerationLiteralsConstraint]:
183
    if that is not None and other is not None:
4✔
184
        if that.enumeration is not other.enumeration:
4✔
NEW
185
            raise ValueError(
×
186
                "Constraints on sets of enumeration literals of different enumerations "
187
                f"can not be merged together; that enumeration is {that.enumeration}, "
188
                f"other enumeration is {other.enumeration}."
189
            )
190

191
        literal_by_id: MutableMapping[int, intermediate.EnumerationLiteral] = dict()
4✔
192

193
        literal_id_histo: MutableMapping[int, int] = collections.OrderedDict()
4✔
194

195
        for literal in itertools.chain(that.literals, other.literals):
4✔
196
            literal_id = id(literal)
4✔
197

198
            if literal_id not in literal_id_histo:
4✔
199
                literal_id_histo[literal_id] = 1
4✔
200
            else:
201
                literal_id_histo[literal_id] += 1
4✔
202

203
            literal_by_id[literal_id] = literal
4✔
204

205
        return SetOfEnumerationLiteralsConstraint(
4✔
206
            # NOTE (mristin):
207
            # We simply pick one of the enumerations as we assert before that
208
            # enumeration and other enumeration are one and the same.
209
            enumeration=that.enumeration,
210
            # NOTE (mristin):
211
            # We need to compute the intersection, so we only pick the literals which we
212
            # observed twice, once in each list of literals.
213
            literals=[
214
                literal_by_id[literal_id]
215
                for literal_id, count in literal_id_histo.items()
216
                if count == 2
217
            ],
218
        )
219

220
    elif that is not None and other is None:
4✔
NEW
221
        return that
×
222

223
    elif that is None and other is not None:
4✔
NEW
224
        return other
×
225

226
    elif that is None and other is None:
4✔
227
        return None
4✔
228

229
    else:
NEW
230
        raise AssertionError("Unhandled execution path")
×
231

232

233
def _merge_constraints(
4✔
234
    that: Optional[Constraints], other: Optional[Constraints]
235
) -> Optional[Constraints]:
236
    """Combine the constraints on tighter bounds."""
237
    if that is not None and other is not None:
4✔
238
        return Constraints(
4✔
239
            len_constraint=_merge_len_constraints(
240
                that.len_constraint, other.len_constraint
241
            ),
242
            patterns=_merge_pattern_constraints(that.patterns, other.patterns),
243
            set_of_primitives=_merge_set_of_primitives_constraints(
244
                that.set_of_primitives, other.set_of_primitives
245
            ),
246
            set_of_enumeration_literals=_merge_set_of_enumeration_literals_constraints(
247
                that.set_of_enumeration_literals, other.set_of_enumeration_literals
248
            ),
249
        )
250

251
    elif that is not None and other is None:
4✔
252
        return that
4✔
253

254
    elif that is None and other is not None:
4✔
255
        return other
4✔
256

257
    elif that is None and other is None:
4✔
258
        return None
4✔
259

260
    else:
NEW
261
        raise AssertionError("Unhandled execution path")
×
262

263

264
def _infer_constraints_of_constrained_primitive_without_inheritance(
4✔
265
    constrained_primitive: intermediate.ConstrainedPrimitive,
266
    pattern_verifications_by_name: infer_for_schema_pattern.PatternVerificationsByName,
267
) -> Tuple[Optional[Constraints], Optional[List[Error]]]:
268
    """
269
    Infer the constraints from the invariants on self of the constrained primitive.
270

271
    We do not go up (or down) the inheritance tree -- the constraints from the parents
272
    are not inherited at this step.
273

274
    If there are no constraints inferred from the constrained primitive, we return None.
275
    """
276
    errors = []  # type: List[Error]
4✔
277

278
    len_constraint: Optional[LenConstraint] = None
4✔
279

280
    if constrained_primitive.constrainee in infer_for_schema_len.LENGTHABLE_PRIMITIVES:
4✔
281
        (
4✔
282
            len_constraint,
283
            len_constraint_errors,
284
        ) = infer_for_schema_len.infer_len_constraint_of_self(
285
            constrained_primitive=constrained_primitive
286
        )
287

288
        if len_constraint_errors is not None:
4✔
289
            errors.extend(len_constraint_errors)
4✔
290
        else:
291
            assert len_constraint is not None
4✔
292

293
            # NOTE (mristin):
294
            # We do not want to keep dummy constraints.
295
            if len_constraint.min_value is None and len_constraint.max_value is None:
4✔
296
                len_constraint = None
4✔
297

298
    pattern_constraints: Optional[Sequence[PatternConstraint]] = None
4✔
299

300
    if constrained_primitive.constrainee is intermediate.PrimitiveType.STR:
4✔
301
        pattern_constraints = infer_for_schema_pattern.infer_patterns_on_self(
4✔
302
            constrained_primitive=constrained_primitive,
303
            pattern_verifications_by_name=pattern_verifications_by_name,
304
        )
305

306
        if len(pattern_constraints) == 0:
4✔
307
            # NOTE (mristin):
308
            # We do not want to keep dummy constraints.
309
            pattern_constraints = None
4✔
310

311
    if len(errors) > 0:
4✔
312
        return None, errors
4✔
313

314
    if len_constraint is None and pattern_constraints is None:
4✔
315
        return None, None
4✔
316

317
    return (
4✔
318
        Constraints(
319
            len_constraint=len_constraint,
320
            patterns=pattern_constraints,
321
            # NOTE (mristin):
322
            # We do not match the set of primitives on the constrained primitives at the
323
            # moment, but this could be easily implemented.
324
            set_of_primitives=None,
325
            set_of_enumeration_literals=None,
326
        ),
327
        None,
328
    )
329

330

331
@ensure(lambda result: not (result[1] is not None) or len(result[1]) > 0)
4✔
332
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
333
def _infer_constraints_by_constrained_primitive(
4✔
334
    symbol_table: intermediate.SymbolTable,
335
    pattern_verifications_by_name: infer_for_schema_pattern.PatternVerificationsByName,
336
) -> Tuple[
337
    Optional[MutableMapping[intermediate.ConstrainedPrimitive, Constraints]],
338
    Optional[List[Error]],
339
]:
340
    """
341
    Infer the constraints from the constrained primitives considering the inheritance.
342

343
    If there are no constraints for the given constrained primitive, it is not present
344
    in the mapping.
345
    """
346
    errors: List[Error] = []
4✔
347

348
    mapping: MutableMapping[intermediate.ConstrainedPrimitive, Constraints] = dict()
4✔
349

350
    # NOTE (mristin):
351
    # We perform the first pass where we disregard inheritance and return if there are
352
    # any errors. We can then be certain that there will be no errors when we stack
353
    # the constraints considering the inheritance tree.
354

355
    for constrained_primitive in symbol_table.constrained_primitives:
4✔
356
        (
4✔
357
            constraints,
358
            errors_constrained_primitive,
359
        ) = _infer_constraints_of_constrained_primitive_without_inheritance(
360
            constrained_primitive=constrained_primitive,
361
            pattern_verifications_by_name=pattern_verifications_by_name,
362
        )
363

364
        if errors_constrained_primitive is not None:
4✔
365
            errors.append(
4✔
366
                Error(
367
                    constrained_primitive.parsed.node,
368
                    f"Failed to infer the schema constraints "
369
                    f"from constrained primitive {constrained_primitive.name!r}",
370
                    errors_constrained_primitive,
371
                )
372
            )
373

374
        if constraints is not None:
4✔
375
            mapping[constrained_primitive] = constraints
4✔
376

377
    if len(errors) > 0:
4✔
378
        return None, errors
4✔
379

380
    for our_type in symbol_table.our_types_topologically_sorted:
4✔
381
        if not isinstance(our_type, intermediate.ConstrainedPrimitive):
4✔
382
            continue
4✔
383

384
        # NOTE (mristin):
385
        # We rename for clarity when the reader reads the previous code.
386
        constrained_primitive = our_type
4✔
387

388
        constraints = mapping.get(constrained_primitive, None)
4✔
389

390
        # NOTE (mristin):
391
        # The topological order ensures that we have processed the parents already.
392
        for parent in constrained_primitive.inheritances:
4✔
393
            # NOTE (mristin):
394
            # The constrained primitive inherits all the constraints from the parent,
395
            # and then it might tighten them some more.
396
            constraints = _merge_constraints(mapping.get(parent, None), constraints)
4✔
397

398
        if constraints is not None:
4✔
399
            mapping[constrained_primitive] = constraints
4✔
400

401
    return mapping, None
4✔
402

403

404
def _over_non_optional_type_annotations(
4✔
405
    type_annotation: intermediate.TypeAnnotationUnion,
406
) -> Iterator[intermediate.TypeAnnotationExceptOptional]:
407
    """
408
    Iterate recursively over the type annotation and all its nested type annotations.
409

410
    The optional type annotations are recursed into, but will not be yielded.
411
    """
412
    if not isinstance(type_annotation, intermediate.OptionalTypeAnnotation):
4✔
413
        yield type_annotation
4✔
414

415
    if isinstance(type_annotation, intermediate.OptionalTypeAnnotation):
4✔
416
        yield from _over_non_optional_type_annotations(type_annotation.value)
4✔
417

418
    elif isinstance(type_annotation, intermediate.ListTypeAnnotation):
4✔
419
        yield from _over_non_optional_type_annotations(type_annotation.items)
4✔
420

421
    elif isinstance(
4✔
422
        type_annotation,
423
        (intermediate.PrimitiveTypeAnnotation, intermediate.OurTypeAnnotation),
424
    ):
425
        pass
4✔
426

427
    else:
428
        # noinspection PyTypeChecker
NEW
429
        assert_never(type_annotation)
×
430

431

432
@ensure(
4✔
433
    lambda result: not (result[0] is not None)
434
    or (all(not constraints.is_empty() for constraints in result[0].values()))
435
)
436
@ensure(lambda result: not (result[1] is not None) or len(result[1]) >= 1)
4✔
437
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
438
def _infer_constraints_of_class_values_without_inheritance(
4✔
439
    cls: intermediate.ClassUnion,
440
    constraints_by_constrained_primitive: Mapping[
441
        intermediate.ConstrainedPrimitive, Constraints
442
    ],
443
    pattern_verifications_by_name: infer_for_schema_pattern.PatternVerificationsByName,
444
    symbol_table: intermediate.SymbolTable,
445
) -> Tuple[Optional[MutableConstraintsByValue], Optional[List[Error]],]:
446
    """
447
    Infer the constraints on all the possible values of the class.
448

449
    The class hierarchy is not taken into account; we consider only the invariants
450
    defined for this class.
451
    """
452
    errors: List[Error] = []
4✔
453

454
    mapping: MutableConstraintsByValue = dict()
4✔
455

456
    # region Constraints on length
457
    (
4✔
458
        len_constraints_from_invariants,
459
        some_errors,
460
    ) = infer_for_schema_len.len_constraints_from_invariants(cls=cls)
461

462
    if some_errors is not None:
4✔
463
        errors.extend(some_errors)
4✔
464
    else:
465
        assert len_constraints_from_invariants is not None
4✔
466

467
        for prop, len_constraint in len_constraints_from_invariants.items():
4✔
468
            if len_constraint.min_value is None and len_constraint.max_value is None:
4✔
NEW
469
                continue
×
470

471
            type_anno = intermediate.beneath_optional(prop.type_annotation)
4✔
472

473
            merged_constraints = _merge_constraints(
4✔
474
                mapping.get(type_anno, None), Constraints(len_constraint=len_constraint)
475
            )
476

477
            if merged_constraints is not None:
4✔
478
                mapping[type_anno] = merged_constraints
4✔
479

480
    # endregion
481

482
    # region Pattern constraints
483

484
    patterns_from_invariants_by_property = (
4✔
485
        infer_for_schema_pattern.patterns_from_invariants(
486
            cls=cls,
487
            pattern_verifications_by_name=pattern_verifications_by_name,
488
        )
489
    )
490

491
    for prop, pattern_constraints in patterns_from_invariants_by_property.items():
4✔
492
        if len(pattern_constraints) == 0:
4✔
NEW
493
            continue
×
494

495
        type_anno = intermediate.beneath_optional(prop.type_annotation)
4✔
496

497
        merged_constraints = _merge_constraints(
4✔
498
            mapping.get(type_anno, None), Constraints(patterns=pattern_constraints)
499
        )
500

501
        if merged_constraints is not None:
4✔
502
            mapping[type_anno] = merged_constraints
4✔
503

504
    # endregion
505

506
    # region Constraints from constant sets
507

508
    # fmt: off
509
    set_constraints, some_errors = (
4✔
510
        infer_for_schema_set.infer_set_constraints_by_property_from_invariants(
511
            cls=cls,
512
            symbol_table=symbol_table
513
        )
514
    )
515
    # fmt: on
516

517
    if some_errors is not None:
4✔
NEW
518
        errors.extend(some_errors)
×
519
    else:
520
        assert set_constraints is not None
4✔
521

522
        for (
4✔
523
            prop,
524
            set_of_primitives_constraint,
525
        ) in set_constraints.set_of_primitives_by_property.items():
526
            type_anno = intermediate.beneath_optional(prop.type_annotation)
4✔
527

528
            merged_constraints = _merge_constraints(
4✔
529
                mapping.get(type_anno, None),
530
                Constraints(set_of_primitives=set_of_primitives_constraint),
531
            )
532

533
            if merged_constraints is not None:
4✔
534
                mapping[type_anno] = merged_constraints
4✔
535

536
        for (
4✔
537
            prop,
538
            set_of_enumeration_literals_constraint,
539
        ) in set_constraints.set_of_enumeration_literals_by_property.items():
540
            type_anno = intermediate.beneath_optional(prop.type_annotation)
4✔
541

542
            merged_constraints = _merge_constraints(
4✔
543
                mapping.get(type_anno, None),
544
                Constraints(
545
                    set_of_enumeration_literals=set_of_enumeration_literals_constraint
546
                ),
547
            )
548

549
            if merged_constraints is not None:
4✔
550
                mapping[type_anno] = merged_constraints
4✔
551

552
    # endregion
553

554
    if len(errors) > 0:
4✔
555
        return None, errors
4✔
556

557
    # region Patterns from constrained primitives
558

559
    for prop in cls.properties:
4✔
560
        for type_anno in _over_non_optional_type_annotations(prop.type_annotation):
4✔
561
            if not isinstance(type_anno, intermediate.OurTypeAnnotation):
4✔
562
                continue
4✔
563

564
            if not isinstance(type_anno.our_type, intermediate.ConstrainedPrimitive):
4✔
565
                continue
4✔
566

567
            constraints = _merge_constraints(
4✔
568
                mapping.get(type_anno, None),
569
                constraints_by_constrained_primitive.get(type_anno.our_type, None),
570
            )
571

572
            if constraints is not None and not constraints.is_empty():
4✔
573
                mapping[type_anno] = constraints
4✔
574

575
    # endregion
576

577
    assert len(errors) == 0
4✔
578
    return mapping, None
4✔
579

580

581
@ensure(lambda result: not (result[1] is not None) or len(result[1]) > 0)
4✔
582
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
583
def infer_constraints_by_class(
4✔
584
    symbol_table: intermediate.SymbolTable,
585
) -> Tuple[
586
    Optional[Mapping[intermediate.ClassUnion, ConstraintsByValue]],
587
    Optional[List[Error]],
588
]:
589
    """Infer the constraints from the invariants and constrained primitives."""
590
    errors = []  # type: List[Error]
4✔
591

592
    pattern_verifications_by_name = (
4✔
593
        infer_for_schema_pattern.map_pattern_verifications_by_name(
594
            verifications=symbol_table.verification_functions
595
        )
596
    )
597

598
    (
4✔
599
        constraints_by_constrained_primitive,
600
        some_errors,
601
    ) = _infer_constraints_by_constrained_primitive(
602
        symbol_table=symbol_table,
603
        pattern_verifications_by_name=pattern_verifications_by_name,
604
    )
605

606
    if some_errors is not None:
4✔
607
        errors.extend(some_errors)
4✔
608

609
    if len(errors) > 0:
4✔
610
        return None, errors
4✔
611

612
    assert constraints_by_constrained_primitive is not None
4✔
613

614
    mapping: MutableMapping[intermediate.ClassUnion, MutableConstraintsByValue] = dict()
4✔
615

616
    for cls in symbol_table.classes:
4✔
617
        (
4✔
618
            constraints_by_value,
619
            some_errors,
620
        ) = _infer_constraints_of_class_values_without_inheritance(
621
            cls=cls,
622
            constraints_by_constrained_primitive=constraints_by_constrained_primitive,
623
            pattern_verifications_by_name=pattern_verifications_by_name,
624
            symbol_table=symbol_table,
625
        )
626

627
        if some_errors is not None:
4✔
628
            errors.extend(some_errors)
4✔
629
            continue
4✔
630

631
        assert constraints_by_value is not None
4✔
632

633
        mapping[cls] = constraints_by_value
4✔
634

635
    if len(errors) > 0:
4✔
636
        return None, errors
4✔
637

638
    # NOTE (mristin):
639
    # We stack now the constraints considering the class hierarchy. The topological
640
    # order guarantees that the parents have been processed before the children.
641
    for our_type in symbol_table.our_types_topologically_sorted:
4✔
642
        if not isinstance(
4✔
643
            our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
644
        ):
645
            continue
4✔
646

647
        # NOTE (mristin):
648
        # We introduce the alias for better readability with the previous section.
649
        cls = our_type
4✔
650

651
        that_constraints_by_value = mapping[cls]
4✔
652

653
        for parent in cls.inheritances:
4✔
654
            parent_constraints_by_value = mapping[parent]
4✔
655

656
            # NOTE (mristin):
657
            # The class inherits all the constraints from the parent, and then it might
658
            # tighten them some more.
659
            for type_anno, parent_constraints in parent_constraints_by_value.items():
4✔
660
                merged_constraints = _merge_constraints(
4✔
661
                    parent_constraints,
662
                    that_constraints_by_value.get(type_anno, None),
663
                )
664

665
                if merged_constraints is not None:
4✔
666
                    that_constraints_by_value[type_anno] = merged_constraints
4✔
667

668
    return mapping, None
4✔
669

670

671
def tightening_steps_from_other_to_that_constraints(
4✔
672
    that: Constraints, other: Optional[Constraints]
673
) -> Optional[Constraints]:
674
    """
675
    Identify constraints to be updated to go from ``other`` to ``that``.
676

677
    This function is used to spot tightening of the constraints in the children
678
    classes which override the parents' constraints.
679

680
    We assume that the in-lining and merging of the constraints has been already
681
    performed before. Any constraints in the ``other`` which differs from ``that``
682
    constraint is assumed to be "overridden" (*i.e.*, tightened) by ``that`` constraint.
683

684
    You can think of the resulting constraint list as all the constraints that a child
685
    class (corresponding to ``that``) imposes in addition to constraints that are already
686
    imposed by the parent class (corresponding to ``other``).
687
    """
688
    if other is None:
4✔
NEW
689
        return that
×
690

691
    len_constraint: Optional[LenConstraint] = None
4✔
692

693
    if other.len_constraint is not None:
4✔
694
        assert that.len_constraint is not None, (
4✔
695
            "The ``other``, corresponding to a parent class, defines a length constraint, "
696
            "but the child class, corresponding to ``that``, relaxes the constraint and "
697
            "defines no length constraints. This breaks the behavioral subtyping. "
698
            "Have the constraints been properly merged and inlined?\n"
699
            f"{other.len_constraint=}"
700
        )
701

702
        len_constraint = (
4✔
703
            that.len_constraint
704
            if not that.len_constraint.equals(other.len_constraint)
705
            else None
706
        )
707
    else:
708
        len_constraint = that.len_constraint
4✔
709

710
    patterns: Optional[Sequence[PatternConstraint]] = None
4✔
711

712
    if other.patterns is not None:
4✔
713
        assert that.patterns is not None, (
4✔
714
            "The ``other``, corresponding to a parent class, defines patterns, "
715
            "but the child class, corresponding to ``that``, relaxes the constraint and "
716
            "defines no patterns. This breaks the behavioral subtyping. "
717
            "Have the constraints been properly merged and inlined?\n"
718
            f"{other.patterns=}, {that.patterns=}"
719
        )
720

721
        other_pattern_set = set(
4✔
722
            pattern_constraint.pattern for pattern_constraint in other.patterns
723
        )
724

725
        that_pattern_set = set(
4✔
726
            pattern_constraint.pattern for pattern_constraint in that.patterns
727
        )
728

729
        assert other_pattern_set.intersection(that_pattern_set) == other_pattern_set, (
4✔
730
            "The ``other``, corresponding to a parent class, defines more patterns "
731
            "than the child class, corresponding to ``that`` -- this amounts to relaxing "
732
            "constraints. This breaks the behavioral subtyping. "
733
            "Have the constraints been properly merged and inlined?\n"
734
            f"{other.patterns=}, {that.patterns=}"
735
        )
736

737
        # NOTE (mristin):
738
        # We select only patterns which are not already defined in ``other``, as
739
        # the ``other`` corresponds to the parent class.
740
        patterns = [
4✔
741
            pattern_constraint
742
            for pattern_constraint in that.patterns
743
            if pattern_constraint.pattern not in other_pattern_set
744
        ]
745

746
        if len(patterns) == 0:
4✔
747
            patterns = None
4✔
748
    else:
749
        patterns = that.patterns
4✔
750

751
    set_of_primitives: Optional[SetOfPrimitivesConstraint] = None
4✔
752

753
    if other.set_of_primitives is not None:
4✔
754
        assert that.set_of_primitives is not None, (
4✔
755
            "The ``other``, corresponding to a parent class, defines set of primitives, "
756
            "but the child class, corresponding to ``that``, relaxes the constraint and "
757
            "defines no set of primitives. This breaks the behavioral subtyping. "
758
            "Have the constraints been properly merged and inlined?\n"
759
            f"{other.set_of_primitives=}"
760
        )
761

762
        other_set_of_primitives = set(
4✔
763
            literal.value for literal in other.set_of_primitives.literals
764
        )
765

766
        that_set_of_primitives = set(
4✔
767
            literal.value for literal in that.set_of_primitives.literals
768
        )
769

770
        assert (
4✔
771
            that_set_of_primitives.intersection(other_set_of_primitives)
772
            == that_set_of_primitives
773
        ), (
774
            "The ``that``, corresponding to a child class, can only tighten the set of "
775
            "allowed primitives from ``other``, which corresponds to the parent class. "
776
            "However, ``that`` defines more literals -- this breaks the behavioral "
777
            "subtyping. Have the constraints been properly merged and inlined?\n"
778
            f"{other.set_of_primitives=}, {that.set_of_primitives=}"
779
        )
780

781
        if not that.set_of_primitives.equals(other.set_of_primitives):
4✔
NEW
782
            set_of_primitives = that.set_of_primitives
×
783

784
    else:
785
        set_of_primitives = that.set_of_primitives
4✔
786

787
    set_of_enumeration_literals: Optional[SetOfEnumerationLiteralsConstraint] = None
4✔
788

789
    if other.set_of_enumeration_literals is not None:
4✔
NEW
790
        assert that.set_of_enumeration_literals is not None, (
×
791
            "The ``other``, corresponding to a parent class, defines set of "
792
            "enumeration literals, but the child class, corresponding to ``that``, "
793
            "relaxes the constraint and defines no set of enumeration literals. This "
794
            "breaks the behavioral subtyping. Have the constraints been properly merged "
795
            "and inlined?\n"
796
            f"{other.set_of_enumeration_literals=}"
797
        )
798

NEW
799
        other_set_of_enumeration_literals = set(
×
800
            literal.value for literal in other.set_of_enumeration_literals.literals
801
        )
802

NEW
803
        that_set_of_enumeration_literals = set(
×
804
            literal.value for literal in that.set_of_enumeration_literals.literals
805
        )
806

NEW
807
        assert (
×
808
            that_set_of_enumeration_literals.intersection(
809
                other_set_of_enumeration_literals
810
            )
811
            == that_set_of_enumeration_literals
812
        ), (
813
            "The ``that``, corresponding to a child class, can only tighten the set of "
814
            "allowed enumeration literals from ``other``, which corresponds to "
815
            "the parent class. However, ``that`` defines more literals -- this breaks "
816
            "the behavioral subtyping. Have the constraints been properly merged and "
817
            "inlined?\n"
818
            f"{other.set_of_enumeration_literals=}, {that.set_of_enumeration_literals=}"
819
        )
820

NEW
821
        if not that.set_of_enumeration_literals.equals(
×
822
            other.set_of_enumeration_literals
823
        ):
NEW
824
            set_of_enumeration_literals = that.set_of_enumeration_literals
×
825
    else:
826
        set_of_enumeration_literals = that.set_of_enumeration_literals
4✔
827

828
    tightening = Constraints(
4✔
829
        len_constraint=len_constraint,
830
        patterns=patterns,
831
        set_of_primitives=set_of_primitives,
832
        set_of_enumeration_literals=set_of_enumeration_literals,
833
    )
834

835
    return tightening
4✔
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