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

MuellerSeb / nml-tools / 28520890054

01 Jul 2026 01:24PM UTC coverage: 84.588% (+0.2%) from 84.375%
28520890054

Pull #47

github

MuellerSeb
Avoid duplicate format annotation validation
Pull Request #47: Support String Format Annotations

53 of 55 new or added lines in 2 files covered. (96.36%)

4078 of 4821 relevant lines covered (84.59%)

0.85 hits per line

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

79.39
/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 get_string_format
1✔
33
from .validate import validate_schema_defaults
1✔
34

35
_DEFAULT_MISSING = object()
1✔
36

37

38
def generate_docs(
1✔
39
    schema: dict[str, Any],
40
    output: str | Path,
41
    *,
42
    constants: dict[str, int] | None = None,
43
    dimensions: dict[str, int] | None = None,
44
    md_doxygen_id_from_name: bool = False,
45
    md_add_toc_statement: bool = False,
46
) -> None:
47
    """Generate Markdown docs for *schema* at *output*."""
48
    rendered = render_docs(
1✔
49
        schema,
50
        constants=constants,
51
        dimensions=dimensions,
52
        md_doxygen_id_from_name=md_doxygen_id_from_name,
53
        md_add_toc_statement=md_add_toc_statement,
54
    )
55
    output_path = Path(output)
1✔
56
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
57
    output_path.write_text(rendered, encoding="ascii")
1✔
58

59

60
def render_docs(
1✔
61
    schema: dict[str, Any],
62
    *,
63
    constants: dict[str, int] | None = None,
64
    dimensions: dict[str, int] | None = None,
65
    md_doxygen_id_from_name: bool = False,
66
    md_add_toc_statement: bool = False,
67
) -> str:
68
    """Render Markdown docs for *schema*."""
69
    namelist_name = schema.get("x-fortran-namelist")
1✔
70
    if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
71
        raise ValueError("schema must define 'x-fortran-namelist'")
×
72
    validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
73

74
    if schema.get("type") != "object":
1✔
75
        raise ValueError("schema root must be of type 'object'")
×
76

77
    properties = schema.get("properties")
1✔
78
    if not isinstance(properties, dict) or not properties:
1✔
79
        raise ValueError("schema must define object 'properties'")
×
80

81
    title = schema.get("title", namelist_name)
1✔
82
    if not isinstance(title, str):
1✔
83
        raise ValueError("schema title must be a string")
×
84
    description = schema.get("description")
1✔
85
    if description is not None and not isinstance(description, str):
1✔
86
        raise ValueError("schema description must be a string")
×
87

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

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

118
    header = ["Name", "Type", "Required", "Info"]
1✔
119
    lines.append(f"| {' | '.join(header)} |")
1✔
120
    lines.append(f"| {' | '.join('---' for _ in header)} |")
1✔
121

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

149
    lines.append("")
1✔
150

151
    lines.append("## Field details")
1✔
152
    lines.append("")
1✔
153

154
    current_property = None
1✔
155
    try:
1✔
156
        for name, prop in properties.items():
1✔
157
            current_property = name
1✔
158
            if not isinstance(prop, dict):
1✔
159
                raise ValueError(f"property '{name}' must be an object")
×
160
            _reject_runtime_dimension_lengths(prop, dimensions)
1✔
161
            type_info = _field_type_info(prop, constants)
1✔
162
            required_label = "yes" if name in required_set else "no"
1✔
163
            default_label = _get_default_value(prop, type_info, shape_constants)
1✔
164
            enum_label = _get_enum_values(prop, type_info, shape_constants)
1✔
165
            example_values = _get_example_values(prop, type_info)
1✔
166
            flex_tail_dims = _get_flex_tail_dims(prop, type_info)
1✔
167
            title = _get_title(prop)
1✔
168
            description_text = _get_description(prop)
1✔
169

170
            # if title:
171
            #     lines.append(f"### `{name}` - {title}")
172
            # else:
173
            #     lines.append(f"### `{name}`")
174
            lines.append(f"### {name}")
1✔
175
            lines.append("")
1✔
176
            if title:
1✔
177
                lines.append(f"{title} `{name}`")
1✔
178
            else:
179
                lines.append(f"`{name}`")
1✔
180
            lines.append("")
1✔
181
            if description_text:
1✔
182
                lines.append(description_text)
1✔
183
                lines.append("")
1✔
184

185
            lines.append("Summary:")
1✔
186
            lines.append(f"- Type: `{_format_specific_type(type_info)}`")
1✔
187
            format_label = _get_format_label(prop, type_info)
1✔
188
            if format_label is not None:
