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

MuellerSeb / nml-tools / 29529393017

16 Jul 2026 07:47PM UTC coverage: 86.054% (+0.01%) from 86.043%
29529393017

push

github

web-flow
Merge pull request #61 from MuellerSeb/template_derived_type_buffer_style

Add Buffer-Style Template Output For Simple Derived Types

75 of 86 new or added lines in 5 files covered. (87.21%)

4856 of 5643 relevant lines covered (86.05%)

0.86 hits per line

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

81.3
/src/nml_tools/codegen_template.py
1
"""Template namelist generation."""
2

3
from __future__ import annotations
1✔
4

5
from pathlib import Path
1✔
6
from typing import Any, Iterable
1✔
7

8
from ._utils import (
1✔
9
    normalize_constant_values,
10
    normalize_runtime_dimensions,
11
    reject_constant_dimension_overlap,
12
    validate_user_fortran_identifier,
13
)
14
from .codegen_fortran import (
1✔
15
    FieldTypeInfo,
16
    _array_default_value,
17
    _collect_dimension_constants,
18
    _derived_schema,
19
    _enum_values,
20
    _field_type_info,
21
    _format_scalar_default,
22
    _parse_default_dimensions,
23
    _reject_runtime_dimension_lengths,
24
)
25
from .schema import _is_simple_derived_schema
1✔
26
from .validate import validate_schema_defaults
1✔
27

28
_MISSING = object()
1✔
29

30

31
def generate_template(
1✔
32
    schemas: Iterable[dict[str, Any]],
33
    output: str | Path,
34
    *,
35
    doc_mode: str = "plain",
36
    value_mode: str = "empty",
37
    simple_derived_mode: str = "components",
38
    title: str | None = None,
39
    description: str | None = None,
40
    constants: dict[str, int] | None = None,
41
    dimensions: dict[str, int] | None = None,
42
    kind_map: dict[str, str] | None = None,
43
    kind_allowlist: set[str] | None = None,
44
    values: dict[str, dict[str, Any]] | None = None,
45
) -> None:
46
    """Generate a template namelist file for *schemas* at *output*."""
47
    rendered = render_template(
1✔
48
        schemas,
49
        doc_mode=doc_mode,
50
        value_mode=value_mode,
51
        simple_derived_mode=simple_derived_mode,
52
        title=title,
53
        description=description,
54
        constants=constants,
55
        dimensions=dimensions,
56
        kind_map=kind_map,
57
        kind_allowlist=kind_allowlist,
58
        values=values,
59
    )
60
    output_path = Path(output)
1✔
61
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
62
    output_path.write_text(rendered, encoding="ascii")
1✔
63

64

65
def render_template(
1✔
66
    schemas: Iterable[dict[str, Any]],
67
    *,
68
    doc_mode: str = "plain",
69
    value_mode: str = "empty",
70
    simple_derived_mode: str = "components",
71
    title: str | None = None,
72
    description: str | None = None,
73
    constants: dict[str, int] | None = None,
74
    dimensions: dict[str, int] | None = None,
75
    kind_map: dict[str, str] | None = None,
76
    kind_allowlist: set[str] | None = None,
77
    values: dict[str, dict[str, Any]] | None = None,
78
) -> str:
79
    """Render a template namelist file for *schemas*."""
80
    doc_mode = doc_mode.strip().lower()
1✔
81
    value_mode = value_mode.strip().lower()
1✔
82
    simple_derived_mode = simple_derived_mode.strip().lower()
1✔
83
    if doc_mode not in {"plain", "documented"}:
1✔
84
        raise ValueError("template doc_mode must be 'plain' or 'documented'")
×
85
    if value_mode not in {"empty", "filled", "minimal-empty", "minimal-filled"}:
1✔
86
        raise ValueError(
×
87
            "template value_mode must be one of: empty, filled, minimal-empty, minimal-filled"
88
        )
89
    if simple_derived_mode not in {"components", "buffer"}:
1✔
90
        raise ValueError("template simple_derived_mode must be 'components' or 'buffer'")
1✔
91

92
    return _render_template(
1✔
93
        schemas,
94
        doc_mode=doc_mode,
95
        value_mode=value_mode,
96
        simple_derived_mode=simple_derived_mode,
97
        title=title,
98
        description=description,
99
        constants=constants,
100
        dimensions=dimensions,
101
        kind_map=kind_map,
102
        kind_allowlist=kind_allowlist,
103
        values=values,
104
    )
