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

MuellerSeb / nml-tools / 26503701596

27 May 2026 09:45AM UTC coverage: 83.479% (+2.4%) from 81.064%
26503701596

Pull #32

github

web-flow
Merge 79e34396a into 7a5f3483f
Pull Request #32: Add One-Level Derived-Type Support For Generated Namelists

676 of 746 new or added lines in 7 files covered. (90.62%)

1 existing line in 1 file now uncovered.

3729 of 4467 relevant lines covered (83.48%)

0.83 hits per line

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

78.36
/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
)
15
from .codegen_fortran import (
1✔
16
    FieldTypeInfo,
17
    _array_default_value,
18
    _bounds_spec,
19
    _collect_dimension_constants,
20
    _derived_origin,
21
    _derived_schema,
22
    _enum_category,
23
    _enum_values,
24
    _field_type_info,
25
    _format_scalar_default,
26
    _parse_default_dimensions,
27
    _prepare_array_default,
28
    _reject_runtime_dimension_lengths,
29
)
30
from .codegen_template import render_template
1✔
31
from .validate import validate_schema_defaults
1✔
32

33
_DEFAULT_MISSING = object()
1✔
34

35

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

57

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

71
    if schema.get("type") != "object":
1✔
72
        raise ValueError("schema root must be of type 'object'")
×
73

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

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

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

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

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

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

146
    lines.append("")
1✔
147

148
    lines.append("## Field details")
1✔
149
    lines.append("")
1✔
150

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

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

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

215
    derived_types = _collect_documented_derived_types(properties)
1✔
216
    if derived_types:
1✔
217
        lines.append("## Derived types")
1✔
218
        lines.append("")
1✔
219
        for derived in derived_types:
1✔
220
            _append_derived_type_documentation(lines, derived, shape_constants)
1✔
221

222
    lines.append("## Example")
1✔
223
    lines.append("")
1✔
224
    lines.append("```fortran")
1✔
225
    filled_template = render_template(
1✔
226
        [schema],
227
        doc_mode="plain",
228
        value_mode="filled",
229
        constants=shape_constants,
230
        kind_map=None,
231
        kind_allowlist=None,
232
    )
233
    lines.extend(filled_template.rstrip("\n").splitlines())
1✔
234
    lines.append("```")
1✔
235
    lines.append("")
1✔
236

237
    return "\n".join(lines) + "\n"
1✔
238

239

240
def _validate_required(values: list[Any]) -> set[str]:
1✔
241
    required: set[str] = set()
1✔
242
    for value in values:
1✔
243
        if not isinstance(value, str):
×
244
            raise ValueError("schema 'required' entries must be strings")
×
245
        required.add(value)
×
246
    return required
1✔
247

248

249
def _format_table_type(type_info: FieldTypeInfo) -> str:
1✔
250
    if type_info.category == "array":
1✔
251
        if type_info.element_category == "derived":
1✔
252
            return f"{type_info.type_spec} array"
1✔
253
        element = _format_scalar_type_name(type_info.element_category)
1✔
254
        return f"{element} array"
1✔
255
    if type_info.category == "derived":
1✔
256
        return type_info.type_spec
1✔
257
    return _format_scalar_type_name(type_info.category)
1✔
258

259

260
def _format_scalar_type_name(category: str | None) -> str:
1✔
261
    if category == "boolean":
1✔
262
        return "logical"
×
263
    if category == "string":
1✔
264
        return "string"
1✔
265
    if category == "integer":
1✔
266
        return "integer"
1✔
267
    if category == "real":
1✔
268
        return "real"
1✔
269
    raise ValueError(f"unsupported type category '{category}'")
×
270

271

272
def _format_specific_type(type_info: FieldTypeInfo) -> str:
1✔
273
    if type_info.category != "array":
1✔
274
        return type_info.type_spec
1✔
275
    dimensions = ", ".join(type_info.dimensions)
