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

MuellerSeb / nml-tools / 29620967587

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

push

github

web-flow
Merge pull request #63 from MuellerSeb/update_derived_type_handling

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