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

MuellerSeb / nml-tools / 29619529631

17 Jul 2026 11:01PM UTC coverage: 86.477% (+0.4%) from 86.054%
29619529631

Pull #63

github

web-flow
Merge b3777c4e5 into 6717357e4
Pull Request #63: Allow defaults to satisfy required values

270 of 287 new or added lines in 7 files covered. (94.08%)

3 existing lines in 1 file now uncovered.

5039 of 5827 relevant lines covered (86.48%)

0.86 hits per line

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

83.21
/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 (
1✔
27
    analyze_property_requirement,
28
    derived_object_default,
29
    validate_schema_defaults,
30
)
31

32
_MISSING = object()
1✔
33

34

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

68

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

96
    return _render_template(
1✔
97
        schemas,
98
        doc_mode=doc_mode,
99
        value_mode=value_mode,
100
        simple_derived_mode=simple_derived_mode,
101
        title=title,
102
        description=description,
103
        constants=constants,
104
        dimensions=dimensions,
105
        kind_map=kind_map,
106
        kind_allowlist=kind_allowlist,
107
        values=values,
108
    )
109

110

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

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

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

167
    if doc_mode == "documented":
1✔
168
        _append_template_comment(lines, title)
1✔
169
        has_title = isinstance(title, str) and bool(title.strip())
1✔
170
        has_description = isinstance(description, str) and bool(description.strip())
1✔
171
        if has_title and has_description:
1✔
172
            lines.append("!")
1✔
173
        _append_template_comment(lines, description)
1✔
174
        if lines:
1✔
175
            lines.append("")
1✔
176

177
    for schema in schemas_list:
1✔
178
        namelist_name = schema.get("x-fortran-namelist")
1✔
179
        if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
180
            raise ValueError("schema must define 'x-fortran-namelist'")
×
181
        validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
182
        if schema.get("type") != "object":
1✔
183
            raise ValueError("schema root must be of type 'object'")
×
184
        properties = schema.get("properties")
1✔
185
        if not isinstance(properties, dict) or not properties:
1✔
186
            raise ValueError("schema must define object 'properties'")
×
187
        required_raw = schema.get("required", [])
1✔
188
        if not isinstance(required_raw, list):
1✔
NEW
189
            raise ValueError("schema 'required' must be a list")
×
190
        required_names = {
1✔
191
            value.lower() for value in required_raw if isinstance(value, str)
192
        }
193
        override_values = (
1✔
194
            values_map.get(namelist_name, {}) if value_mode in {"filled", "minimal-filled"} else {}
195
        )
196

197
        if doc_mode == "documented":
1✔
198
            title = schema.get("title")
1✔
199
            if isinstance(title, str) and title.strip():
1✔
200
                lines.append(f"! {title.strip()}")
1✔
201

202
        lines.append(f"&{namelist_name}")
1✔
203

204
        current_property: str | None = None
1✔
205
        try:
1✔
206
            for name, prop in properties.items():
1✔
207
                current_property = name
1✔
208
                if not isinstance(prop, dict):
1✔
209
                    raise ValueError(f"property '{name}' must be an object")
×
210
                _reject_runtime_dimension_lengths(prop, dimensions)
1✔
211
                type_info = _field_type_info(prop, constants)
1✔
212
                _collect_dimension_constants(type_info.dimensions, shape_constants)
1✔
213
                _validate_kind_allowlist(type_info, kind_map, kind_allowlist)
1✔
214

215
                requirement = analyze_property_requirement(
1✔
216
                    name,
217
                    prop,
218
                    declared_required=name.lower() in required_names,
219
                )
220
                has_default = requirement.fully_default_initialized
1✔
221
                has_override = name in override_values
1✔
222
                if value_mode in {"minimal-empty", "minimal-filled"} and has_default:
1✔
223
                    if value_mode == "minimal-filled" and has_override:
1✔
224
                        pass
1✔
225
                    else:
226
                        continue
1✔
227

228
                if doc_mode == "documented":
1✔
229
                    title = prop.get("title")
1✔
230
                    if isinstance(title, str) and title.strip():