1✔
276
    return f"{type_info.type_spec}, dimension({dimensions})"
1✔
277

278

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

308

309
def _collect_documented_derived_types(properties: dict[str, Any]) -> list[dict[str, Any]]:
1✔
310
    seen: set[tuple[str, str]] = set()
1✔
311
    collected: list[dict[str, Any]] = []
1✔
312
    for prop in properties.values():
1✔
313
        if not isinstance(prop, dict):
1✔
NEW
314
            continue
×
315
        derived = _derived_schema(prop)
1✔
316
        if derived is None:
1✔
317
            continue
1✔
318
        origin = _derived_origin(derived)
1✔
319
        identity = origin["identity"]
1✔
320
        if identity in seen:
1✔
321
            continue
1✔
322
        seen.add(identity)
1✔
323
        collected.append(origin["definition"])
1✔
324
    return collected
1✔
325

326

327
def _append_derived_type_documentation(
1✔
328
    lines: list[str],
329
    derived: dict[str, Any],
330
    constants: dict[str, int] | None,
331
) -> None:
332
    type_name = derived.get("x-fortran-type")
1✔
333
    if not isinstance(type_name, str):
1✔
NEW
334
        return
×
335
    title = derived.get("title", type_name)
1✔
336
    lines.append(f"### `{type_name}`")
1✔
337
    lines.append("")
1✔
338
    if isinstance(title, str) and title != type_name:
1✔
339
        lines.append(title)
1✔
340
        lines.append("")
1✔
341
    description = derived.get("description")
1✔
342
    if isinstance(description, str) and description.strip():
1✔
343
        lines.append(description.strip())
1✔
344
        lines.append("")
1✔
345
    module = derived.get("x-fortran-module")
1✔
346
    ownership = f"imported from `{module}`" if isinstance(module, str) else "`nml_helper`"
1✔
347
    lines.append(f"- Ownership: {ownership}")
1✔
348
    components = derived.get("properties")
1✔
349
    if isinstance(components, dict):
1✔
350
        for child_name, child in components.items():
1✔
351
            if not isinstance(child_name, str) or not isinstance(child, dict):
1✔
NEW
352
                continue
×
353
            info = _field_type_info(child, constants)
1✔
354
            lines.append(f"- `{child_name}`: `{_format_specific_type(info)}`")
1✔
355
    lines.append("")
1✔
356

357

358
def _get_default_value(
1✔
359
    prop: dict[str, Any],
360
    type_info: FieldTypeInfo,
361
    constants: dict[str, int] | None,
362
) -> str | tuple[str, str | None] | None:
363
    if type_info.category == "array":
1✔
364
        array_default = _array_default_value(prop)
1✔
365
        if array_default is None:
1✔
366
            return None
1✔
367
        default_value, default_from_items = array_default
1✔
368
        return _format_array_default_display(
1✔
369
            prop,
370
            type_info,
371
            constants,
372
            default_value=default_value,
373
            from_items=default_from_items,
374
        )
375
    if "default" not in prop:
1✔
376
        return None
1✔
377
    return _format_default_plain(prop["default"], type_info, prop, constants)
1✔
378

379

380
def _format_info(prop: dict[str, Any]) -> str:
1✔
381
    title = _get_title(prop)
1✔
382
    return title or "n/a"
1✔
383

384

385
def _get_title(prop: dict[str, Any]) -> str | None:
1✔
386
    title = prop.get("title")
1✔
387
    if title is None:
1✔
388
        return None
1✔
389
    if not isinstance(title, str):
1✔
390
        raise ValueError("property title must be a string")
×
391
    title = title.strip()
1✔
392
    return title or None
1✔
393

394

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

404

405
def _get_enum_values(
1✔
406
    prop: dict[str, Any],
407
    type_info: FieldTypeInfo,
408
    constants: dict[str, int] | None,
409
) -> str | None:
410
    enum_values = _enum_values(prop, type_info, constants)