1✔
189
                lines.append(format_label)
1✔
190
            if type_info.category == "array" and flex_tail_dims > 0:
1✔
191
                lines.append(f"- Flexible tail dims: {flex_tail_dims}")
×
192
            lines.append(f"- Required: {required_label}")
1✔
193
            if default_label is not None:
1✔
194
                if isinstance(default_label, tuple):
1✔
195
                    base, note = default_label
1✔
196
                    if note:
1✔
197
                        lines.append(f"- Default: `{base}` {note}")
1✔
198
                    else:
199
                        lines.append(f"- Default: `{base}`")
1✔
200
                else:
201
                    lines.append(f"- Default: `{default_label}`")
1✔
202
            for bounds_label in _get_bounds_labels(prop, type_info):
1✔
203
                lines.append(bounds_label)
1✔
204
            if enum_label is not None:
1✔
205
                lines.append(f"- Allowed values: {enum_label}")
1✔
206
            if example_values is not None:
1✔
207
                examples_text = ", ".join(f"`{value}`" for value in example_values)
×
208
                lines.append(f"- Examples: {examples_text}")
×
209
            lines.append("")
1✔
210
            derived = _derived_schema(prop)
1✔
211
            if derived is not None:
1✔
212
                _append_derived_field_components(lines, name, derived, shape_constants)
1✔
213
    except ValueError as exc:
×
214
        if current_property is None:
×
215
            raise
×
216
        msg = str(exc)
×
217
        if f"property '{current_property}'" in msg:
×
218
            raise
×
219
        raise ValueError(f"property '{current_property}': {msg}") from exc
×
220

221
    derived_types = _collect_documented_derived_types(properties)
1✔
222
    if derived_types:
1✔
223
        lines.append("## Derived types")
1✔
224
        lines.append("")
1✔
225
        for derived in derived_types:
1✔
226
            _append_derived_type_documentation(lines, derived, shape_constants)
1✔
227

228
    lines.append("## Example")
1✔
229
    lines.append("")
1✔
230
    lines.append("```fortran")
1✔
231
    filled_template = render_template(
1✔
232
        [schema],
233
        doc_mode="plain",
234
        value_mode="filled",
235
        constants=shape_constants,
236
        kind_map=None,
237
        kind_allowlist=None,
238
    )
239
    lines.extend(filled_template.rstrip("\n").splitlines())
1✔
240
    lines.append("```")
1✔
241
    lines.append("")
1✔
242

243
    return "\n".join(lines) + "\n"
1✔
244

245

246
def _validate_required(values: list[Any]) -> set[str]:
1✔
247
    required: set[str] = set()
1✔
248
    for value in values:
1✔
249
        if not isinstance(value, str):
×
250
            raise ValueError("schema 'required' entries must be strings")
×
251
        required.add(value)
×
252
    return required
1✔
253

254

255
def _format_table_type(type_info: FieldTypeInfo) -> str:
1✔
256
    if type_info.category == "array":
1✔
257
        if type_info.element_category == "derived":
1✔
258
            return f"{type_info.type_spec} array"
1✔
259
        element = _format_scalar_type_name(type_info.element_category)
1✔
260
        return f"{element} array"
1✔
261
    if type_info.category == "derived":
1✔
262
        return type_info.type_spec
1✔
263
    return _format_scalar_type_name(type_info.category)
1✔
264

265

266
def _format_scalar_type_name(category: str | None) -> str:
1✔
267
    if category == "boolean":
1✔
268
        return "logical"
×
269
    if category == "string":
1✔
270
        return "string"
1✔
271
    if category == "integer":
1✔
272
        return "integer"
1✔
273
    if category == "real":
1✔
274
        return "real"
1✔
275
    raise ValueError(f"unsupported type category '{category}'")
×
276

277

278
def _format_specific_type(type_info: FieldTypeInfo) -> str:
1✔
279
    if type_info.category != "array":
1✔
280
        return type_info.type_spec
1✔
281
    dimensions = ", ".join(type_info.dimensions)
1✔
282
    return f"{type_info.type_spec}, dimension({dimensions})"
1✔
283

284

285
def _append_derived_field_components(
1✔
286
    lines: list[str],
287
    field_name: str,
288
    derived: dict[str, Any],
289
    constants: dict[str, int] | None,
290
) -> None:
291
    components = derived.get("properties")
1✔
292
    if not isinstance(components, dict):
1✔
293
        return
×
294
    lines.append("Components:")
1✔
295
    for child_name, child in components.items():
1✔
296
        if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
297
            continue
×
298
        type_info = _field_type_info(child, constants)