105

106

107
def _render_template(
1✔
108
    schemas: Iterable[dict[str, Any]],
109
    *,
110
    doc_mode: str,
111
    value_mode: str,
112
    simple_derived_mode: str,
113
    title: str | None,
114
    description: str | None,
115
    constants: dict[str, int] | None,
116
    dimensions: dict[str, int] | None,
117
    kind_map: dict[str, str] | None,
118
    kind_allowlist: set[str] | None,
119
    values: dict[str, dict[str, Any]] | None,
120
) -> str:
121
    lines: list[str] = []
1✔
122
    schemas_list = list(schemas)
1✔
123
    values_map = values or {}
1✔
124
    if not isinstance(values_map, dict):
1✔
125
        raise ValueError("template values must be a table of namelist tables")
×
126
    constants = normalize_constant_values(constants)
1✔
127
    dimensions = normalize_runtime_dimensions(dimensions)
1✔
128
    reject_constant_dimension_overlap(constants, dimensions)
1✔
129
    shape_constants: dict[str, int] = {
1✔
130
        **constants,
131
        **dimensions,
132
    }
133

134
    schema_by_name: dict[str, dict[str, Any]] = {}
1✔
135
    for schema in schemas_list:
1✔
136
        validate_schema_defaults(schema, constants=constants, dimensions=dimensions)
1✔
137
        namelist_name = schema.get("x-fortran-namelist")
1✔
138
        if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
139
            raise ValueError("schema must define 'x-fortran-namelist'")
×
140
        validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
141
        schema_by_name[namelist_name] = schema
1✔
142

143
    for namelist_name, namelist_values in values_map.items():
1✔
144
        if not isinstance(namelist_name, str):
1✔
145
            raise ValueError("template values namelist names must be strings")
×
146
        if namelist_name not in schema_by_name:
1✔
147
            raise ValueError(f"template values namelist '{namelist_name}' not found in schemas")
×
148
        if not isinstance(namelist_values, dict):
1✔
149
            raise ValueError(f"template values for namelist '{namelist_name}' must be a table")
×
150
        properties = schema_by_name[namelist_name].get("properties")
1✔
151
        if not isinstance(properties, dict) or not properties:
1✔
152
            raise ValueError("schema must define object 'properties'")
×
153
        for key in namelist_values:
1✔
154
            if not isinstance(key, str):
1✔
155
                raise ValueError(
×
156
                    f"template values for namelist '{namelist_name}' must use string keys"
157
                )
158
            if key not in properties:
1✔
159
                raise ValueError(
×
160
                    f"template values for namelist '{namelist_name}' include unknown field '{key}'"
161
                )
162

163
    if doc_mode == "documented":
1✔
164
        _append_template_comment(lines, title)
1✔
165
        has_title = isinstance(title, str) and bool(title.strip())
1✔
166
        has_description = isinstance(description, str) and bool(description.strip())
1✔
167
        if has_title and has_description:
1✔
168
            lines.append("!")
1✔
169
        _append_template_comment(lines, description)
1✔
170
        if lines:
1✔
171
            lines.append("")
1✔
172

173
    for schema in schemas_list:
1✔
174
        namelist_name = schema.get("x-fortran-namelist")
1✔
175
        if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
176
            raise ValueError("schema must define 'x-fortran-namelist'")
×
177
        validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
178
        if schema.get("type") != "object":
1✔
179
            raise ValueError("schema root must be of type 'object'")
×
180
        properties = schema.get("properties")
1✔
181
        if not isinstance(properties, dict) or not properties:
1✔
182
            raise ValueError("schema must define object 'properties'")
×
183
        override_values = (
1✔
184
            values_map.get(namelist_name, {}) if value_mode in {"filled", "minimal-filled"} else {}
185
        )
186

187
        if doc_mode == "documented":
1✔
188
            title = schema.get("title")
1✔
189
            if isinstance(title, str) and title.strip():
1✔
190
                lines.append(f"! {title.strip()}")
1✔
191

192
        lines.append(f"&{namelist_name}")
1✔
193

194
        current_property: str | None = None