1✔
411
    if enum_values is None:
1✔
412
        return None
1✔
413
    category = _enum_category(type_info)
1✔
414
    values = [_format_scalar_default(value, None, category) for value in enum_values]
1✔
415
    return ", ".join(f"`{value}`" for value in values)
1✔
416

417

418
def _get_bounds_labels(
1✔
419
    prop: dict[str, Any],
420
    type_info: FieldTypeInfo,
421
) -> list[str]:
422
    bounds = _bounds_spec(prop, type_info)
1✔
423
    if bounds is None:
1✔
424
        return []
1✔
425
    category = bounds["category"]
1✔
426
    labels: list[str] = []
1✔
427
    min_value = bounds["min_value"]
1✔
428
    max_value = bounds["max_value"]
1✔
429
    if min_value is not None:
1✔
430
        op = ">" if bounds["min_exclusive"] else ">="
1✔
431
        literal = _format_scalar_default(min_value, None, category)
1✔
432
        labels.append(f"- Minimum: `{op} {literal}`")
1✔
433
    if max_value is not None:
1✔
434
        op = "<" if bounds["max_exclusive"] else "<="
1✔
435
        literal = _format_scalar_default(max_value, None, category)
1✔
436
        labels.append(f"- Maximum: `{op} {literal}`")
1✔
437
    return labels
1✔
438

439

440
def _get_flex_tail_dims(
1✔
441
    prop: dict[str, Any],
442
    type_info: FieldTypeInfo,
443
) -> int:
444
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
445
    if flex_raw is None:
1✔
446
        flex_value = 0
1✔
447
    else:
448
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
×
449
            raise ValueError("property flex tail dims must be an integer")
×
450
        flex_value = flex_raw
×
451
    if flex_value < 0:
1✔
452
        raise ValueError("property flex tail dims must be >= 0")
×
453
    if flex_value == 0:
1✔
454
        return 0
1✔
455
    if type_info.category != "array":
×
456
        raise ValueError("flex tail dims only apply to arrays")
×
457
    if flex_value > len(type_info.dimensions):
×
458
        raise ValueError("flex tail dims must not exceed array rank")
×
459
    return flex_value
×
460

461

462
def _escape_table_cell(value: str) -> str:
1✔
463
    escaped = value.replace("|", "\\|").replace("\n", " ").strip()
1✔
464
    return escaped or "n/a"
1✔
465

466

467
def _get_example_values(
1✔
468
    prop: dict[str, Any],
469
    type_info: FieldTypeInfo,
470
) -> list[str] | None:
471
    examples = prop.get("examples")
1✔
472
    if examples is None:
1✔
473
        return None
1✔
474
    if not isinstance(examples, list):
×
475
        raise ValueError("property examples must be a list")
×
476
    if not examples:
×
477
        return None
×
478
    return [_format_example_value(value, type_info) for value in examples]
×
479

480

481
def _format_example_value(value: Any, type_info: FieldTypeInfo) -> str:
1✔
482
    if type_info.category == "array":
×
483
        if isinstance(value, list):
×
484
            formatted = [
×
485
                _format_scalar_default(item, None, type_info.element_category) for item in value
486
            ]
487
            return f"[{', '.join(formatted)}]"
×
488
        return _format_scalar_default(value, None, type_info.element_category)
×
489
    if isinstance(value, list):
×
490
        raise ValueError("scalar examples must not be lists")
×
491
    return _format_scalar_default(value, None, type_info.category)
×
492

493

494
def _format_default_plain(
1✔
495
    value: Any,
496
    type_info: FieldTypeInfo,
497
    prop: dict[str, Any],
498
    constants: dict[str, int] | None,
499
) -> str:
500
    if type_info.category == "array":
1✔
501
        if not isinstance(value, list):
×
502
            raise ValueError("array default must be a list")
×
503
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
504
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
505
        elements = [
×
506
            _format_scalar_default(element, None, type_info.element_category)
507
            for element in array_default.source_values
508
        ]
