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

MuellerSeb / nml-tools / 29525046973

16 Jul 2026 06:41PM UTC coverage: 86.028% (+1.3%) from 84.697%
29525046973

Pull #60

github

MuellerSeb
Revert "Preserve trailing namelist null values"

This reverts commit 5fd338717.
Pull Request #60: Add A Schema-Aware Standard Fortran Namelist Parser

842 of 911 new or added lines in 4 files covered. (92.43%)

9 existing lines in 3 files now uncovered.

4778 of 5554 relevant lines covered (86.03%)

0.86 hits per line

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

77.33
/src/nml_tools/validate.py
1
"""Namelist validation utilities."""
2

3
from __future__ import annotations
1✔
4

5
import math
1✔
6
from dataclasses import dataclass
1✔
7
from typing import Any, Iterable, Mapping
1✔
8

9
from ._utils import (
1✔
10
    FORTRAN_IDENTIFIER,
11
    normalize_constant_values,
12
    normalize_runtime_dimensions,
13
    reject_constant_dimension_overlap,
14
    validate_user_fortran_identifier,
15
)
16

17

18
@dataclass(frozen=True)
1✔
19
class ScalarConstraints:
1✔
20
    category: str
1✔
21
    length: int | None
1✔
22
    enum_values: tuple[int | str, ...] | None
1✔
23
    enum_trimmed: tuple[str, ...] | None
1✔
24
    min_value: int | float | None
1✔
25
    max_value: int | float | None
1✔
26
    min_exclusive: bool
1✔
27
    max_exclusive: bool
1✔
28

29

30
def validate_schema_defaults(
1✔
31
    schema: Mapping[str, Any],
32
    *,
33
    constants: dict[str, int] | None = None,
34
    dimensions: dict[str, int] | None = None,
35
) -> None:
36
    """Validate operational defaults against each property's constraints."""
37
    if _has_reachable_reference(schema, position="root"):
1✔
38
        raise ValueError(
1✔
39
            "schema contains unresolved '$ref'; use load_schema() or resolve_schema() "
40
            "before validation or generation"
41
        )
42
    constants = normalize_constant_values(constants)
1✔
43
    dimensions = normalize_runtime_dimensions(dimensions)
1✔
44
    reject_constant_dimension_overlap(constants, dimensions)
1✔
45
    properties = schema.get("properties")
1✔
46
    if not isinstance(properties, Mapping):
1✔
47
        return
×
48
    required_names = {
1✔
49
        name.lower()
50
        for name in schema.get("required", [])
51
        if isinstance(name, str)
52
    }
53
    for name, prop in properties.items():
1✔
54
        if not isinstance(name, str) or not isinstance(prop, Mapping):
1✔
55
            continue
×
56
        _validate_derived_presence_policy(name, prop, name.lower() in required_names)
1✔
57
        _validate_property_defaults(
1✔
58
            name,
59
            prop,
60
            constants=constants,
61
            dimensions=dimensions,
62
        )
63

64

65
def _has_reachable_reference(raw: Mapping[str, Any], *, position: str) -> bool:
1✔
66
    if "$ref" in raw:
1✔
67
        return True
1✔
68
    if position == "root":
1✔
69
        properties = raw.get("properties")
1✔
70
        if isinstance(properties, Mapping):
1✔
71
            for prop in properties.values():
1✔
72
                if isinstance(prop, Mapping) and _has_reachable_reference(
1✔
73
                    prop, position="property"
74
                ):
75
                    return True
1✔
76
    items = raw.get("items")
1✔
77
    return isinstance(items, Mapping) and _has_reachable_reference(items, position="items")
1✔
78

79

80
def _validate_property_defaults(
1✔
81
    name: str,
82
    prop: Mapping[str, Any],
83
    *,
84
    constants: dict[str, int] | None,
85
    dimensions: dict[str, int] | None,
86
) -> None:
87
    if prop.get("type") == "object":
1✔
88
        _validate_derived_declaration(name, prop)
1✔
89
        if "default" in prop:
1✔
90
            raise ValueError(f"derived property '{name}' must not define an object default")
×
91
        properties = prop.get("properties")
1✔
92
        if not isinstance(properties, Mapping):
1✔
93
            raise ValueError(f"derived property '{name}' must define object 'properties'")
×
94
        for child_name, child in properties.items():
