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

MuellerSeb / nml-tools / 29619881679

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

Pull #63

github

MuellerSeb
Allow defaults to satisfy required values

Separate declared requiredness from effective input requirements across validation, namelist evaluation, and generated APIs. Support scalar derived object defaults and broadcast item defaults with deterministic coverage and precedence rules.

Update Fortran, f2py/Python, Markdown, templates, documentation, tests, and regenerated derived-type example artifacts.
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

81.65
/src/nml_tools/codegen_markdown.py
1
"""Markdown documentation generation."""
2

3
from __future__ import annotations
1✔
4

5
import re
1✔
6
import string
1✔
7
from pathlib import Path
1✔
8
from typing import Any
1✔
9

10
from ._utils import (
1✔
11
    normalize_constant_values,
12
    normalize_runtime_dimensions,
13
    reject_constant_dimension_overlap,
14
    validate_user_fortran_identifier,
15
)
16
from .codegen_fortran import (
1✔
17
    FieldTypeInfo,
18
    _array_default_value,
19
    _bounds_spec,
20
    _collect_dimension_constants,
21
    _derived_origin,
22
    _derived_schema,
23
    _enum_category,
24
    _enum_values,
25
    _field_type_info,
26
    _format_scalar_default,
27
    _parse_default_dimensions,
28
    _prepare_array_default,
29
    _reject_runtime_dimension_lengths,
30
)
31
from .codegen_template import render_template
1✔
32
from .schema import _is_simple_derived_schema, get_string_format
1✔
33
from .validate import (
1✔
34
    analyze_property_requirement,
35
    derived_component_defaults,
36
    derived_object_default,
37
    validate_schema_defaults,
38
)
39

40
_DEFAULT_MISSING = object()
1✔
41

42

43
def generate_docs(
1✔
44
    schema: dict[str, Any],
45
    output: str | Path,
46
    *,
47
    constants: dict[str, int] | None = None,
48
    dimensions: dict[str, int] | None = None,
49
    md_doxygen_id_from_name: bool = False,
50
    md_add_toc_statement: bool = False,
51
) -> None:
52
    """Generate Markdown docs for *schema* at *output*."""