1✔
231
                        lines.append(f"  ! {title.strip()}")
1✔
232

233
                entries = _value_entries(
1✔
234
                    name,
235
                    prop,
236
                    type_info,
237
                    value_mode=value_mode,
238
                    simple_derived_mode=simple_derived_mode,
239
                    override=override_values.get(name, _MISSING),
240
                    constants=shape_constants,
241
                )
242
                derived = _derived_schema(prop)
1✔
243
                if (
1✔
244
                    doc_mode == "documented"
245
                    and simple_derived_mode == "buffer"
246
                    and derived is not None
247
                    and _is_simple_derived_schema(derived)
248
                    and entries
249
                ):
250
                    component_names = ", ".join(str(key) for key in derived["properties"])
1✔
251
                    lines.append(f"  ! Component order: {component_names}")
1✔
252
                for entry_name, value_text in entries:
1✔
253
                    if value_text is None:
1✔
254
                        lines.append(f"  {entry_name} =")
1✔
255
                    else:
256
                        lines.append(f"  {entry_name} = {value_text}")
1✔
257
        except ValueError as exc:
1✔
258
            if current_property is None:
1✔
259
                raise
×
260
            msg = str(exc)
1✔
261
            if f"property '{current_property}'" in msg:
1✔
262
                raise
×
263
            raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
264

265
        lines.append("/")
1✔
266
        lines.append("")
1✔
267

268
    if lines and lines[-1] == "":
1✔
269
        lines.pop()
1✔
270
    return "\n".join(lines) + "\n"
1✔
271

272

273
def _append_template_comment(lines: list[str], value: str | None) -> None:
1✔
274
    if not isinstance(value, str) or not value.strip():
1✔
275
        return
1✔
276
    for line in value.strip().splitlines():
1✔
277
        lines.append(f"! {line.strip()}" if line.strip() else "!")
1✔
278

279

280
def _array_slice(rank: int) -> str:
1✔
281
    return "(" + ",".join(":" for _ in range(rank)) + ")"
1✔
282

283

284
def _value_entries(
1✔
285
    name: str,
286
    prop: dict[str, Any],
287
    type_info: FieldTypeInfo,
288
    *,
289
    value_mode: str,
290
    simple_derived_mode: str,
291
    override: Any,
292
    constants: dict[str, int] | None,
293
    fallback_default: Any = _MISSING,
294
) -> list[tuple[str, str | None]]:
295
    derived = _derived_schema(prop)
1✔
296
    if derived is not None:
1✔
297
        if simple_derived_mode == "buffer" and _is_simple_derived_schema(derived):
1✔
298
            return _derived_buffer_entries(
1✔
299
                name,
300
                derived,
301
                type_info,
302
                value_mode=value_mode,
303
                override=override,
304
                constants=constants,
305
            )
306
        return _derived_value_entries(
1✔
307
            name,
308
            prop,
309
            derived,
310
            type_info,
311
            value_mode=value_mode,
312
            override=override,
313
            constants=constants,
314
        )
315
    if value_mode in {"empty", "minimal-empty"}:
1✔
316
        entry_name = name
1✔
317
        if type_info.category == "array":
1✔
318
            entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
×
319
        return [(entry_name, None)]
1✔
320

321
    enum_values = _enum_values(prop, type_info, constants)
1✔
322

323
    if override is not _MISSING:
1✔
324
        return _entries_from_value(name, override, type_info, prop, constants)
1✔
325

326
    example_value = _get_first_example(prop, type_info)
1✔
327
    if example_value is not None:
1✔
328
        return _entries_from_value(name, example_value, type_info, prop, constants)
1✔
329

330
    if fallback_default is not _MISSING:
1✔
331
        return _entries_from_value(name, fallback_default, type_info, prop, constants)
1✔
332

333
    if type_info.category == "array":
1✔
334
        array_default = _array_default_value(prop)
1✔
335
        if array_default is not None:
1✔
336
            default_value, default_from_items = array_default
1✔
337
            if default_from_items and isinstance(default_value, list):
1✔
338
                raise ValueError("array items default must be a scalar")
×
339
            if isinstance(default_value, list):