1✔
95
            if isinstance(child_name, str):
1✔
96
                validate_user_fortran_identifier(
1✔
97
                    child_name, label=f"derived property '{name}' component '{child_name}'"
98
                )
99
            if not isinstance(child_name, str) or not isinstance(child, Mapping) or child.get(
1✔
100
                "type"
101
            ) not in {"integer", "number", "boolean", "string"}:
102
                raise ValueError(
1✔
103
                    f"derived property '{name}' component '{child_name}' "
104
                    "must define an intrinsic scalar type"
105
                )
106
            _validate_property_defaults(
1✔
107
                f"{name}.{child_name}",
108
                child,
109
                constants=constants,
110
                dimensions=dimensions,
111
            )
112
        return
1✔
113

114
    if prop.get("type") != "array":
1✔
115
        if "default" in prop:
1✔
116
            _validate_scalar_default(
1✔
117
                name,
118
                prop,
119
                prop["default"],
120
                constants=constants,
121
                dimensions=dimensions,
122
            )
123
        return
1✔
124

125
    controls = {
1✔
126
        "x-fortran-default-order",
127
        "x-fortran-default-repeat",
128
        "x-fortran-default-pad",
129
    }
130
    if controls.intersection(prop) and "default" not in prop:
1✔
UNCOV
131
        raise ValueError(f"array property '{name}' default options require an array default")
×
132
    items = prop.get("items")
1✔
133
    if not isinstance(items, Mapping):
1✔
UNCOV
134
        if "default" in prop:
×
UNCOV
135
            raise ValueError(f"array property '{name}' with a default must define object 'items'")
×
136
        return
×
137
    if items.get("type") == "object":
1✔
138
        if "default" in prop or controls.intersection(prop):
1✔
139
            raise ValueError(f"derived array property '{name}' must not define defaults")
×
140
        if "x-fortran-flex-tail-dims" in prop:
1✔
141
            raise ValueError(
×
142
                f"derived array property '{name}' must not define x-fortran-flex-tail-dims"
143
            )
144
        _validate_property_defaults(
1✔
145
            f"{name}[]",
146
            items,
147
            constants=constants,
148
            dimensions=dimensions,
149
        )
150
        return
1✔
151
    if "default" in prop and "default" in items:
1✔
152
        raise ValueError(
×
153
            f"array property '{name}' default must be defined on property or items, not both"
154
        )
155
    has_default = "default" in prop or "default" in items
1✔
156
    if has_default:
1✔
157
        shape_constants = {**(constants or {}), **(dimensions or {})}
1✔
158
        shape = _parse_shape(prop.get("x-fortran-shape"), shape_constants, name)
1✔
159
        if _parse_flex_tail_dims(prop, len(shape), name, shape) > 0:
1✔
160
            raise ValueError(f"array property '{name}' flex arrays cannot define defaults")
1✔
161

162
    if "default" in prop:
1✔
163
        default = prop["default"]
1✔
164
        if not isinstance(default, list):
1✔
165
            raise ValueError(f"array default must be a list for property '{name}'")
1✔
166
        _validate_array_default_layout(name, prop, default, constants, dimensions)
1✔
167
        for value in _iter_scalars(default):
1✔
168
            _validate_scalar_default(
1✔
169
                name,
170
                items,
171
                value,
172
                constants=constants,
173
                dimensions=dimensions,
174
            )
175
    elif "default" in items:
1✔
176
        _validate_scalar_default(
1✔
177
            name,
178
            items,
179
            items["default"],
180
            constants=constants,
181
            dimensions=dimensions,
182
        )
183

184
    if "x-fortran-default-pad" in prop:
1✔
185
        pad = prop["x-fortran-default-pad"]
1✔
186
        pad_values = pad if isinstance(pad, list) else [pad]
1✔
187
        for value in _iter_scalars(pad_values):
1✔
188
            _validate_scalar_default(
1✔
189
                name,
190
                items,
191
                value,
192
                constants=constants,
193
                dimensions=dimensions,
194
            )
195

196

197
def _validate_scalar_default(
1✔
198
    name: str,
199
    prop: Mapping[str, Any],
200
    value: Any,
201
    *,
202
    constants: dict[str, int] | None,
203
    dimensions: dict[str, int] | None,
204
) -> None:
205
    category = prop.get("type")