1✔
195
        try:
1✔
196
            for name, prop in properties.items():
1✔
197
                current_property = name
1✔
198
                if not isinstance(prop, dict):
1✔
199
                    raise ValueError(f"property '{name}' must be an object")
×
200
                _reject_runtime_dimension_lengths(prop, dimensions)
1✔
201
                type_info = _field_type_info(prop, constants)
1✔
202
                _collect_dimension_constants(type_info.dimensions, shape_constants)
1✔
203
                _validate_kind_allowlist(type_info, kind_map, kind_allowlist)
1✔
204

205
                if type_info.category == "array":
1✔
206
                    has_default = _array_default_value(prop) is not None
1✔
207
                else:
208
                    has_default = "default" in prop
1✔
209
                has_override = name in override_values
1✔
210
                if value_mode in {"minimal-empty", "minimal-filled"} and has_default:
1✔
211
                    if value_mode == "minimal-filled" and has_override:
1✔
212
                        pass
1✔
213
                    else:
214
                        continue
1✔
215

216
                if doc_mode == "documented":
1✔
217
                    title = prop.get("title")
1✔
218
                    if isinstance(title, str) and title.strip():
1✔
219
                        lines.append(f"  ! {title.strip()}")
1✔
220

221
                entries = _value_entries(
1✔
222
                    name,
223
                    prop,
224
                    type_info,
225
                    value_mode=value_mode,
226
                    simple_derived_mode=simple_derived_mode,
227
                    override=override_values.get(name, _MISSING),
228
                    constants=shape_constants,
229
                )
230
                derived = _derived_schema(prop)
1✔
231
                if (
1✔
232
                    doc_mode == "documented"
233
                    and simple_derived_mode == "buffer"
234
                    and derived is not None
235
                    and _is_simple_derived_schema(derived)
236
                    and entries
237
                ):
238
                    component_names = ", ".join(str(key) for key in derived["properties"])
1✔
239
                    lines.append(f"  ! Component order: {component_names}")
1✔
240
                for entry_name, value_text in entries:
1✔
241
                    if value_text is None:
1✔
242
                        lines.append(f"  {entry_name} =")
1✔
243
                    else:
244
                        lines.append(f"  {entry_name} = {value_text}")
1✔
245
        except ValueError as exc:
1✔
246
            if current_property is None:
1✔
247
                raise
×
248
            msg = str(exc)
1✔
249
            if f"property '{current_property}'" in msg:
1✔
250
                raise
×
251
            raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
252

253
        lines.append("/")
1✔
254
        lines.append("")
1✔
255

256
    if lines and lines[-1] == "":
1✔
257
        lines.pop()
1✔
258
    return "\n".join(lines) + "\n"
1✔
259

260

261
def _append_template_comment(lines: list[str], value: str | None) -> None:
1✔
262
    if not isinstance(value, str) or not value.strip():
1✔
263
        return
1✔
264
    for line in value.strip().splitlines():
1✔
265
        lines.append(f"! {line.strip()}" if line.strip() else "!")
1✔
266

267

268
def _array_slice(rank: int) -> str:
1✔
269
    return "(" + ",".join(":" for _ in range(rank)) + ")"
1✔
270

271

272
def _value_entries(
1✔
273
    name: str,
274
    prop: dict[str, Any],
275
    type_info: FieldTypeInfo,
276
    *,
277
    value_mode: str,
278
    simple_derived_mode: str,
279
    override: Any,
280
    constants: dict[str, int] | None,
281
) -> list[tuple[str, str | None]]:
282
    derived = _derived_schema(prop)
1✔
283
    if derived is not None:
1✔
284
        if simple_derived_mode == "buffer" and _is_simple_derived_schema(derived):
1✔
285
            return _derived_buffer_entries(
1✔
286
                name,
287
                derived,
288
                type_info,
289
                value_mode=value_mode,
290
                override=override,
291
                constants=constants,
292
            )
293
        return _derived_value_entries(
1✔
294
            name,
295
            prop,
296
            derived,
297
            type_info,
298
            value_mode=value_mode,
299
            override=override,
300
            constants=constants,
301
        )
302
    if value_mode in {"empty", "minimal-empty"}:
1✔
303
        entry_name = name
1✔
304
        if type_info.category == "array":