1✔
299
        path = f"{field_name}%{child_name}"
1✔
300
        details = [f"`{_format_specific_type(type_info)}`"]
1✔
301
        default = _get_default_value(child, type_info, constants)
1✔
302
        if isinstance(default, tuple):
1✔
303
            details.append(f"default `{default[0]}`")
×
304
        elif default is not None:
1✔
305
            details.append(f"default `{default}`")
1✔
306
        format_label = _get_format_label(child, type_info)
1✔
307
        if format_label is not None:
1✔
308
            details.append(format_label[2:] if format_label.startswith("- ") else format_label)
1✔
309
        bounds = _get_bounds_labels(child, type_info)
1✔
310
        details.extend(label[2:] if label.startswith("- ") else label for label in bounds)
1✔
311
        enum = _get_enum_values(child, type_info, constants)
1✔
312
        if enum is not None:
1✔
313
            details.append(f"allowed values {enum}")
×
314
        lines.append(f"- `{path}`: " + "; ".join(details))
1✔
315
    lines.append("")
1✔
316

317

318
def _collect_documented_derived_types(properties: dict[str, Any]) -> list[dict[str, Any]]:
1✔
319
    seen: set[tuple[str, str]] = set()
1✔
320
    collected: list[dict[str, Any]] = []
1✔
321
    for prop in properties.values():
1✔
322
        if not isinstance(prop, dict):
1✔
323
            continue
×
324
        derived = _derived_schema(prop)
1✔
325
        if derived is None:
1✔
326
            continue
1✔
327
        origin = _derived_origin(derived)
1✔
328
        identity = origin["identity"]
1✔
329
        if identity in seen:
1✔
330
            continue
1✔
331
        seen.add(identity)
1✔
332
        collected.append(origin["definition"])
1✔
333
    return collected
1✔
334

335

336
def _append_derived_type_documentation(
1✔
337
    lines: list[str],
338
    derived: dict[str, Any],
339
    constants: dict[str, int] | None,
340
) -> None:
341
    type_name = derived.get("x-fortran-type")
1✔
342
    if not isinstance(type_name, str):
1✔
343
        return
×
344
    title = derived.get("title", type_name)
1✔
345
    lines.append(f"### `{type_name}`")
1✔
346
    lines.append("")
1✔
347
    if isinstance(title, str) and title != type_name:
1✔
348
        lines.append(title)
1✔
349
        lines.append("")
1✔
350
    description = derived.get("description")
1✔
351
    if isinstance(description, str) and description.strip():
1✔
352
        lines.append(description.strip())
1✔
353
        lines.append("")
1✔
354
    module = derived.get("x-fortran-module")
1✔
355
    ownership = f"imported from `{module}`" if isinstance(module, str) else "`nml_helper`"
1✔
356
    lines.append(f"- Ownership: {ownership}")
1✔
357
    components = derived.get("properties")
1✔
358
    if isinstance(components, dict):
1✔
359
        for child_name, child in components.items():
1✔
360
            if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
361
                continue
×
362
            info = _field_type_info(child, constants)
1✔
363
            details = [f"`{_format_specific_type(info)}`"]
1✔
364
            format_label = _get_format_label(child, info)
1✔
365
            if format_label is not None:
1✔
366
                details.append(format_label[2:] if format_label.startswith("- ") else format_label)
1✔
367
            lines.append(f"- `{child_name}`: " + "; ".join(details))
1✔
368
    lines.append("")
1✔
369

370

371
def _get_default_value(
1✔
372
    prop: dict[str, Any],
373
    type_info: FieldTypeInfo,
374
    constants: dict[str, int] | None,
375
) -> str | tuple[str, str | None] | None:
376
    if type_info.category == "array":
1✔
377
        array_default = _array_default_value(prop)
1✔
378
        if array_default is None:
1✔
379
            return None
1✔
380
        default_value, default_from_items = array_default
1✔
381
        return _format_array_default_display(
1✔
382
            prop,
383
            type_info,
384
            constants,
385
            default_value=default_value,
386
            from_items=default_from_items,
387
        )
388
    if "default" not in prop:
1✔
389
        return None
1✔
390
    return _format_default_plain(prop["default"], type_info, prop, constants)
1✔
391

392

393
def _format_info(prop: dict[str, Any]) -> str:
1✔
394
    title = _get_title(prop)
1✔
395
    return title or "n/a"
1✔
396

397

398
def _get_title(prop: dict[str, Any]) -> str | None:
1✔
399
    title = prop.get("title")
1✔
400
    if title is None:
1✔
401
        return None
1✔
402
    if not isinstance(title, str):