1✔
206
    if category not in {"integer", "number", "boolean", "string"}:
1✔
NEW
207
        raise ValueError(f"property '{name}' has unsupported type '{category}'")
×
208
    constraints = _scalar_constraints(name, prop, category, constants, dimensions)
1✔
209
    _validate_scalar_value(name, value, constraints)
1✔
210

211

212
def _validate_derived_declaration(name: str, prop: Mapping[str, Any]) -> None:
1✔
213
    type_name = prop.get("x-fortran-type")
1✔
214
    if not isinstance(type_name, str) or not type_name.strip():
1✔
215
        raise ValueError(f"derived property '{name}' must define non-empty 'x-fortran-type'")
1✔
216
    try:
1✔
217
        validate_user_fortran_identifier(
1✔
218
            type_name.strip(), label=f"derived property '{name}' x-fortran-type"
219
        )
220
    except ValueError as exc:
1✔
221
        raise ValueError(str(exc).replace("Fortran identifier", "identifier")) from exc
1✔
222
    module_name = prop.get("x-fortran-module")
1✔
223
    if module_name is not None and (
1✔
224
        not isinstance(module_name, str) or not module_name.strip()
225
    ):
226
        raise ValueError(
×
227
            f"derived property '{name}' x-fortran-module must be a valid identifier"
228
        )
229
    if isinstance(module_name, str):
1✔
230
        try:
1✔
231
            validate_user_fortran_identifier(
1✔
232
                module_name.strip(), label=f"derived property '{name}' x-fortran-module"
233
            )
UNCOV
234
        except ValueError as exc:
×
UNCOV
235
            raise ValueError(str(exc).replace("Fortran identifier", "identifier")) from exc
×
236

237

238
def _validate_array_default_layout(
1✔
239
    name: str,
240
    prop: Mapping[str, Any],
241
    default: list[Any],
242
    constants: dict[str, int] | None,
243
    dimensions: dict[str, int] | None,
244
) -> None:
245
    if any(isinstance(value, list) for value in default):
1✔
246
        raise ValueError(f"array property '{name}' default must be a flat list")
×
247
    if not default:
1✔
248
        raise ValueError(f"array property '{name}' default must contain at least one value")
×
249
    shape_constants = {**(constants or {}), **(dimensions or {})}
1✔
250
    shape = _parse_shape(prop.get("x-fortran-shape"), shape_constants, name)
1✔
251
    if any(dimension is None for dimension in shape):
1✔
252
        raise ValueError(
×
253
            f"array property '{name}' defaults do not support deferred-size dimensions"
254
        )
255
    total_size = math.prod(dimension for dimension in shape if dimension is not None)
1✔
256
    if len(default) > total_size:
1✔
257
        raise ValueError(f"array property '{name}' default is longer than its shape")
×
258
    order = prop.get("x-fortran-default-order", "F")
1✔
259
    if not isinstance(order, str) or order.upper() not in {"F", "C"}:
1✔
260
        raise ValueError(f"array property '{name}' default order must be 'F' or 'C'")
×
261
    repeat = prop.get("x-fortran-default-repeat", False)
1✔
262
    if not isinstance(repeat, bool):
1✔
263
        raise ValueError(f"array property '{name}' default repeat must be boolean")
×
264
    pad = prop.get("x-fortran-default-pad")
1✔
265
    if pad is not None and repeat:
1✔
266
        raise ValueError(f"array property '{name}' default cannot set both pad and repeat")
×
267
    if isinstance(pad, list) and not pad:
1✔
268
        raise ValueError(f"array property '{name}' default pad must not be empty")
×
269
    if len(default) < total_size and pad is None and not repeat:
1✔
270
        raise ValueError(
1✔
271
            "array default shorter than declared x-fortran-shape without pad or repeat"
272
        )
273

274

275
def _normalize_properties(
1✔
276
    properties: Mapping[str, Any],
277
    namelist_name: str,
278
) -> dict[str, tuple[str, dict[str, Any]]]:
279
    normalized: dict[str, tuple[str, dict[str, Any]]] = {}
1✔
280
    for name, prop in properties.items():
1✔
281
        if not isinstance(name, str):
1✔
282
            raise ValueError(f"schema '{namelist_name}' property names must be strings")
×
283
        validate_user_fortran_identifier(
1✔
284
            name, label=f"schema '{namelist_name}' property '{name}'"
285
        )