53
    rendered = render_docs(
1✔
54
        schema,
55
        constants=constants,
56
        dimensions=dimensions,
57
        md_doxygen_id_from_name=md_doxygen_id_from_name,
58
        md_add_toc_statement=md_add_toc_statement,
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_docs(
1✔
66
    schema: dict[str, Any],
67
    *,
68
    constants: dict[str, int] | None = None,
69
    dimensions: dict[str, int] | None = None,
70
    md_doxygen_id_from_name: bool = False,
71
    md_add_toc_statement: bool = False,
72
) -> str:
73
    """Render Markdown docs for *schema*."""
74
    namelist_name = schema.get("x-fortran-namelist")
1✔
75
    if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
76
        raise ValueError("schema must define 'x-fortran-namelist'")
×
77
    validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
78

79
    if schema.get("type") != "object":
1✔
80
        raise ValueError("schema root must be of type 'object'")
×
81

82
    properties = schema.get("properties")
1✔
83
    if not isinstance(properties, dict) or not properties:
1✔
84
        raise ValueError("schema must define object 'properties'")
×
85

86
    title = schema.get("title", namelist_name)
1✔
87
    if not isinstance(title, str):
1✔
88
        raise ValueError("schema title must be a string")
×
89
    description = schema.get("description")
1✔
90
    if description is not None and not isinstance(description, str):
1✔
91
        raise ValueError("schema description must be a string")
×
92

93
    required_raw = schema.get("required", [])
1✔
94
    if required_raw is None:
1✔
95
        required_raw = []
×
96
    if not isinstance(required_raw, list):
1✔
97
        raise ValueError("schema 'required' must be a list")
×
98
    required_set = _validate_required(required_raw)
1✔
99
    constants = normalize_constant_values(constants)
1✔
100
    dimensions = normalize_runtime_dimensions(dimensions)
1✔
101
    reject_constant_dimension_overlap(constants, dimensions)
1✔
102
    validate_schema_defaults(schema, constants=constants, dimensions=dimensions)
1✔
103
    shape_constants: dict[str, int] = {
1✔
104
        **constants,
105
        **dimensions,
106
    }
107

108
    title_line = f"# {title}"
1✔
109
    if md_doxygen_id_from_name:
1✔
110
        title_line = f"{title_line} {{#{namelist_name}}}"
1✔
111
    lines = [title_line, ""]
1✔
112
    if md_add_toc_statement:
1✔
113
        lines.append("[TOC]")
1✔
114
        lines.append("")
1✔
115
    if description:
1✔
116
        lines.append(description)
×
117
        lines.append("")
×
118
    lines.append(f"**Namelist**: `{namelist_name}`")
1✔
119
    lines.append("")
1✔
120
    lines.append("## Fields")
1✔
121
    lines.append("")
1✔
122

123
    header = ["Name", "Type", "Declared required", "Input required", "Info"]
1✔
124
    lines.append(f"| {' | '.join(header)} |")
1✔
125
    lines.append(f"| {' | '.join('---' for _ in header)} |")
1✔
126

127
    current_property: str | None = None
1✔
128
    try:
1✔
129
        for name, prop in properties.items():
1✔
130
            current_property = name
1✔
131
            if not isinstance(prop, dict):
1✔
132
                raise ValueError(f"property '{name}' must be an object")
×
133
            _reject_runtime_dimension_lengths(prop, dimensions)
1✔
134
            type_info = _field_type_info(prop, constants)
1✔
135
            _collect_dimension_constants(type_info.dimensions, shape_constants)
1✔
136
            type_label = _format_table_type(type_info)
1✔
137
            info_label = _format_info(prop)
1✔
138
            declared_required = name.lower() in required_set
1✔
139
            requirement = analyze_property_requirement(
1✔
140
                name,
141
                prop,
142
                declared_required=declared_required,
143
            )
144
            row = [
1✔
145
                f"[{name}](#{_github_section_id(name)})",
146
                type_label,
147
                "yes" if declared_required else "no",
148
                "yes" if requirement.requires_input else "no",
149
                info_label,
150
            ]
151
            lines.append(f"| {' | '.join(_escape_table_cell(cell) for cell in row)} |")
1✔
152
    except ValueError as exc:
1✔
153
        if current_property is None:
1✔
154
            raise
×
155
        msg = str(exc)
1✔
156
        if f"property '{current_property}'" in msg:
1✔
157
            raise
×
158
        raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
159

160
    lines.append("")
1✔
161

162
    lines.append("## Field details")
1✔
163
    lines.append("")
1✔
164

165
    current_property = None
1✔
166
    try:
1✔
167
        for name, prop in properties.items():
1✔
168
            current_property = name
1✔
169
            if not isinstance(prop, dict):
1✔
170
                raise ValueError(f"property '{name}' must be an object")
×
171
            _reject_runtime_dimension_lengths(prop, dimensions)
1✔
172
            type_info = _field_type_info(prop, constants)
1✔
173
            declared_required = name.lower() in required_set
1✔
174
            requirement = analyze_property_requirement(
1✔
175
                name,
176
                prop,
177
                declared_required=declared_required,
178
            )
179
            default_label = _get_default_value(prop, type_info, shape_constants)
1✔
180
            enum_label = _get_enum_values(prop, type_info, shape_constants)
1✔
181
            example_values = _get_example_values(prop, type_info)
1✔
182
            flex_tail_dims = _get_flex_tail_dims(prop, type_info)
1✔
183
            title = _get_title(prop)
1✔
184
            description_text = _get_description(prop)
1✔
185

186
            # if title:
187
            #     lines.append(f"### `{name}` - {title}")
188
            # else:
189
            #     lines.append(f"### `{name}`")
190
            lines.append(f"### {name}")
1✔
191
            lines.append("")
1✔
192
            if title:
1✔
193
                lines.append(f"{title} `{name}`")
1✔
194
            else:
195
                lines.append(f"`{name}`")
1✔
196
            lines.append("")
1✔
197
            if description_text:
1✔
198
                lines.append(description_text)
1✔
199
                lines.append("")
1✔
200

201
            lines.append("Summary:")
1✔
202
            lines.append(f"- Type: `{_format_specific_type(type_info)}`")
1✔
203
            format_label = _get_format_label(prop, type_info)
1✔
204
            if format_label is not None:
1✔
205
                lines.append(format_label)
1✔
206
            if type_info.category == "array" and flex_tail_dims > 0:
1✔
207
                lines.append(f"- Flexible tail dims: {flex_tail_dims}")
×
208
            lines.append(f"- Declared required: {'yes' if declared_required else 'no'}")
1✔
209
            lines.append(f"- Input required: {'yes' if requirement.requires_input else 'no'}")
1✔
210
            if default_label is not None:
1✔
211
                if isinstance(default_label, tuple):
1✔
212
                    base, note = default_label
1✔
213
                    if note:
1✔
214
                        lines.append(f"- Default: `{base}` {note}")
1✔
215
                    else:
216
                        lines.append(f"- Default: `{base}`")
1✔
217
                else:
218
                    lines.append(f"- Default: `{default_label}`")
1✔
219
            for bounds_label in _get_bounds_labels(prop, type_info):
1✔
220
                lines.append(bounds_label)
1✔
221
            if enum_label is not None:
1✔
222
                lines.append(f"- Allowed values: {enum_label}")
1✔
223
            if example_values is not None:
1✔
224
                examples_text = ", ".join(f"`{value}`" for value in example_values)
×
225
                lines.append(f"- Examples: {examples_text}")
×
226
            lines.append("")
1✔
227
            derived = _derived_schema(prop)
1✔
228
            if derived is not None:
1✔
229
                _append_derived_field_components(
1✔
230
                    lines,
231
                    name,
232
                    prop,
233
                    derived,
234
                    shape_constants,
235
                )
236
    except ValueError as exc:
×
237
        if current_property is None:
×
238
            raise
×
239
        msg = str(exc)
×
240
        if f"property '{current_property}'" in msg:
×
241
            raise
×
242
        raise ValueError(f"property '{current_property}': {msg}") from exc
×
243

244
    derived_types = _collect_documented_derived_types(properties)
1✔
245
    if derived_types:
1✔
246
        lines.append("## Derived types")
1✔
247
        lines.append("")
1✔
248
        for derived in derived_types:
1✔
249
            _append_derived_type_documentation(lines, derived, shape_constants)
1✔
250

251
    lines.append("## Example")
1✔
252
    lines.append("")
1✔
253
    lines.append("```fortran")
1✔
254
    filled_template = render_template(
1✔
255
        [schema],
256
        doc_mode="plain",
257
        value_mode="filled",
258
        constants=shape_constants,
259
        kind_map=None,
260
        kind_allowlist=None,
261
    )
262
    lines.extend(filled_template.rstrip("\n").splitlines())
1✔
263
    lines.append("```")
1✔
264
    lines.append("")
1✔
265

266
    return "\n".join(lines) + "\n"
1✔
267

268

269
def _validate_required(values: list[Any]) -> set[str]:
1✔
270
    required: set[str] = set()
1✔
271
    for value in values:
1✔
272
        if not isinstance(value, str):
1✔
273
            raise ValueError("schema 'required' entries must be strings")
×
274
        required.add(value.lower())
1✔
275
    return required
1✔
276

277

278
def _format_table_type(type_info: FieldTypeInfo) -> str:
1✔
279
    if type_info.category == "array":
1✔
280
        if type_info.element_category == "derived":
1✔
281
            return f"{type_info.type_spec} array"
1✔
282
        element = _format_scalar_type_name(type_info.element_category)
1✔
283
        return f"{element} array"
1✔
284
    if type_info.category == "derived":
1✔
285
        return type_info.type_spec
1✔
286
    return _format_scalar_type_name(type_info.category)
1✔
287

288

289
def _format_scalar_type_name(category: str | None) -> str:
1✔
290
    if category == "boolean":
1✔
291
        return "logical"
×
292
    if category == "string":
1✔
293
        return "string"
1✔
294
    if category == "integer":
1✔
295
        return "integer"
1✔
296
    if category == "real":
1✔
297
        return "real"
1✔
298
    raise ValueError(f"unsupported type category '{category}'")
×
299

300

301
def _format_specific_type(type_info: FieldTypeInfo) -> str:
1✔
302
    if type_info.category != "array":
1✔
303
        return type_info.type_spec
1✔
304
    dimensions = ", ".join(type_info.dimensions)
1✔
305
    return f"{type_info.type_spec}, dimension({dimensions})"
1✔
306

307

308
def _append_derived_field_components(
1✔
309
    lines: list[str],
310
    field_name: str,
311
    prop: dict[str, Any],
312
    derived: dict[str, Any],
313
    constants: dict[str, int] | None,
314
) -> None:
315
    components = derived.get("properties")
1✔
316
    if not isinstance(components, dict):
1✔
317
        return
×
318
    required = {
1✔
319
        value.lower()
320
        for value in derived.get("required", [])
321
        if isinstance(value, str)
322
    }
323
    effective_defaults = derived_component_defaults(field_name, prop)
1✔
324
    object_defaults = derived_object_default(field_name, prop)
1✔
325
    object_source = "item default" if prop.get("type") == "array" else "object default"
1✔
326
    lines.append("Components:")
1✔
327
    for child_name, child in components.items():
1✔
328
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
329
            continue
×
330
        type_info = _field_type_info(child, constants)
1✔
331
        path = f"{field_name}%{child_name}"
1✔
332
        child_key = child_name.lower()
1✔
333
        declared_required = child_key in required
1✔
334
        input_required = declared_required and child_key not in effective_defaults
1✔
335
        details = [
1✔
336
            f"`{_format_specific_type(type_info)}`",
337
            f"declared required {'yes' if declared_required else 'no'}",
338
            f"input required {'yes' if input_required else 'no'}",
339
        ]
340
        if child_key in effective_defaults:
1✔
341
            default_text = _format_scalar_default(
1✔
342
                effective_defaults[child_key],
343
                None,
344
                type_info.category,
345
            )
346
            source = object_source if child_key in object_defaults else "component default"
1✔
347
            details.append(f"default `{default_text}` ({source})")
1✔
348
        format_label = _get_format_label(child, type_info)
1✔
349
        if format_label is not None:
1✔
350
            details.append(format_label[2:] if format_label.startswith("- ") else format_label)
1✔
351
        bounds = _get_bounds_labels(child, type_info)
1✔
352
        details.extend(label[2:] if label.startswith("- ") else label for label in bounds)
1✔
353
        enum = _get_enum_values(child, type_info, constants)
1✔
354
        if enum is not None:
1✔
355
            details.append(f"allowed values {enum}")
×
356
        lines.append(f"- `{path}`: " + "; ".join(details))
1✔
357
    lines.append("")
1✔
358

359

360
def _collect_documented_derived_types(properties: dict[str, Any]) -> list[dict[str, Any]]:
1✔
361
    seen: set[tuple[str, str]] = set()
1✔
362
    collected: list[dict[str, Any]] = []
1✔
363
    for prop in properties.values():
1✔
364
        if not isinstance(prop, dict):
1✔
365
            continue
×
366
        derived = _derived_schema(prop)
1✔
367
        if derived is None:
1✔
368
            continue
1✔
369
        origin = _derived_origin(derived)
1✔
370
        identity = origin["identity"]
1✔
371
        if identity in seen:
1✔
372
            continue
1✔
373
        seen.add(identity)
1✔
374
        collected.append(origin["definition"])
1✔
375
    return collected
1✔
376

377

378
def _append_derived_type_documentation(
1✔
379
    lines: list[str],
380
    derived: dict[str, Any],
381
    constants: dict[str, int] | None,
382
) -> None:
383
    type_name = derived.get("x-fortran-type")
1✔
384
    if not isinstance(type_name, str):
1✔
385
        return
×
386
    title = derived.get("title", type_name)
1✔
387
    lines.append(f"### `{type_name}`")
1✔
388
    lines.append("")
1✔
389
    if isinstance(title, str) and title != type_name:
1✔
390
        lines.append(title)
1✔
391
        lines.append("")
1✔
392
    description = derived.get("description")
1✔
393
    if isinstance(description, str) and description.strip():
1✔
394
        lines.append(description.strip())
1✔
395
        lines.append("")
1✔
396
    module = derived.get("x-fortran-module")
1✔
397
    ownership = f"imported from `{module}`" if isinstance(module, str) else "`nml_helper`"
1✔
398
    lines.append(f"- Ownership: {ownership}")
1✔
399
    components = derived.get("properties")
1✔
400
    if isinstance(components, dict):
1✔
401
        required = {
1✔
402
            value.lower()
403
            for value in derived.get("required", [])
404
            if isinstance(value, str)
405
        }
406
        effective_defaults = derived_component_defaults(type_name, derived)
1✔
407
        object_defaults = derived_object_default(type_name, derived)
1✔
408
        lines.append(
1✔
409
            "- Buffer-compatible: " + ("yes" if _is_simple_derived_schema(derived) else "no")
410
        )
411
        lines.append(f"- Component order: {', '.join(str(name) for name in components)}")
1✔
412
        if isinstance(module, str):
1✔
413
            lines.append(
1✔
414
                "- **Declaration-order contract:** the imported Fortran type must declare "
415
                "components in the resolved schema order shown above."
416
            )
417
        for child_name, child in components.items():
1✔
418
            if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
419
                continue
×
420
            info = _field_type_info(child, constants)
1✔
421
            child_key = child_name.lower()
1✔
422
            declared_required = child_key in required
1✔
423
            input_required = declared_required and child_key not in effective_defaults
1✔
424
            details = [
1✔
425
                f"`{_format_specific_type(info)}`",
426
                f"declared required {'yes' if declared_required else 'no'}",
427
                f"input required {'yes' if input_required else 'no'}",
428
            ]
429
            if child_key in effective_defaults:
1✔
430
                default_text = _format_scalar_default(
1✔
431
                    effective_defaults[child_key],
432
                    None,
433
                    info.category,
434
                )
435
                source = (
1✔
436
                    "object default" if child_key in object_defaults else "component default"
437
                )
438
                details.append(f"default `{default_text}` ({source})")
1✔
439
            format_label = _get_format_label(child, info)
1✔
440
            if format_label is not None:
1✔
441
                details.append(format_label[2:] if format_label.startswith("- ") else format_label)
1✔
442
            lines.append(f"- `{child_name}`: " + "; ".join(details))
1✔
443
    lines.append("")
1✔
444

445

446
def _get_default_value(
1✔
447
    prop: dict[str, Any],
448
    type_info: FieldTypeInfo,
449
    constants: dict[str, int] | None,
450
) -> str | tuple[str, str | None] | None:
451
    derived = _derived_schema(prop)
1✔
452
    if derived is not None:
1✔
453
        object_default = derived_object_default("field", prop)
1✔
454
        if not object_default:
1✔
455
            return None
1✔
456
        base = _format_derived_default_mapping(derived, object_default, constants)
1✔
457
        if type_info.category == "array":
1✔
458
            return base, "(broadcast item default)"
1✔
459
        return base
1✔
460
    if type_info.category == "array":
1✔
461
        array_default = _array_default_value(prop)
1✔
462
        if array_default is None:
1✔
463
            return None
1✔
464
        default_value, default_from_items = array_default
1✔
465
        return _format_array_default_display(
1✔
466
            prop,
467
            type_info,
468
            constants,
469
            default_value=default_value,
470
            from_items=default_from_items,
471
        )
472
    if "default" not in prop:
1✔
473
        return None
1✔
474
    return _format_default_plain(prop["default"], type_info, prop, constants)
1✔
475

476

477
def _format_derived_default_mapping(
1✔
478
    derived: dict[str, Any],
479
    default: dict[str, Any],
480
    constants: dict[str, int] | None,
481
) -> str:
482
    components = derived.get("properties")
1✔
483
    if not isinstance(components, dict):
1✔
NEW
484
        raise ValueError("derived object must define properties")
×
485
    entries: list[str] = []
1✔
486
    for child_name, child in components.items():
1✔
487
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
NEW
488
            raise ValueError("derived object components must be schema objects")
×
489
        key = child_name.lower()
1✔
490
        if key not in default:
1✔
491
            continue
1✔
492
        info = _field_type_info(child, constants)
1✔
493
        value = _format_scalar_default(default[key], None, info.category)
1✔
494
        entries.append(f"{child_name}: {value}")
1✔
495
    return "{" + ", ".join(entries) + "}"
1✔
496

497

498
def _format_info(prop: dict[str, Any]) -> str:
1✔
499
    title = _get_title(prop)
1✔
500
    return title or "n/a"
1✔
501

502

503
def _get_title(prop: dict[str, Any]) -> str | None:
1✔
504
    title = prop.get("title")
1✔
505
    if title is None:
1✔
506
        return None
1✔
507
    if not isinstance(title, str):
1✔
508
        raise ValueError("property title must be a string")
×
509
    title = title.strip()
1✔
510
    return title or None
1✔
511

512

513
def _get_description(prop: dict[str, Any]) -> str | None:
1✔
514
    description = prop.get("description")
1✔
515
    if description is None:
1✔
516
        return None
1✔
517
    if not isinstance(description, str):
1✔
518
        raise ValueError("property description must be a string")
×
519
    description = description.strip()
1✔
520
    return description or None
1✔
521

522

523
def _get_format_label(prop: dict[str, Any], type_info: FieldTypeInfo) -> str | None:
1✔
524
    if type_info.category == "array":
1✔
525
        items = prop.get("items")
1✔
526
        if not isinstance(items, dict):
1✔
527
            return None
×
528
        value = get_string_format(items)
1✔
529
        if value is None:
1✔
530
            return None
1✔
531
        return f"- Item format: `{value}`"
1✔
532
    value = get_string_format(prop)
1✔
533
    if value is None:
1✔
534
        return None
1✔
535
    return f"- Format: `{value}`"
1✔
536

537

538
def _get_enum_values(
1✔
539
    prop: dict[str, Any],
540
    type_info: FieldTypeInfo,
541
    constants: dict[str, int] | None,
542
) -> str | None:
543
    enum_values = _enum_values(prop, type_info, constants)
1✔
544
    if enum_values is None:
1✔
545
        return None
1✔
546
    category = _enum_category(type_info)
1✔
547
    values = [_format_scalar_default(value, None, category) for value in enum_values]
1✔
548
    return ", ".join(f"`{value}`" for value in values)
1✔
549

550

551
def _get_bounds_labels(
1✔
552
    prop: dict[str, Any],
553
    type_info: FieldTypeInfo,
554
) -> list[str]:
555
    bounds = _bounds_spec(prop, type_info)
1✔
556
    if bounds is None:
1✔
557
        return []
1✔
558
    category = bounds["category"]
1✔
559
    labels: list[str] = []
1✔
560
    min_value = bounds["min_value"]
1✔
561
    max_value = bounds["max_value"]
1✔
562
    if min_value is not None:
1✔
563
        op = ">" if bounds["min_exclusive"] else ">="
1✔
564
        literal = _format_scalar_default(min_value, None, category)
1✔
565
        labels.append(f"- Minimum: `{op} {literal}`")
1✔
566
    if max_value is not None:
1✔
567
        op = "<" if bounds["max_exclusive"] else "<="
1✔
568
        literal = _format_scalar_default(max_value, None, category)
1✔
569
        labels.append(f"- Maximum: `{op} {literal}`")
1✔
570
    return labels
1✔
571

572

573
def _get_flex_tail_dims(
1✔
574
    prop: dict[str, Any],
575
    type_info: FieldTypeInfo,
576
) -> int:
577
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
578
    if flex_raw is None:
1✔
579
        flex_value = 0
1✔
580
    else:
581
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
×
582
            raise ValueError("property flex tail dims must be an integer")
×
583
        flex_value = flex_raw
×
584
    if flex_value < 0:
1✔
585
        raise ValueError("property flex tail dims must be >= 0")
×
586
    if flex_value == 0:
1✔
587
        return 0
1✔
588
    if type_info.category != "array":
×
589
        raise ValueError("flex tail dims only apply to arrays")
×
590
    if flex_value > len(type_info.dimensions):
×
591
        raise ValueError("flex tail dims must not exceed array rank")
×
592
    return flex_value
×
593

594

595
def _escape_table_cell(value: str) -> str:
1✔
596
    escaped = value.replace("|", "\\|").replace("\n", " ").strip()
1✔
597
    return escaped or "n/a"
1✔
598

599

600
def _get_example_values(
1✔
601
    prop: dict[str, Any],
602
    type_info: FieldTypeInfo,
603
) -> list[str] | None:
604
    examples = prop.get("examples")
1✔
605
    if examples is None:
1✔
606
        return None
1✔
607
    if not isinstance(examples, list):
×
608
        raise ValueError("property examples must be a list")
×
609
    if not examples:
×
610
        return None
×
611
    return [_format_example_value(value, type_info) for value in examples]
×
612

613

614
def _format_example_value(value: Any, type_info: FieldTypeInfo) -> str:
1✔
615
    if type_info.category == "array":
×
616
        if isinstance(value, list):
×
617
            formatted = [
×
618
                _format_scalar_default(item, None, type_info.element_category) for item in value
619
            ]
620
            return f"[{', '.join(formatted)}]"
×
621
        return _format_scalar_default(value, None, type_info.element_category)
×
622
    if isinstance(value, list):
×
623
        raise ValueError("scalar examples must not be lists")
×
624
    return _format_scalar_default(value, None, type_info.category)
×
625

626

627
def _format_default_plain(
1✔
628
    value: Any,
629
    type_info: FieldTypeInfo,
630
    prop: dict[str, Any],
631
    constants: dict[str, int] | None,
632
) -> str:
633
    if type_info.category == "array":
1✔
634
        if not isinstance(value, list):
×
635
            raise ValueError("array default must be a list")
×
636
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
637
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
638
        elements = [
×
639
            _format_scalar_default(element, None, type_info.element_category)
640
            for element in array_default.source_values
641
        ]
642
        if (
×
643
            len(type_info.dimensions) == 1
644
            and array_default.order_values is None
645
            and array_default.pad_values is None
646
        ):
647
            return f"[{', '.join(elements)}]"
×
648

649
        shape_literal = ", ".join(type_info.dimensions)
×
650
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
651

652
        if array_default.order_values is not None:
×
653
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
654
            arguments.append(f"order=[{order_literal}]")
×
655

656
        if array_default.pad_values is not None:
×
657
            pad_elements = [
×
658
                _format_scalar_default(element, None, type_info.element_category)
659
                for element in array_default.pad_values
660
            ]
661
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
662

663
        return f"reshape({', '.join(arguments)})"
×
664

665
    return _format_scalar_default(value, None, type_info.category)
1✔
666

667

668
def _format_array_default_display(
1✔
669
    prop: dict[str, Any],
670
    type_info: FieldTypeInfo,
671
    constants: dict[str, int] | None,
672
    *,
673
    default_value: Any = _DEFAULT_MISSING,
674
    from_items: bool = False,
675
) -> tuple[str, str | None]:
676
    if default_value is _DEFAULT_MISSING:
1✔
677
        default_value = prop["default"]
×
678
    if not from_items and not isinstance(default_value, list):
1✔
679
        raise ValueError("array default must be a list")
×
680
    default_is_list = isinstance(default_value, list)
1✔
681
    default_list = default_value if default_is_list else [default_value]
1✔
682

683
    parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
1✔
684
    default_prop = prop
1✔
685
    if from_items:
1✔
686
        default_prop = dict(prop)
1✔
687
        default_prop["x-fortran-default-repeat"] = True
1✔
688
    _prepare_array_default(default_list, parsed_dims, default_prop)
1✔
689

690
    if default_is_list:
1✔
691
        elements = [
1✔
692
            _format_scalar_default(value, None, type_info.element_category)
693
            for value in default_list
694
        ]
695
        base = f"[{', '.join(elements)}]"
1✔
696
    else:
697
        base = _format_scalar_default(default_value, None, type_info.element_category)
1✔
698

699
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
700
    if not isinstance(repeat_raw, bool):
1✔
701
        raise ValueError("array default repeat must be a boolean")
×
702
    notes: list[str] = []
1✔
703
    if repeat_raw:
1✔
704
        notes.append("repeated")
1✔
705

706
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
707
    if not isinstance(order_raw, str):
1✔
708
        raise ValueError("array default order must be 'F' or 'C'")
×
709
    order = order_raw.upper()
1✔
710
    if order not in {"F", "C"}:
1✔
711
        raise ValueError("array default order must be 'F' or 'C'")
×
712
    if order == "C":
1✔
713
        notes.append("order: C")
×
714

715
    pad_raw = prop.get("x-fortran-default-pad")
1✔
716
    if pad_raw is not None:
1✔
717
        pad_list = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
718
        pad_elements = [
×
719
            _format_scalar_default(value, None, type_info.element_category) for value in pad_list
720
        ]
721
        if isinstance(pad_raw, list):
×
722
            pad_text = f"[{', '.join(pad_elements)}]"
×
723
        else:
724
            pad_text = pad_elements[0]
×
725
        notes.append(f"pad: `{pad_text}`")
×
726

727
    if notes:
1✔
728
        return base, f"({', '.join(notes)})"
1✔
729
    return base, None
1✔
730

731

732
def _github_section_id(title: str) -> str:
1✔
733
    # lowercase
734
    s = title.strip().lower()
1✔
735
    # remove punctuation characters
736
    punctuation = string.punctuation.replace("_", "")
1✔
737
    s = re.sub(f"[{re.escape(punctuation)}]", "", s)
1✔
738
    # replace any whitespace run with '-'
739
    s = re.sub(r"\s+", "-", s)
1✔
740
    return s
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