1✔
305
            entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
×
306
        return [(entry_name, None)]
1✔
307

308
    enum_values = _enum_values(prop, type_info, constants)
1✔
309

310
    if override is not _MISSING:
1✔
311
        return _entries_from_value(name, override, type_info, prop, constants)
1✔
312

313
    example_value = _get_first_example(prop, type_info)
1✔
314
    if example_value is not None:
1✔
315
        return _entries_from_value(name, example_value, type_info, prop, constants)
×
316

317
    if type_info.category == "array":
1✔
318
        array_default = _array_default_value(prop)
1✔
319
        if array_default is not None:
1✔
320
            default_value, default_from_items = array_default
1✔
321
            if default_from_items and isinstance(default_value, list):
1✔
322
                raise ValueError("array items default must be a scalar")
×
323
            if isinstance(default_value, list):
1✔
324
                return _array_list_entries(name, default_value, type_info, prop, constants)
1✔
325
            scalar = _format_scalar_default(
1✔
326
                default_value,
327
                None,
328
                type_info.element_category,
329
            )
330
            entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
1✔
331
            return [(entry_name, scalar)]
1✔
332
    elif "default" in prop:
1✔
333
        default_value = prop["default"]
1✔
334
        scalar = _format_scalar_default(default_value, None, type_info.category)
1✔
335
        return [(name, scalar)]
1✔
336

337
    if enum_values:
1✔
338
        return _entries_from_value(name, enum_values[0], type_info, prop, constants)
1✔
339

340
    if type_info.category == "array":
1✔
341
        scalar = _format_scalar_default(
1✔
342
            _fallback_scalar_value(type_info),
343
            None,
344
            type_info.element_category,
345
        )
346
        entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
1✔
347
        return [(entry_name, scalar)]
1✔
348

349
    scalar = _format_scalar_default(
1✔
350
        _fallback_scalar_value(type_info),
351
        None,
352
        type_info.category,
353
    )
354
    return [(name, scalar)]
1✔
355

356

357
def _derived_value_entries(
1✔
358
    name: str,
359
    prop: dict[str, Any],
360
    derived: dict[str, Any],
361
    type_info: FieldTypeInfo,
362
    *,
363
    value_mode: str,
364
    override: Any,
365
    constants: dict[str, int] | None,
366
) -> list[tuple[str, str | None]]:
367
    properties = derived.get("properties")
1✔
368
    if not isinstance(properties, dict) or not properties:
1✔
369
        raise ValueError(f"derived property '{name}' must define properties")
×
370
    is_array = type_info.category == "array"
1✔
371
    entries: list[tuple[str, str | None]] = []
1✔
372
    if is_array and override is not _MISSING:
1✔
373
        if not isinstance(override, list):
1✔
374
            raise ValueError(f"derived array template value '{name}' must be an array of tables")
×
375
        for index, value in enumerate(override, start=1):
1✔
376
            if not isinstance(value, dict):
1✔
377
                raise ValueError(
×
378
                    f"derived array template value '{name}[{index}]' must be a table"
379
                )
380
            value_map = _as_str_keyed_table(
1✔
381
                value,
382
                f"derived array template value '{name}[{index}]'",
383
            )
384
            entries.extend(
1✔
385
                _derived_component_entries(
386
                    f"{name}({index})",
387
                    properties,
388
                    value_mode=value_mode,
389
                    overrides=value_map,
390
                    constants=constants,
391
                )
392
            )
393
        return entries
1✔
394
    if override is not _MISSING and not isinstance(override, dict):
1✔
395
        raise ValueError(f"derived template value '{name}' must be a table")
×
396
    override_map: dict[str, Any] = {}
1✔
397
    if override is not _MISSING:
1✔
398
        override_map = _as_str_keyed_table(override, f"derived template value '{name}'")
1✔
399
    prefix = name
1✔
400
    if is_array:
1✔
401
        prefix = f"{name}{_array_slice(len(type_info.dimensions))}"
1✔
402
    return _derived_component_entries(
1✔
403
        prefix,
404
        properties,
405
        value_mode=value_mode,
406
        overrides=override_map,
407
        constants=constants,
408
    )
409

410

411
def _as_str_keyed_table(value: object, context: str) -> dict[str, Any]:
1✔
412
    if not isinstance(value, dict):