286
        if not isinstance(prop, dict):
1✔
287
            raise ValueError(f"schema '{namelist_name}' property '{name}' must be an object")
×
288
        key = name.lower()
1✔
289
        if key in normalized:
1✔
290
            raise ValueError(
×
291
                f"schema '{namelist_name}' defines duplicate property '{name}'"
292
            )
293
        normalized[key] = (name, prop)
1✔
294
    return normalized
1✔
295

296

297
def _parse_required(
1✔
298
    raw: Any,
299
    properties: Mapping[str, tuple[str, dict[str, Any]]],
300
    namelist_name: str,
301
) -> set[str]:
302
    if raw is None:
1✔
303
        return set()
×
304
    if not isinstance(raw, list):
1✔
305
        raise ValueError(f"schema '{namelist_name}' required must be a list")
×
306
    required: set[str] = set()
1✔
307
    for item in raw:
1✔
308
        if not isinstance(item, str):
1✔
309
            raise ValueError(f"schema '{namelist_name}' required entries must be strings")
×
310
        key = item.lower()
1✔
311
        if key not in properties:
1✔
312
            raise ValueError(
×
313
                f"schema '{namelist_name}' required entry '{item}' is not a property"
314
            )
315
        required.add(key)
1✔
316
    return required
1✔
317

318

319
def _validate_derived_presence_policy(
1✔
320
    name: str,
321
    prop: Mapping[str, Any],
322
    is_required: bool,
323
) -> None:
324
    derived = _derived_object_schema(prop)
1✔
325
    if derived is None or is_required:
1✔
326
        return
1✔
327
    inner_required = derived.get("required", [])
1✔
328
    if isinstance(inner_required, list) and inner_required:
1✔
329
        raise ValueError(
1✔
330
            f"optional derived property '{name}' must not define required inner components"
331
        )
332

333

334
def _derived_object_schema(prop: Mapping[str, Any]) -> Mapping[str, Any] | None:
1✔
335
    if prop.get("type") == "object":
1✔
336
        return prop
1✔
337
    if prop.get("type") == "array":
1✔
338
        items = prop.get("items")
1✔
339
        if isinstance(items, Mapping) and items.get("type") == "object":
1✔
340
            return items
1✔
341
    return None
1✔
342

343

344
def _scalar_constraints(
1✔
345
    name: str,
346
    prop: Mapping[str, Any],
347
    category: str,
348
    constants: dict[str, int] | None,
349
    dimensions: dict[str, int] | None,
350
) -> ScalarConstraints:
351
    length = None
1✔
352
    if category == "string":
1✔
353
        length = _parse_length(prop, constants, dimensions, name)
1✔
354
    enum_values, enum_trimmed = _parse_enum(prop, category, length, name)
1✔
355
    min_value, min_exclusive = _extract_bound(prop, "minimum", "exclusiveMinimum", name)
1✔
356
    max_value, max_exclusive = _extract_bound(prop, "maximum", "exclusiveMaximum", name)
1✔
357
    if min_value is not None or max_value is not None:
1✔
358
        if category not in {"integer", "number"}:
1✔
359
            raise ValueError(f"property '{name}' bounds require integer or number")
×
360
        _validate_bound_scalar(min_value, category, name, "minimum")
1✔
361
        _validate_bound_scalar(max_value, category, name, "maximum")
1✔
362
        _validate_bound_range(min_value, max_value, min_exclusive, max_exclusive, name)
1✔
363
    return ScalarConstraints(
1✔
364
        category=category,
365
        length=length,
366
        enum_values=enum_values,
367
        enum_trimmed=enum_trimmed,
368
        min_value=min_value,
369
        max_value=max_value,
370
        min_exclusive=min_exclusive,
371
        max_exclusive=max_exclusive,
372
    )
373

374

375
def _parse_length(
1✔
376
    prop: Mapping[str, Any],
377
    constants: dict[str, int] | None,
378
    dimensions: dict[str, int] | None,
379
    name: str,
380
) -> int:
381
    raw = prop.get("x-fortran-len")
1✔
382
    if isinstance(raw, bool) or raw is None:
1✔
383
        raise ValueError(f"string property '{name}' must define 'x-fortran-len'")
×
384
    if isinstance(raw, int):
1✔
385
        if raw <= 0:
1✔
386
            raise ValueError(f"string property '{name}' length must be positive")
×
387
        return raw
1✔
388
    if isinstance(raw, str):
1✔
389
        token = raw.strip()
1✔
390
        if not token:
1✔
391
            raise ValueError(f"string property '{name}' length must be non-empty")
×
392
        if _is_int_literal(token):
1✔
393
            length = int(token)
×
394
            if length <= 0:
×
395
                raise ValueError(f"string property '{name}' length must be positive")
×
396
            return length
×
397
        if not FORTRAN_IDENTIFIER.match(token):
1✔
398
            raise ValueError(f"string property '{name}' length must be literal or identifier")
×
399
        token_key = token.lower()
1✔
400
        if dimensions is not None and token_key in dimensions:
1✔
UNCOV
401
            raise ValueError(
×
402
                f"string property '{name}' length must not use runtime dimension '{token}'"
403
            )
404
        if constants is None or token_key not in constants:
1✔
405
            raise ValueError(f"string property '{name}' length constant '{token}' not defined")
×
406
        value = constants[token_key]
1✔
407
        if isinstance(value, bool) or not isinstance(value, int):
1✔
408
            raise ValueError(f"string property '{name}' length constant '{token}' must be int")
×
409
        if value <= 0:
1✔
410
            raise ValueError(f"string property '{name}' length constant '{token}' must be positive")
×
411
        return value
1✔
412
    raise ValueError(f"string property '{name}' must define 'x-fortran-len'")
×
413

414

415
def _parse_enum(
1✔
416
    prop: Mapping[str, Any],
417
    category: str,
418
    length: int | None,
419
    name: str,
420
) -> tuple[tuple[int | str, ...] | None, tuple[str, ...] | None]:
421
    if "enum" not in prop:
1✔
422
        return None, None
1✔
423
    enum_raw = prop.get("enum")
1✔
424
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
425
        raise ValueError(f"property '{name}' enum must be a non-empty list")
×
426
    if category == "integer":
1✔
427
        values: list[int] = []
1✔
428
        for item in enum_raw:
1✔
429
            if isinstance(item, bool) or not isinstance(item, int):
1✔
430
                raise ValueError(f"property '{name}' enum values must be integers")
×
431
            values.append(item)
1✔
432
        return tuple(values), None
1✔
433
    if category == "string":
1✔
434
        values_str: list[str] = []
1✔
435
        trimmed: list[str] = []
1✔
436
        for item in enum_raw:
1✔
437
            if not isinstance(item, str):
1✔
438
                raise ValueError(f"property '{name}' enum values must be strings")
×
439
            if length is not None and len(item) > length:
1✔
440
                raise ValueError(f"property '{name}' enum value '{item}' exceeds length")
×
441
            values_str.append(item)
1✔
442
            trimmed.append(item.rstrip())
1✔
443
        return tuple(values_str), tuple(trimmed)
1✔
444
    raise ValueError(f"property '{name}' enum only supports strings or integers")
×
445

446

447
def _extract_bound(
1✔
448
    prop: Mapping[str, Any],
449
    inclusive_key: str,
450
    exclusive_key: str,
451
    name: str,
452
) -> tuple[int | float | None, bool]:
453
    has_inclusive = inclusive_key in prop
1✔
454
    has_exclusive = exclusive_key in prop
1✔
455
    if has_inclusive and has_exclusive:
1✔
456
        raise ValueError(
×
457
            f"property '{name}' must not define both '{inclusive_key}' and '{exclusive_key}'"
458
        )
459
    if has_exclusive:
1✔
460
        value = prop.get(exclusive_key)
×
461
        if value is None:
×
462
            raise ValueError(f"property '{name}' {exclusive_key} must be a number")
×
463
        return _ensure_number(value, name, exclusive_key), True
×
464
    if has_inclusive:
1✔
465
        value = prop.get(inclusive_key)
1✔
466
        if value is None:
1✔
467
            raise ValueError(f"property '{name}' {inclusive_key} must be a number")
×
468
        return _ensure_number(value, name, inclusive_key), False
1✔
469
    return None, False
1✔
470

471

472
def _ensure_number(value: object, name: str, label: str) -> int | float:
1✔
473
    if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
474
        raise ValueError(f"property '{name}' {label} must be a number")
×
475
    return value