509
        if (
×
510
            len(type_info.dimensions) == 1
511
            and array_default.order_values is None
512
            and array_default.pad_values is None
513
        ):
514
            return f"[{', '.join(elements)}]"
×
515

516
        shape_literal = ", ".join(type_info.dimensions)
×
517
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
518

519
        if array_default.order_values is not None:
×
520
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
521
            arguments.append(f"order=[{order_literal}]")
×
522

523
        if array_default.pad_values is not None:
×
524
            pad_elements = [
×
525
                _format_scalar_default(element, None, type_info.element_category)
526
                for element in array_default.pad_values
527
            ]
528
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
529

530
        return f"reshape({', '.join(arguments)})"
×
531

532
    return _format_scalar_default(value, None, type_info.category)
1✔
533

534

535
def _format_array_default_display(
1✔
536
    prop: dict[str, Any],
537
    type_info: FieldTypeInfo,
538
    constants: dict[str, int] | None,
539
    *,
540
    default_value: Any = _DEFAULT_MISSING,
541
    from_items: bool = False,
542
) -> tuple[str, str | None]:
543
    if default_value is _DEFAULT_MISSING:
1✔
544
        default_value = prop["default"]
×
545
    if not from_items and not isinstance(default_value, list):
1✔
546
        raise ValueError("array default must be a list")
×
547
    default_is_list = isinstance(default_value, list)
1✔
548
    default_list = default_value if default_is_list else [default_value]
1✔
549

550
    parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
1✔
551
    default_prop = prop
1✔
552
    if from_items:
1✔
553
        default_prop = dict(prop)
1✔
554
        default_prop["x-fortran-default-repeat"] = True
1✔
555
    _prepare_array_default(default_list, parsed_dims, default_prop)
1✔
556

557
    if default_is_list:
1✔
558
        elements = [
1✔
559
            _format_scalar_default(value, None, type_info.element_category)
560
            for value in default_list
561
        ]
562
        base = f"[{', '.join(elements)}]"
1✔
563
    else:
564
        base = _format_scalar_default(default_value, None, type_info.element_category)
1✔
565

566
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
567
    if not isinstance(repeat_raw, bool):
1✔
568
        raise ValueError("array default repeat must be a boolean")
×
569
    notes: list[str] = []
1✔
570
    if repeat_raw:
1✔
571
        notes.append("repeated")
1✔
572

573
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
574
    if not isinstance(order_raw, str):
1✔
575
        raise ValueError("array default order must be 'F' or 'C'")
×
576
    order = order_raw.upper()
1✔
577
    if order not in {"F", "C"}:
1✔
578
        raise ValueError("array default order must be 'F' or 'C'")
×
579
    if order == "C":
1✔
580
        notes.append("order: C")
×
581

582
    pad_raw = prop.get("x-fortran-default-pad")
1✔
583
    if pad_raw is not None:
1✔
584
        pad_list = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
585
        pad_elements = [
×
586
            _format_scalar_default(value, None, type_info.element_category) for value in pad_list
587
        ]
588
        if isinstance(pad_raw, list):
×
589
            pad_text = f"[{', '.join(pad_elements)}]"
×
590
        else:
591
            pad_text = pad_elements[0]
×
592
        notes.append(f"pad: `{pad_text}`")
×
593

594
    if notes:
1✔
595
        return base, f"({', '.join(notes)})"
1✔
596
    return base, None
1✔
597

598

599
def _github_section_id(title: str) -> str:
1✔
600
    # lowercase
601
    s = title.strip().lower()
1✔
602
    # remove punctuation characters
603
    punctuation = string.punctuation.replace("_", "")
1✔
604
    s = re.sub(f"[{re.escape(punctuation)}]", "", s)
1✔
605
    # replace any whitespace run with '-'
606
    s = re.sub(r"\s+", "-", s)
1✔
607
    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