1✔
413
        raise ValueError(f"{context} must be a table")
×
414
    table: dict[str, Any] = {}
1✔
415
    for key, entry in value.items():
1✔
416
        if not isinstance(key, str):
1✔
417
            raise ValueError(f"{context} must use string keys")
1✔
418
        table[key] = entry
1✔
419
    return table
1✔
420

421

422
def _derived_component_entries(
1✔
423
    prefix: str,
424
    properties: dict[str, Any],
425
    *,
426
    value_mode: str,
427
    overrides: dict[str, Any],
428
    constants: dict[str, int] | None,
429
) -> list[tuple[str, str | None]]:
430
    for key in overrides:
1✔
431
        if key not in properties:
1✔
432
            raise ValueError(f"derived template value '{prefix}%{key}' is unknown")
×
433
    entries: list[tuple[str, str | None]] = []
1✔
434
    for child_name, child in properties.items():
1✔
435
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
436
            raise ValueError(f"derived property '{prefix}' components must be schema objects")
×
437
        child_type = _field_type_info(child, constants)
1✔
438
        entries.extend(
1✔
439
            _value_entries(
440
                f"{prefix}%{child_name}",
441
                child,
442
                child_type,
443
                value_mode=value_mode,
444
                simple_derived_mode="components",
445
                override=overrides.get(child_name, _MISSING),
446
                constants=constants,
447
            )
448
        )
449
    return entries
1✔
450

451

452
def _derived_buffer_entries(
1✔
453
    name: str,
454
    derived: dict[str, Any],
455
    type_info: FieldTypeInfo,
456
    *,
457
    value_mode: str,
458
    override: Any,
459
    constants: dict[str, int] | None,
460
) -> list[tuple[str, str | None]]:
461
    properties = derived.get("properties")
1✔
462
    if not isinstance(properties, dict) or not properties:
1✔
NEW
463
        raise ValueError(f"derived property '{name}' must define properties")
×
464

465
    is_array = type_info.category == "array"
1✔
466
    coordinates: list[list[int]] = [[]]
1✔
467
    if is_array:
1✔
468
        dimensions = _parse_default_dimensions(type_info.dimensions, constants)
1✔
469
        coordinates = [
1✔
470
            [index + 1 for index in coordinate]
471
            for coordinate in _iter_fixed_indices(dimensions, "F")
472
        ]
473

474
    if value_mode in {"empty", "minimal-empty"}:
1✔
475
        return [(_derived_element_name(name, coordinate), None) for coordinate in coordinates]
1✔
476

477
    override_maps: list[dict[str, Any]]
478
    if is_array and override is not _MISSING:
1✔
479
        if not isinstance(override, list):
1✔
NEW
480
            raise ValueError(f"derived array template value '{name}' must be an array of tables")
×
481
        if len(override) > len(coordinates):
1✔
NEW
482
            raise ValueError(f"derived array template value '{name}' is longer than its shape")
×
483
        coordinates = coordinates[: len(override)]
1✔
484
        override_maps = []
1✔
485
        for index, value in enumerate(override, start=1):
1✔
486
            override_maps.append(
1✔
487
                _as_str_keyed_table(
488
                    value,
489
                    f"derived array template value '{name}[{index}]'",
490
                )
491
            )
492
    else:
493
        if override is not _MISSING and not isinstance(override, dict):
1✔
NEW
494
            raise ValueError(f"derived template value '{name}' must be a table")
×
495
        override_map = (
1✔
496
            _as_str_keyed_table(override, f"derived template value '{name}'")
497
            if override is not _MISSING
498
            else {}
499
        )
500
        override_maps = [override_map for _ in coordinates]
1✔
501

502
    entries: list[tuple[str, str | None]] = []
1✔
503
    for coordinate, overrides in zip(coordinates, override_maps):
1✔
504
        prefix = _derived_element_name(name, coordinate)
1✔
505
        entries.append(
1✔
506
            (
507
                prefix,
508
                _derived_buffer_value(prefix, properties, overrides, value_mode, constants),
509
            )
510
        )
511
    return entries
1✔
512

513

514
def _derived_element_name(name: str, coordinate: list[int]) -> str:
1✔
515
    if not coordinate:
1✔
516
        return name
1✔
517
    return f"{name}({','.join(str(index) for index in coordinate)})"
1✔
518

519

520
def _derived_buffer_value(
1✔
521
    prefix: str,
522
    properties: dict[str, Any],
523
    overrides: dict[str, Any],
524
    value_mode: str,
525
    constants: dict[str, int] | None,
526
) -> str | None:
527
    for key in overrides:
1✔
528
        if key not in properties:
1✔
NEW
529
            raise ValueError(f"derived template value '{prefix}%{key}' is unknown")
×
530
    slots: list[str | None] = []
1✔
531
    for child_name, child in properties.items():
1✔
532
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
NEW
533
            raise ValueError(f"derived property '{prefix}' components must be schema objects")
×
534
        child_entries = _value_entries(
1✔
535
            child_name,
536
            child,
537
            _field_type_info(child, constants),
538
            value_mode=value_mode,
539
            simple_derived_mode="components",
540
            override=overrides.get(child_name, _MISSING),
541
            constants=constants,
542
        )
543
        if len(child_entries) != 1:
1✔
NEW
544
            raise ValueError(f"derived property '{prefix}%{child_name}' must be scalar")
×
545
        slots.append(child_entries[0][1])
1✔
546
    while slots and slots[-1] is None:
1✔
NEW
547
        slots.pop()
×
548
    if not slots:
1✔
NEW
549
        return None
×
550
    return ", ".join("" if slot is None else slot for slot in slots)
1✔
551

552

553
def _array_list_entries(
1✔
554
    name: str,
555
    values: list[Any],
556
    type_info: FieldTypeInfo,
557
    prop: dict[str, Any],
558
    constants: dict[str, int] | None,
559
) -> list[tuple[str, str | None]]:
560
    rank = len(type_info.dimensions)
1✔
561
    if rank <= 1:
1✔
562
        entry_name = f"{name}{_array_slice(rank)}"
1✔
563
        return [(entry_name, _format_value_list(values, type_info))]
1✔
564

565
    dims = _parse_default_dimensions(type_info.dimensions, constants)
1✔
566
    order = _resolve_default_order(prop)
1✔
567
    slice_dim = _slice_dim_for_order(rank, order)
1✔
568
    slice_size = dims[slice_dim]
1✔
569
    fixed_dims = [dims[idx] for idx in range(rank) if idx != slice_dim]
1✔
570
    entries: list[tuple[str, str | None]] = []
1✔
571
    offset = 0
1✔
572
    for fixed_indices in _iter_fixed_indices(fixed_dims, order):
1✔
573
        if offset >= len(values):
1✔
574
            break
×
575
        chunk = values[offset : offset + slice_size]
1✔
576
        if not chunk:
1✔
577
            break
×
578
        entry_name = _slice_entry_name(
1✔
579
            name,
580
            rank,
581
            slice_dim,
582
            [index + 1 for index in fixed_indices],
583
        )
584
        entries.append((entry_name, _format_value_list(chunk, type_info)))
1✔
585
        offset += slice_size
1✔
586
    if not entries:
1✔
587
        entry_name = f"{name}{_array_slice(rank)}"
×
588
        entries.append((entry_name, _format_value_list(values, type_info)))
×
589
    return entries
1✔
590

591

592
def _slice_dim_for_order(rank: int, order: str) -> int:
1✔
593
    if order == "F":
1✔
594
        return 0
1✔
595
    return rank - 1
×
596

597

598
def _iter_fixed_indices(dims: list[int], order: str) -> Iterable[list[int]]:
1✔
599
    if not dims:
1✔
600
        yield []
×
601
        return
×
602
    indices = [0] * len(dims)
1✔
603
    while True:
1✔
604
        yield indices.copy()
1✔
605
        positions = range(len(dims)) if order == "F" else range(len(dims) - 1, -1, -1)
1✔
606
        for pos in positions:
1✔
607
            indices[pos] += 1
1✔
608
            if indices[pos] < dims[pos]:
1✔
609
                break
1✔
610
            indices[pos] = 0
1✔
611
        else:
612
            break
1✔
613

614

615
def _slice_entry_name(
1✔
616
    name: str,
617
    rank: int,
618
    slice_dim: int,
619
    fixed_indices: list[int],
620
) -> str:
621
    indices: list[str] = []