1✔
476

477

478
def _validate_bound_scalar(
1✔
479
    value: int | float | None,
480
    category: str,
481
    name: str,
482
    label: str,
483
) -> None:
484
    if value is None:
1✔
485
        return
1✔
486
    if category == "integer":
1✔
487
        if isinstance(value, bool) or not isinstance(value, int):
1✔
488
            raise ValueError(f"property '{name}' {label} must be an integer")
×
489
        return
1✔
490
    if category == "number":
1✔
491
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
492
            raise ValueError(f"property '{name}' {label} must be a number")
×
493
        if math.isinf(float(value)):
1✔
494
            raise ValueError(f"property '{name}' {label} must not be infinite")
×
495
        if math.isnan(float(value)):
1✔
496
            raise ValueError(f"property '{name}' {label} must not be NaN")
×
497
        return
1✔
498
    raise ValueError(f"property '{name}' bounds only support integers or numbers")
×
499

500

501
def _validate_bound_range(
1✔
502
    min_value: int | float | None,
503
    max_value: int | float | None,
504
    min_exclusive: bool,
505
    max_exclusive: bool,
506
    name: str,
507
) -> None:
508
    if min_value is None or max_value is None:
1✔
509
        return
1✔
510
    min_comp = float(min_value)
1✔
511
    max_comp = float(max_value)
1✔
512
    if min_exclusive or max_exclusive:
1✔
513
        if min_comp >= max_comp:
×
514
            raise ValueError(f"property '{name}' minimum must be < maximum for exclusive bounds")
×
515
    else:
516
        if min_comp > max_comp:
1✔
517
            raise ValueError(f"property '{name}' minimum must be <= maximum")
×
518

519

520
def _validate_scalar_value(
1✔
521
    name: str,
522
    value: Any,
523
    constraints: ScalarConstraints,
524
) -> None:
525
    category = constraints.category
1✔
526
    if category == "integer":
1✔
527
        if isinstance(value, bool) or not isinstance(value, int):
1✔
528
            raise ValueError(f"property '{name}' must be an integer")
×
529
        if constraints.enum_values is not None and value not in constraints.enum_values:
1✔
530
            raise ValueError(f"property '{name}' has value outside enum")
1✔
531
        _validate_value_bounds(value, constraints, name)
1✔
532
        return
1✔
533
    if category == "number":
1✔
534
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
535
            raise ValueError(f"property '{name}' must be a number")
×
536
        if math.isnan(float(value)):
1✔
537
            raise ValueError(f"property '{name}' must not be NaN")
1✔
538
        if math.isinf(float(value)):
1✔
539
            raise ValueError(f"property '{name}' must not be infinite")
1✔
540
        _validate_value_bounds(float(value), constraints, name)
1✔
541
        return
1✔
542
    if category == "boolean":
1✔
543
        if not isinstance(value, bool):
1✔
544
            raise ValueError(f"property '{name}' must be boolean")
×
545
        return
1✔
546
    if category == "string":
1✔
547
        if not isinstance(value, str):
1✔
548
            raise ValueError(f"property '{name}' must be a string")
×
549
        if constraints.length is not None and len(value) > constraints.length:
1✔
550
            raise ValueError(f"property '{name}' exceeds length {constraints.length}")
1✔
551
        if constraints.enum_trimmed is not None:
1✔
552
            if value.rstrip() not in constraints.enum_trimmed:
1✔
553
                raise ValueError(f"property '{name}' has value outside enum")
×
554
        return
1✔
555
    raise ValueError(f"property '{name}' has unsupported type '{category}'")
×
556

557

558
def _validate_value_bounds(
1✔
559
    value: int | float,
560
    constraints: ScalarConstraints,
561
    name: str,
562
) -> None:
563
    if constraints.min_value is not None:
1✔
564
        if constraints.min_exclusive:
1✔
565
            if value <= constraints.min_value:
×
566
                raise ValueError(f"property '{name}' must be > {constraints.min_value}")
×
567
        else:
568
            if value < constraints.min_value:
1✔
569
                raise ValueError(f"property '{name}' must be >= {constraints.min_value}")
1✔
570
    if constraints.max_value is not None:
1✔
571
        if constraints.max_exclusive:
1✔
572
            if value >= constraints.max_value:
×
573
                raise ValueError(f"property '{name}' must be < {constraints.max_value}")