1✔
403
        raise ValueError("property title must be a string")
×
404
    title = title.strip()
1✔
405
    return title or None
1✔
406

407

408
def _get_description(prop: dict[str, Any]) -> str | None:
1✔
409
    description = prop.get("description")
1✔
410
    if description is None:
1✔
411
        return None
1✔
412
    if not isinstance(description, str):
1✔
413
        raise ValueError("property description must be a string")
×
414
    description = description.strip()
1✔
415
    return description or None
1✔
416

417

418
def _get_format_label(prop: dict[str, Any], type_info: FieldTypeInfo) -> str | None:
1✔
419
    if type_info.category == "array":
1✔
420
        items = prop.get("items")
1✔
421
        if not isinstance(items, dict):
1✔
NEW
422
            return None
×
423
        value = get_string_format(items)
1✔
424
        if value is None:
1✔
425
            return None
1✔
426
        return f"- Item format: `{value}`"
1✔
427
    value = get_string_format(prop)
1✔
428
    if value is None:
1✔
429
        return None
1✔
430
    return f"- Format: `{value}`"
1✔
431

432

433
def _get_enum_values(
1✔
434
    prop: dict[str, Any],
435
    type_info: FieldTypeInfo,
436
    constants: dict[str, int] | None,
437
) -> str | None:
438
    enum_values = _enum_values(prop, type_info, constants)
1✔
439
    if enum_values is None:
1✔
440
        return None
1✔
441
    category = _enum_category(type_info)
1✔
442
    values = [_format_scalar_default(value, None, category) for value in enum_values]
1✔
443
    return ", ".join(f"`{value}`" for value in values)
1✔
444

445

446
def _get_bounds_labels(
1✔
447
    prop: dict[str, Any],
448
    type_info: FieldTypeInfo,
449
) -> list[str]:
450
    bounds = _bounds_spec(prop, type_info)
1✔
451
    if bounds is None:
1✔
452
        return []
1✔
453
    category = bounds["category"]
1✔
454
    labels: list[str] = []
1✔
455
    min_value = bounds["min_value"]
1✔
456
    max_value = bounds["max_value"]
1✔
457
    if min_value is not None:
1✔
458
        op = ">" if bounds["min_exclusive"] else ">="
1✔
459
        literal = _format_scalar_default(min_value, None, category)
1✔
460
        labels.append(f"- Minimum: `{op} {literal}`")
1✔
461
    if max_value is not None:
1✔
462
        op = "<" if bounds["max_exclusive"] else "<="
1✔
463
        literal = _format_scalar_default(max_value, None, category)
1✔
464
        labels.append(f"- Maximum: `{op} {literal}`")
1✔
465
    return labels
1✔
466

467

468
def _get_flex_tail_dims(
1✔
469
    prop: dict[str, Any],
470
    type_info: FieldTypeInfo,
471
) -> int:
472
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
473
    if flex_raw is None:
1✔
474
        flex_value = 0
1✔
475
    else:
476
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
×
477
            raise ValueError("property flex tail dims must be an integer")
×
478
        flex_value = flex_raw
×
479
    if flex_value < 0:
1✔
480
        raise ValueError("property flex tail dims must be >= 0")
×
481
    if flex_value == 0:
1✔
482
        return 0
1✔
483
    if type_info.category != "array":
×
484
        raise ValueError("flex tail dims only apply to arrays")
×
485
    if flex_value > len(type_info.dimensions):
×
486
        raise ValueError("flex tail dims must not exceed array rank")
×
487
    return flex_value
×
488

489

490
def _escape_table_cell(value: str) -> str:
1✔
491
    escaped = value.replace("|", "\\|").replace("\n", " ").strip()
1✔
492
    return escaped or "n/a"
1✔
493

494

495
def _get_example_values(
1✔
496
    prop: dict[str, Any],
497
    type_info: FieldTypeInfo,
498
) -> list[str] | None:
499
    examples = prop.get("examples")
1✔
500
    if examples is None:
1✔
501
        return None
1✔
502
    if not isinstance(examples, list):
×
503
        raise ValueError("property examples must be a list")
×
504
    if not examples:
×
505
        return None
×
506
    return [_format_example_value(value, type_info) for value in examples]
×
507

508

509
def _format_example_value(value: Any, type_info: FieldTypeInfo) -> str:
1✔
510
    if type_info.category == "array":
×
511
        if isinstance(value, list):
×
512
            formatted = [
×
513
                _format_scalar_default(item, None, type_info.element_category) for item in value
514
            ]
515
            return f"[{', '.join(formatted)}]"