1✔
340
                return _array_list_entries(name, default_value, type_info, prop, constants)
1✔
341
            scalar = _format_scalar_default(
1✔
342
                default_value,
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
    elif "default" in prop:
1✔
349
        default_value = prop["default"]
1✔
350
        scalar = _format_scalar_default(default_value, None, type_info.category)
1✔
351
        return [(name, scalar)]
1✔
352

353
    if enum_values:
1✔
354
        return _entries_from_value(name, enum_values[0], type_info, prop, constants)
1✔
355

356
    if type_info.category == "array":
1✔
357
        scalar = _format_scalar_default(
1✔
358
            _fallback_scalar_value(type_info),
359
            None,
360
            type_info.element_category,
361
        )
362
        entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
1✔
363
        return [(entry_name, scalar)]
1✔
364

365
    scalar = _format_scalar_default(
1✔
366
        _fallback_scalar_value(type_info),
367
        None,
368
        type_info.category,
369
    )
370
    return [(name, scalar)]
1✔
371

372

373
def _derived_value_entries(
1✔
374
    name: str,
375
    prop: dict[str, Any],
376
    derived: dict[str, Any],
377
    type_info: FieldTypeInfo,
378
    *,
379
    value_mode: str,
380
    override: Any,
381
    constants: dict[str, int] | None,
382
) -> list[tuple[str, str | None]]:
383
    properties = derived.get("properties")
1✔
384
    if not isinstance(properties, dict) or not properties:
1✔
385
        raise ValueError(f"derived property '{name}' must define properties")
×
386
    is_array = type_info.category == "array"
1✔
387
    object_default = derived_object_default(name, prop)
1✔
388
    entries: list[tuple[str, str | None]] = []
1✔
389
    if is_array and override is not _MISSING:
1✔
390
        if not isinstance(override, list):
1✔
391
            raise ValueError(f"derived array template value '{name}' must be an array of tables")
×
392
        for index, value in enumerate(override, start=1):
1✔
393
            if not isinstance(value, dict):
1✔
394
                raise ValueError(
×
395
                    f"derived array template value '{name}[{index}]' must be a table"
396
                )
397
            value_map = _as_str_keyed_table(
1✔
398
                value,
399
                f"derived array template value '{name}[{index}]'",
400
            )
401
            entries.extend(
1✔
402
                _derived_component_entries(
403
                    f"{name}({index})",
404
                    properties,
405
                    value_mode=value_mode,
406
                    overrides=value_map,
407
                    object_default=object_default,
408
                    constants=constants,
409
                )
410
            )
411
        return entries
1✔
412
    if override is not _MISSING and not isinstance(override, dict):
1✔
413
        raise ValueError(f"derived template value '{name}' must be a table")
×
414
    override_map: dict[str, Any] = {}
1✔
415
    if override is not _MISSING:
1✔
416
        override_map = _as_str_keyed_table(override, f"derived template value '{name}'")
1✔
417
    prefix = name
1✔
418
    if is_array:
1✔
419
        prefix = f"{name}{_array_slice(len(type_info.dimensions))}"
1✔
420
    return _derived_component_entries(
1✔
421
        prefix,
422
        properties,
423
        value_mode=value_mode,
424
        overrides=override_map,
425
        object_default=object_default,
426
        constants=constants,
427
    )
428

429

430
def _as_str_keyed_table(value: object, context: str) -> dict[str, Any]:
1✔
431
    if not isinstance(value, dict):
1✔
432
        raise ValueError(f"{context} must be a table")
×
433
    table: dict[str, Any] = {}
1✔
434
    for key, entry in value.items():
1✔
435
        if not isinstance(key, str):
1✔
436
            raise ValueError(f"{context} must use string keys")
1✔
437
        table[key] = entry
1✔
438
    return table
1✔
439

440

441
def _derived_component_entries(
1✔
442
    prefix: str,
443
    properties: dict[str, Any],
444
    *,
445
    value_mode: str,
446
    overrides: dict[str, Any],
447
    object_default: dict[str, Any],
448
    constants: dict[str, int] | None,
449
) -> list[tuple[str, str | None]]:
450
    for key in overrides:
1✔
451
        if key not in properties:
1✔
452
            raise ValueError(f"derived template value '{prefix}%{key}' is unknown")
×
453
    entries: list[tuple[str, str | None]] = []
1✔
454
    for child_name, child in properties.items():
1✔
455
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
456
            raise ValueError(f"derived property '{prefix}' components must be schema objects")
×
457
        child_type = _field_type_info(child, constants)
1✔
458
        entries.extend(
1✔
459
            _value_entries(
460
                f"{prefix}%{child_name}",
461
                child,
462
                child_type,
463
                value_mode=value_mode,
464
                simple_derived_mode="components",
465
                override=overrides.get(child_name, _MISSING),
466
                constants=constants,
467
                fallback_default=object_default.get(child_name.lower(), _MISSING),
468
            )
469
        )
470
    return entries
1✔
471

472

473
def _derived_buffer_entries(
1✔
474
    name: str,
475
    derived: dict[str, Any],
476
    type_info: FieldTypeInfo,
477
    *,
478
    value_mode: str,
479
    override: Any,
480
    constants: dict[str, int] | None,
481
) -> list[tuple[str, str | None]]:
482
    properties = derived.get("properties")
1✔
483
    if not isinstance(properties, dict) or not properties:
1✔
484
        raise ValueError(f"derived property '{name}' must define properties")
×
485

486
    is_array = type_info.category == "array"
1✔
487
    object_default = derived_object_default(name, derived)
1✔
488
    coordinates: list[list[int]] = [[]]
1✔
489
    if is_array:
1✔
490
        dimensions = _parse_default_dimensions(type_info.dimensions, constants)
1✔
491
        coordinates = [
1✔
492
            [index + 1 for index in coordinate]
493
            for coordinate in _iter_fixed_indices(dimensions, "F")
494
        ]
495

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

499
    override_maps: list[dict[str, Any]]
500
    if is_array and override is not _MISSING:
1✔
501
        if not isinstance(override, list):
1✔
502
            raise ValueError(f"derived array template value '{name}' must be an array of tables")
×
503
        if len(override) > len(coordinates):
1✔
504
            raise ValueError(f"derived array template value '{name}' is longer than its shape")
×
505
        coordinates = coordinates[: len(override)]
1✔
506
        override_maps = []
1✔
507
        for index, value in enumerate(override, start=1):
1✔
508
            override_maps.append(
1✔
509
                _as_str_keyed_table(
510
                    value,
511
                    f"derived array template value '{name}[{index}]'",
512
                )
513
            )
514
    else:
515
        if override is not _MISSING and not isinstance(override, dict):
1✔
516
            raise ValueError(f"derived template value '{name}' must be a table")
×
517
        override_map = (
1✔
518
            _as_str_keyed_table(override, f"derived template value '{name}'")
519
            if override is not _MISSING
520
            else {}
521
        )
522
        override_maps = [override_map for _ in coordinates]
1✔
523

524
    entries: list[tuple[str, str | None]] = []
1✔
525
    for coordinate, overrides in zip(coordinates, override_maps):
1✔
526
        prefix = _derived_element_name(name, coordinate)
1✔
527
        entries.append(
1✔
528
            (
529
                prefix,
530
                _derived_buffer_value(
531
                    prefix,
532
                    properties,
533
                    overrides,
534
                    value_mode,
535
                    constants,
536
                    object_default,
537
                ),
538
            )
539
        )
540
    return entries
1✔
541

542

543
def _derived_element_name(name: str, coordinate: list[int]) -> str:
1✔
544
    if not coordinate:
1✔
545
        return name
1✔
546
    return f"{name}({','.join(str(index) for index in coordinate)})"
1✔
547

548

549
def _derived_buffer_value(
1✔
550
    prefix: str,
551
    properties: dict[str, Any],
552
    overrides: dict[str, Any],
553
    value_mode: str,
554
    constants: dict[str, int] | None,
555
    object_default: dict[str, Any] | None = None,
556
) -> str | None:
557
    for key in overrides:
1✔
558
        if key not in properties:
1✔
559
            raise ValueError(f"derived template value '{prefix}%{key}' is unknown")
×
560
    slots: list[str | None] = []
1✔
561
    selected_default = object_default or {}
1✔
562
    for child_name, child in properties.items():
1✔
563
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
564
            raise ValueError(f"derived property '{prefix}' components must be schema objects")
×
565
        child_entries = _value_entries(
1✔
566
            child_name,
567
            child,
568
            _field_type_info(child, constants),
569
            value_mode=value_mode,
570
            simple_derived_mode="components",
571
            override=overrides.get(child_name, _MISSING),
572
            constants=constants,
573
            fallback_default=selected_default.get(child_name.lower(), _MISSING),
574
        )
575
        if len(child_entries) != 1:
1✔
576
            raise ValueError(f"derived property '{prefix}%{child_name}' must be scalar")
×
577
        slots.append(child_entries[0][1])
1✔
578
    while slots and slots[-1] is None:
1✔
579
        slots.pop()
×
580
    if not slots:
1✔
581
        return None
×
582
    return ", ".join("" if slot is None else slot for slot in slots)
1✔
583

584

585
def _array_list_entries(
1✔
586
    name: str,
587
    values: list[Any],
588
    type_info: FieldTypeInfo,
589
    prop: dict[str, Any],
590
    constants: dict[str, int] | None,
591
) -> list[tuple[str, str | None]]:
592
    rank = len(type_info.dimensions)
1✔
593
    if rank <= 1:
1✔
594
        entry_name = f"{name}{_array_slice(rank)}"
1✔
595
        return [(entry_name, _format_value_list(values, type_info))]
1✔
596

597
    dims = _parse_default_dimensions(type_info.dimensions, constants)
1✔
598
    order = _resolve_default_order(prop)
1✔
599
    slice_dim = _slice_dim_for_order(rank, order)
1✔
600
    slice_size = dims[slice_dim]
1✔
601
    fixed_dims = [dims[idx] for idx in range(rank) if idx != slice_dim]
1✔
602
    entries: list[tuple[str, str | None]] = []
1✔
603
    offset = 0
1✔
604
    for fixed_indices in _iter_fixed_indices(fixed_dims, order):
1✔
605
        if offset >= len(values):
1✔
606
            break
×
607
        chunk = values[offset : offset + slice_size]
1✔
608
        if not chunk:
1✔
609
            break
×
610
        entry_name = _slice_entry_name(
1✔
611
            name,
612
            rank,
613
            slice_dim,
614
            [index + 1 for index in fixed_indices],
615
        )
616
        entries.append((entry_name, _format_value_list(chunk, type_info)))
1✔
617
        offset += slice_size
1✔
618
    if not entries:
1✔
619
        entry_name = f"{name}{_array_slice(rank)}"
×
620
        entries.append((entry_name, _format_value_list(values, type_info)))
×
621
    return entries
1✔
622

623

624
def _slice_dim_for_order(rank: int, order: str) -> int:
1✔
625
    if order == "F":
1✔
626
        return 0
1✔
627
    return rank - 1
×
628

629

630
def _iter_fixed_indices(dims: list[int], order: str) -> Iterable[list[int]]:
1✔
631
    if not dims:
1✔
632
        yield []
×
633
        return
×
634
    indices = [0] * len(dims)
1✔
635
    while True:
1✔
636
        yield indices.copy()
1✔
637
        positions = range(len(dims)) if order == "F" else range(len(dims) - 1, -1, -1)
1✔
638
        for pos in positions:
1✔
639
            indices[pos] += 1
1✔
640
            if indices[pos] < dims[pos]:
1✔
641
                break
1✔
642
            indices[pos] = 0
1✔
643
        else:
644
            break
1✔
645

646

647
def _slice_entry_name(
1✔
648
    name: str,
649
    rank: int,
650
    slice_dim: int,
651
    fixed_indices: list[int],
652
) -> str:
653
    indices: list[str] = []
1✔
654
    fixed_iter = iter(fixed_indices)
1✔
655
    for idx in range(rank):
1✔
656
        if idx == slice_dim:
1✔
657
            indices.append(":")
1✔
658
        else:
659
            indices.append(str(next(fixed_iter)))
1✔
660
    return f"{name}({','.join(indices)})"
1✔
661

662

663
def _format_value_list(values: list[Any], type_info: FieldTypeInfo) -> str:
1✔
664
    formatted = [
1✔
665
        _format_scalar_default(value, None, type_info.element_category) for value in values
666
    ]
667
    return ", ".join(formatted)
1✔
668

669

670
def _entries_from_value(
1✔
671
    name: str,
672
    value: Any,
673
    type_info: FieldTypeInfo,
674
    prop: dict[str, Any],
675
    constants: dict[str, int] | None,
676
) -> list[tuple[str, str | None]]:
677
    if type_info.category == "array":
1✔
678
        if isinstance(value, list):
×
679
            _validate_example_list(value)
×
680
            return _array_list_entries(name, value, type_info, prop, constants)
×
681
        scalar = _format_scalar_default(value, None, type_info.element_category)
×
682
        entry_name = f"{name}{_array_slice(len(type_info.dimensions))}"
×
683
        return [(entry_name, scalar)]
×
684
    if isinstance(value, list):
1✔
685
        raise ValueError("scalar template values must not be lists")
×
686
    scalar = _format_scalar_default(value, None, type_info.category)
1✔
687
    return [(name, scalar)]
1✔
688

689

690
def _get_first_example(prop: dict[str, Any], type_info: FieldTypeInfo) -> Any | None:
1✔
691
    examples = prop.get("examples")
1✔
692
    if examples is None:
1✔
693
        return None
1✔
694
    if not isinstance(examples, list):
1✔
695
        raise ValueError("property examples must be a list")
×
696
    if not examples:
1✔
697
        return None
×
698
    first = examples[0]
1✔
699
    if type_info.category == "array":
1✔
700
        if isinstance(first, list):
×
701
            _validate_example_list(first)
×
702
        return first
×
703
    if isinstance(first, list):
1✔
704
        raise ValueError("scalar examples must not be lists")
×
705
    return first
1✔
706

707

708
def _validate_example_list(values: list[Any]) -> None:
1✔
709
    for value in values:
×
710
        if isinstance(value, list):
×
711
            raise ValueError("array examples must be flat lists")
×
712

713

714
def _fallback_scalar_value(type_info: FieldTypeInfo) -> Any:
1✔
715
    category = type_info.element_category if type_info.category == "array" else type_info.category
1✔
716
    if category == "integer":
1✔
717
        return 0
1✔
718
    if category == "real":
1✔
719
        return 0.0
1✔
720
    if category == "boolean":
1✔
721
        return False
×
722
    if category == "string":
1✔
723
        return ""
1✔
724
    raise ValueError(f"unsupported template category '{category}'")
×
725

726

727
def _validate_kind_allowlist(
1✔
728
    type_info: FieldTypeInfo,
729
    kind_map: dict[str, str] | None,
730
    allowlist: set[str] | None,
731
) -> None:
732
    if type_info.kind is None or allowlist is None:
1✔
733
        return
1✔
734
    kind_id = type_info.kind
1✔
735
    mapped = None
1✔
736
    if kind_map is not None and kind_id in kind_map:
1✔
737
        mapped = kind_map[kind_id]
1✔
738
        if not isinstance(mapped, str) or not mapped:
1✔
739
            raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
740
        if mapped not in allowlist:
1✔
741
            raise ValueError(
×
742
                f"kind map target '{mapped}' for '{kind_id}' not present in kind module list"
743
            )
744
        return
1✔
745
    if kind_id not in allowlist:
×
746
        raise ValueError(f"kind '{kind_id}' not present in kind module list")
×
747

748

749
def _resolve_default_order(prop: dict[str, Any]) -> str:
1✔
750
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
751
    if not isinstance(order_raw, str):
1✔
752
        raise ValueError("array default order must be 'F' or 'C'")
×
753
    order = order_raw.upper()
1✔
754
    if order not in {"F", "C"}:
1✔
755
        raise ValueError("array default order must be 'F' or 'C'")
×
756
    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