1✔
622
    fixed_iter = iter(fixed_indices)
1✔
623
    for idx in range(rank):
1✔
624
        if idx == slice_dim:
1✔
625
            indices.append(":")
1✔
626
        else:
627
            indices.append(str(next(fixed_iter)))
1✔
628
    return f"{name}({','.join(indices)})"
1✔
629

630

631
def _format_value_list(values: list[Any], type_info: FieldTypeInfo) -> str:
1✔
632
    formatted = [
1✔
633
        _format_scalar_default(value, None, type_info.element_category) for value in values
634
    ]
635
    return ", ".join(formatted)
1✔
636

637

638
def _entries_from_value(
1✔
639
    name: str,
640
    value: Any,
641
    type_info: FieldTypeInfo,
642
    prop: dict[str, Any],
643
    constants: dict[str, int] | None,
644
) -> list[tuple[str, str | None]]:
645
    if type_info.category == "array":
1✔
646
        if isinstance(value, list):
×
647
            _validate_example_list(value)
×
648
            return _array_list_entries(name, value, type_info, prop, constants)
×
649
        scalar = _format_scalar_default(value, None, type_info.element_category)
×
650
        entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
×
651
        return [(entry_name, scalar)]
×
652
    if isinstance(value, list):
1✔
653
        raise ValueError("scalar template values must not be lists")
×
654
    scalar = _format_scalar_default(value, None, type_info.category)
1✔
655
    return [(name, scalar)]
1✔
656

657

658
def _get_first_example(prop: dict[str, Any], type_info: FieldTypeInfo) -> Any | None:
1✔
659
    examples = prop.get("examples")
1✔
660
    if examples is None:
1✔
661
        return None
1✔
662
    if not isinstance(examples, list):
×
663
        raise ValueError("property examples must be a list")
×
664
    if not examples:
×
665
        return None
×
666
    first = examples[0]
×
667
    if type_info.category == "array":
×
668
        if isinstance(first, list):
×
669
            _validate_example_list(first)
×
670
        return first
×
671
    if isinstance(first, list):
×
672
        raise ValueError("scalar examples must not be lists")
×
673
    return first
×
674

675

676
def _validate_example_list(values: list[Any]) -> None:
1✔
677
    for value in values:
×
678
        if isinstance(value, list):
×
679
            raise ValueError("array examples must be flat lists")
×
680

681

682
def _fallback_scalar_value(type_info: FieldTypeInfo) -> Any:
1✔
683
    category = type_info.element_category if type_info.category == "array" else type_info.category
1✔
684
    if category == "integer":
1✔
685
        return 0
1✔
686
    if category == "real":
1✔
687
        return 0.0
1✔
688
    if category == "boolean":
1✔
689
        return False
×
690
    if category == "string":
1✔
691
        return ""
1✔
692
    raise ValueError(f"unsupported template category '{category}'")
×
693

694

695
def _validate_kind_allowlist(
1✔
696
    type_info: FieldTypeInfo,
697
    kind_map: dict[str, str] | None,
698
    allowlist: set[str] | None,
699
) -> None:
700
    if type_info.kind is None or allowlist is None:
1✔
701
        return
1✔
702
    kind_id = type_info.kind
1✔
703
    mapped = None
1✔
704
    if kind_map is not None and kind_id in kind_map:
1✔
705
        mapped = kind_map[kind_id]
1✔
706
        if not isinstance(mapped, str) or not mapped:
1✔
707
            raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
708
        if mapped not in allowlist:
1✔
709
            raise ValueError(
×
710
                f"kind map target '{mapped}' for '{kind_id}' not present in kind module list"
711
            )
712
        return
1✔
713
    if kind_id not in allowlist:
×
714
        raise ValueError(f"kind '{kind_id}' not present in kind module list")
×
715

716

717
def _resolve_default_order(prop: dict[str, Any]) -> str:
1✔
718
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
719
    if not isinstance(order_raw, str):
1✔
720
        raise ValueError("array default order must be 'F' or 'C'")
×
721
    order = order_raw.upper()
1✔
722
    if order not in {"F", "C"}:
1✔
723
        raise ValueError("array default order must be 'F' or 'C'")
×
724
    return order
1✔
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