×
516
        return _format_scalar_default(value, None, type_info.element_category)
×
517
    if isinstance(value, list):
×
518
        raise ValueError("scalar examples must not be lists")
×
519
    return _format_scalar_default(value, None, type_info.category)
×
520

521

522
def _format_default_plain(
1✔
523
    value: Any,
524
    type_info: FieldTypeInfo,
525
    prop: dict[str, Any],
526
    constants: dict[str, int] | None,
527
) -> str:
528
    if type_info.category == "array":
1✔
529
        if not isinstance(value, list):
×
530
            raise ValueError("array default must be a list")
×
531
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
532
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
533
        elements = [
×
534
            _format_scalar_default(element, None, type_info.element_category)
535
            for element in array_default.source_values
536
        ]
537
        if (
×
538
            len(type_info.dimensions) == 1
539
            and array_default.order_values is None
540
            and array_default.pad_values is None
541
        ):
542
            return f"[{', '.join(elements)}]"
×
543

544
        shape_literal = ", ".join(type_info.dimensions)
×
545
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
546

547
        if array_default.order_values is not None:
×
548
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
549
            arguments.append(f"order=[{order_literal}]")
×
550

551
        if array_default.pad_values is not None:
×
552
            pad_elements = [
×
553
                _format_scalar_default(element, None, type_info.element_category)
554
                for element in array_default.pad_values
555
            ]
556
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
557

558
        return f"reshape({', '.join(arguments)})"
×
559

560
    return _format_scalar_default(value, None, type_info.category)
1✔
561

562

563
def _format_array_default_display(
1✔
564
    prop: dict[str, Any],
565
    type_info: FieldTypeInfo,
566
    constants: dict[str, int] | None,
567
    *,
568
    default_value: Any = _DEFAULT_MISSING,
569
    from_items: bool = False,
570
) -> tuple[str, str | None]:
571
    if default_value is _DEFAULT_MISSING:
1✔
572
        default_value = prop["default"]
×
573
    if not from_items and not isinstance(default_value, list):
1✔
574
        raise ValueError("array default must be a list")
×
575
    default_is_list = isinstance(default_value, list)
1✔
576
    default_list = default_value if default_is_list else [default_value]
1✔
577

578
    parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
1✔
579
    default_prop = prop
1✔
580
    if from_items:
1✔
581
        default_prop = dict(prop)
1✔
582
        default_prop["x-fortran-default-repeat"] = True
1✔
583
    _prepare_array_default(default_list, parsed_dims, default_prop)
1✔
584

585
    if default_is_list:
1✔
586
        elements = [
1✔
587
            _format_scalar_default(value, None, type_info.element_category)
588
            for value in default_list
589
        ]
590
        base = f"[{', '.join(elements)}]"
1✔
591
    else:
592
        base = _format_scalar_default(default_value, None, type_info.element_category)
1✔
593

594
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
595
    if not isinstance(repeat_raw, bool):
1✔
596
        raise ValueError("array default repeat must be a boolean")
×
597
    notes: list[str] = []
1✔
598
    if repeat_raw:
1✔
599
        notes.append("repeated")
1✔
600

601
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
602
    if not isinstance(order_raw, str):
1✔
603
        raise ValueError("array default order must be 'F' or 'C'")
×
604
    order = order_raw.upper()
1✔
605
    if order not in {"F", "C"}:
1✔
606
        raise ValueError("array default order must be 'F' or 'C'")
×
607
    if order == "C":
1✔
608
        notes.append("order: C")
×
609

610
    pad_raw = prop.get("x-fortran-default-pad")
1✔
611
    if pad_raw is not None:
1✔
612
        pad_list = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
613
        pad_elements = [
×
614
            _format_scalar_default(value, None, type_info.element_category) for value in pad_list
615
        ]
616
        if isinstance(pad_raw, list):
×
617
            pad_text = f"[{', '.join(pad_elements)}]"
×
618
        else:
619
            pad_text = pad_elements[0]
×
620
        notes.append(f"pad: `{pad_text}`")
×
621

622
    if notes:
1✔
623
        return base, f"({', '.join(notes)})"
1✔
624
    return base, None
1✔
625

626

627
def _github_section_id(title: str) -> str:
1✔
628
    # lowercase
629
    s = title.strip().lower()
1✔
630
    # remove punctuation characters
631
    punctuation = string.punctuation.replace("_", "")
1✔
632
    s = re.sub(f"[{re.escape(punctuation)}]", "", s)
1✔
633
    # replace any whitespace run with '-'
634
    s = re.sub(r"\s+", "-", s)
1✔
635
    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