×
574
        else:
575
            if value > constraints.max_value:
1✔
576
                raise ValueError(f"property '{name}' must be <= {constraints.max_value}")
×
577

578

579
def _parse_shape(
1✔
580
    raw: Any,
581
    constants: dict[str, int] | None,
582
    name: str,
583
) -> list[int | None]:
584
    shape_entries: list[Any]
585
    if isinstance(raw, bool) or raw is None:
1✔
586
        raise ValueError(f"array property '{name}' must define non-empty x-fortran-shape")
×
587
    if isinstance(raw, (int, str)):
1✔
588
        shape_entries = [raw]
1✔
589
    elif isinstance(raw, list) and raw:
1✔
590
        shape_entries = raw
1✔
591
    else:
592
        raise ValueError(f"array property '{name}' must define non-empty x-fortran-shape")
×
593

594
    parsed: list[int | None] = []
1✔
595
    for dim in shape_entries:
1✔
596
        if isinstance(dim, bool):
1✔
597
            raise ValueError(f"array property '{name}' shape entries must be int or str")
×
598
        if isinstance(dim, int):
1✔
599
            if dim <= 0:
1✔
600
                raise ValueError(f"array property '{name}' shape values must be positive")
×
601
            parsed.append(dim)
1✔
602
            continue
1✔
603
        if isinstance(dim, str):
1✔
604
            token = dim.strip()
1✔
605
            if not token:
1✔
606
                raise ValueError(f"array property '{name}' shape entries must be non-empty")
×
607
            if token == ":":
1✔
608
                parsed.append(None)
1✔
609
                continue
1✔
610
            if _is_int_literal(token):
1✔
611
                size = int(token)
×
612
                if size <= 0:
×
613
                    raise ValueError(f"array property '{name}' shape values must be positive")
×
614
                parsed.append(size)
×
615
                continue
×
616
            if not FORTRAN_IDENTIFIER.match(token):
1✔
617
                raise ValueError(
×
618
                    f"array property '{name}' shape entries must be ints or identifiers"
619
                )
620
            token_key = token.lower()
1✔
621
            if constants is None or token_key not in constants:
1✔
622
                raise ValueError(f"array property '{name}' constant '{token}' not defined")
×
623
            value = constants[token_key]
1✔
624
            if isinstance(value, bool) or not isinstance(value, int):
1✔
625
                raise ValueError(f"array property '{name}' constant '{token}' must be int")
×
626
            if value <= 0:
1✔
627
                raise ValueError(f"array property '{name}' constant '{token}' must be positive")
×
628
            parsed.append(value)
1✔
629
            continue
1✔
630
        raise ValueError(f"array property '{name}' shape entries must be int or str")
×
631
    return parsed
1✔
632

633

634
def _parse_flex_tail_dims(
1✔
635
    prop: Mapping[str, Any],
636
    rank: int,
637
    name: str,
638
    dimensions: Iterable[int | None],
639
) -> int:
640
    raw = prop.get("x-fortran-flex-tail-dims")
1✔
641
    if raw is None:
1✔
642
        flex = 0
1✔
643
    else:
644
        if isinstance(raw, bool) or not isinstance(raw, int):
1✔
645
            raise ValueError(f"array property '{name}' flex tail dims must be an integer")
×
646
        flex = raw
1✔
647
    if flex < 0:
1✔
648
        raise ValueError(f"array property '{name}' flex tail dims must be >= 0")
×
649
    if flex == 0:
1✔
650
        return 0
1✔
651
    if flex > rank:
1✔
652
        raise ValueError(f"array property '{name}' flex tail dims must not exceed rank")
×
653
    if any(dim is None for dim in dimensions):
1✔
654
        raise ValueError(f"array property '{name}' flex tail dims require concrete shape")
×
655
    return flex
1✔
656

657

658
def _iter_scalars(value: Any) -> Iterable[Any]:
1✔
659
    if isinstance(value, (list, tuple)):
1✔
660
        for item in value:
1✔
661
            yield from _iter_scalars(item)
1✔
662
        return
1✔
663
    yield value
1✔
664

665

666
def _is_int_literal(value: str) -> bool:
1✔
667
    try:
1✔
668
        int(value)
1✔
669
    except ValueError:
1✔
670
        return False
1✔
671
    return True
×
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