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

MuellerSeb / nml-tools / 26209372979

21 May 2026 06:25AM UTC coverage: 79.274% (+0.2%) from 79.105%
26209372979

Pull #28

github

MuellerSeb
Avoid helper alias clashes with property defaults
Pull Request #28: Simplify generated internal naming with indexed clash handling

62 of 69 new or added lines in 2 files covered. (89.86%)

1 existing line in 1 file now uncovered.

2620 of 3305 relevant lines covered (79.27%)

0.79 hits per line

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

92.94
/src/nml_tools/codegen_f2py.py
1
"""f2py and Python wrapper code generation."""
2

3
from __future__ import annotations
1✔
4

5
import keyword
1✔
6
import re
1✔
7
from dataclasses import dataclass
1✔
8
from pathlib import Path
1✔
9
from typing import Any, Iterable, cast
1✔
10

11
from jinja2 import Environment, FileSystemLoader, StrictUndefined
1✔
12

13
from ._utils import (
1✔
14
    normalize_constant_values,
15
    normalize_runtime_dimensions,
16
    reject_constant_dimension_overlap,
17
    strip_trailing_whitespace,
18
)
19
from .codegen_fortran import (
1✔
20
    FieldSpec,
21
    FieldTypeInfo,
22
    _build_context,
23
    _field_type_info,
24
    _reject_runtime_dimension_lengths,
25
)
26

27
_TEMPLATE_ENV = Environment(
1✔
28
    loader=FileSystemLoader(Path(__file__).resolve().parent / "templates"),
29
    trim_blocks=True,
30
    lstrip_blocks=False,
31
    keep_trailing_newline=True,
32
    undefined=StrictUndefined,
33
)
34

35

36
@dataclass
1✔
37
class F2pyArgumentSpec:
1✔
38
    """Python wrapper argument metadata."""
39

40
    name: str
1✔
41
    title: str
1✔
42
    required: bool
1✔
43
    rank: int
1✔
44
    numpy_dtype: str | None
1✔
45
    dummy_value: str
1✔
46
    doc_type: str
1✔
47
    requirement: str
1✔
48
    has_flag: str | None = None
1✔
49
    fixed_shape: list[int] | None = None
1✔
50
    python_name: str | None = None
1✔
51

52

53
@dataclass
1✔
54
class F2pyArrayDimensionSpec:
1✔
55
    """Dimension arguments for a f2py-visible array dummy."""
56

57
    field_name: str
1✔
58
    names: list[str]
1✔
59

60

61
@dataclass
1✔
62
class F2pyNamelistSpec:
1✔
63
    """Metadata needed for f2py wrapper generation."""
64

65
    namelist_name: str
1✔
66
    brief: str
1✔
67
    details: str
1✔
68
    details_lines: list[str]
1✔
69
    module_name: str
1✔
70
    type_name: str
1✔
71
    helper_module: str
1✔
72
    kind_module: str
1✔
73
    kind_imports: list[str]
1✔
74
    f2py_module_name: str
1✔
75
    resolve_handle_name: str
1✔
76
    handle_ctype: str
1✔
77
    errmsg_len: int
1✔
78
    argument_list: list[str]
1✔
79
    argument_declarations: list[str]
1✔
80
    bridge_declarations: list[str]
1✔
81
    bridge_assignments: list[str]
1✔
82
    set_call_arguments: list[str]
1✔
83
    set_dims_argument_list: list[str]
1✔
84
    set_dims_argument_declarations: list[str]
1✔
85
    set_dims_bridge_declarations: list[str]
1✔
86
    set_dims_bridge_assignments: list[str]
1✔
87
    set_dims_call_arguments: list[str]
1✔
88
    set_dims_args: list[F2pyArgumentSpec]
1✔
89
    array_dimensions: list[F2pyArrayDimensionSpec]
1✔
90
    required_args: list[F2pyArgumentSpec]
1✔
91
    optional_args: list[F2pyArgumentSpec]
1✔
92
    all_args: list[F2pyArgumentSpec]
1✔
93

94

95
@dataclass
1✔
96
class PythonWrapperSpec:
1✔
97
    """Metadata needed for Python wrapper generation."""
98

99
    class_name: str
1✔
100
    namelist_name: str
1✔
101
    brief: str
1✔
102
    f2py_module_name: str
1✔
103
    extension_module: str
1✔
104
    required_args: list[F2pyArgumentSpec]
1✔
105
    optional_args: list[F2pyArgumentSpec]
1✔
106
    all_args: list[F2pyArgumentSpec]
1✔
107
    set_dims_args: list[F2pyArgumentSpec]
1✔
108

109

110
@dataclass
1✔
111
class F2pyKindUsage:
1✔
112
    """Kind aliases used by f2py wrapper dummy arguments."""
113

114
    real: set[str]
1✔
115
    integer: set[str]
1✔
116

117

118
@dataclass
1✔
119
class F2pyCTypeMap:
1✔
120
    """Explicit C type mapping for f2py kinds."""
121

122
    real: dict[str, str]
1✔
123
    integer: dict[str, str]
1✔
124

125

126
def generate_f2py_wrappers(
1✔
127
    schemas: Iterable[dict[str, Any]],
128
    output: str | Path,
129
    *,
130
    helper_module: str = "nml_helper",
131
    kind_module: str | None = None,
132
    kind_map: dict[str, str] | None = None,
133
    kind_allowlist: Iterable[str] | None = None,
134
    constants: dict[str, int] | None = None,
135
    dimensions: dict[str, int] | None = None,
136
    errmsg_len: int = 1024,
137
) -> None:
138
    """Generate f2py-facing Fortran wrappers for *schemas* at *output*."""
139
    output_path = Path(output)
1✔
140
    rendered = render_f2py_wrappers(
1✔
141
        schemas,
142
        file_name=output_path.name,
143
        helper_module=helper_module,
144
        kind_module=kind_module,
145
        kind_map=kind_map,
146
        kind_allowlist=kind_allowlist,
147
        constants=constants,
148
        dimensions=dimensions,
149
        errmsg_len=errmsg_len,
150
    )
151
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
152
    output_path.write_text(rendered, encoding="ascii")
1✔
153

154

155
def render_f2py_wrappers(
1✔
156
    schemas: Iterable[dict[str, Any]],
157
    *,
158
    file_name: str,
159
    helper_module: str = "nml_helper",
160
    kind_module: str | None = None,
161
    kind_map: dict[str, str] | None = None,
162
    kind_allowlist: Iterable[str] | None = None,
163
    constants: dict[str, int] | None = None,
164
    dimensions: dict[str, int] | None = None,
165
    errmsg_len: int = 1024,
166
) -> str:
167
    """Render f2py-facing Fortran wrappers for *schemas*."""
168
    specs = [
1✔
169
        build_f2py_namelist_spec(
170
            schema,
171
            helper_module=helper_module,
172
            kind_module=kind_module,
173
            kind_map=kind_map,
174
            kind_allowlist=kind_allowlist,
175
            constants=constants,
176
            dimensions=dimensions,
177
            errmsg_len=errmsg_len,
178
        )
179
        for schema in schemas
180
    ]
181
    rendered = _TEMPLATE_ENV.get_template("f2py_wrappers.f90.j2").render(
1✔
182
        {"file_name": file_name, "specs": specs}
183
    )
184
    return strip_trailing_whitespace(rendered)
1✔
185

186

187
def generate_python_wrappers(
1✔
188
    specs: Iterable[tuple[F2pyNamelistSpec, str]],
189
    output: str | Path,
190
    *,
191
    py_style: str = "numpy",
192
) -> None:
193
    """Generate Python wrapper classes for f2py namelist *specs* at *output*."""
194
    rendered = render_python_wrappers(specs, py_style=py_style)
1✔
195
    output_path = Path(output)
1✔
196
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
197
    output_path.write_text(rendered, encoding="ascii")
1✔
198

199

200
def render_python_wrappers(
1✔
201
    specs: Iterable[tuple[F2pyNamelistSpec, str]],
202
    *,
203
    py_style: str = "numpy",
204
) -> str:
205
    """Render Python wrapper classes for f2py namelist *specs*."""
206
    if py_style not in {"numpy", "doxygen"}:
1✔
207
        raise ValueError("python documentation style must be 'numpy' or 'doxygen'")
1✔
208
    spec_entries = list(specs)
1✔
209
    extension_modules: set[str] = set()
1✔
210
    for _, extension_module in spec_entries:
1✔
211
        _validate_python_module_name(extension_module)
1✔
212
        extension_modules.add(extension_module)
1✔
213
    classes: list[PythonWrapperSpec] = []
1✔
214
    for spec, extension_module in spec_entries:
1✔
215
        classes.append(
1✔
216
            PythonWrapperSpec(
217
                class_name=_class_name(spec.namelist_name),
218
                namelist_name=spec.namelist_name,
219
                brief=spec.brief,
220
                f2py_module_name=spec.f2py_module_name,
221
                extension_module=extension_module,
222
                required_args=spec.required_args,
223
                optional_args=spec.optional_args,
224
                all_args=spec.all_args,
225
                set_dims_args=spec.set_dims_args,
226
            )
227
        )
228
    rendered = _TEMPLATE_ENV.get_template("python_wrappers.py.j2").render(
1✔
229
        {"imports": sorted(extension_modules), "classes": classes, "py_style": py_style}
230
    )
231
    return strip_trailing_whitespace(rendered)
1✔
232

233

234
def generate_f2cmap(
1✔
235
    output: str | Path,
236
    usage: F2pyKindUsage,
237
    c_types: F2pyCTypeMap,
238
) -> None:
239
    """Generate a .f2py_f2cmap file for the explicitly mapped *usage*."""
240
    rendered = render_f2cmap(usage, c_types)
1✔
241
    output_path = Path(output)
1✔
242
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
243
    output_path.write_text(rendered, encoding="ascii")
1✔
244

245

246
def render_f2cmap(
1✔
247
    usage: F2pyKindUsage,
248
    c_types: F2pyCTypeMap,
249
) -> str:
250
    """Render a .f2py_f2cmap file for the explicitly mapped *usage*."""
251
    missing_real = sorted(usage.real - set(c_types.real))
1✔
252
    missing_integer = sorted(usage.integer - set(c_types.integer))
1✔
253
    if missing_real:
1✔
254
        raise ValueError("missing f2py real C type mappings: " + ", ".join(missing_real))
1✔
255
    if missing_integer:
1✔
256
        raise ValueError(
×
257
            "missing f2py integer C type mappings: " + ", ".join(missing_integer)
258
        )
259

260
    integer_map = dict(c_types.integer)
1✔
261
    integer_map.setdefault("c_intptr_t", "long_long")
1✔
262
    real_items = ", ".join(
1✔
263
        f"{name}={c_types.real[name]!r}" for name in sorted(usage.real)
264
    )
265
    integer_items = ", ".join(
1✔
266
        f"{name}={integer_map[name]!r}" for name in sorted(usage.integer | {"c_intptr_t"})
267
    )
268
    return f"dict(real=dict({real_items}), integer=dict({integer_items}))\n"
1✔
269

270

271
def collect_f2py_kind_usage(
1✔
272
    schemas: Iterable[dict[str, Any]],
273
    *,
274
    constants: dict[str, int] | None = None,
275
    dimensions: dict[str, int] | None = None,
276
) -> F2pyKindUsage:
277
    """Collect schema kind aliases used in f2py wrapper arguments."""
278
    usage = F2pyKindUsage(real=set(), integer=set())
1✔
279
    for schema in schemas:
1✔
280
        for _, type_info in _iter_field_type_infos(schema, constants, dimensions):
1✔
281
            category = (
1✔
282
                type_info.element_category
283
                if type_info.category == "array"
284
                else type_info.category
285
            )
286
            if type_info.kind is None:
1✔
287
                continue
1✔
288
            if category == "real":
1✔
289
                usage.real.add(type_info.kind)
1✔
290
            elif category == "integer":
1✔
291
                usage.integer.add(type_info.kind)
1✔
292
    return usage
1✔
293

294

295
def merge_f2py_kind_usage(usages: Iterable[F2pyKindUsage]) -> F2pyKindUsage:
1✔
296
    """Merge multiple f2py kind usage objects."""
297
    merged = F2pyKindUsage(real=set(), integer=set())
1✔
298
    for usage in usages:
1✔
299
        merged.real.update(usage.real)
1✔
300
        merged.integer.update(usage.integer)
1✔
301
    return merged
1✔
302

303

304
def build_f2py_namelist_spec(
1✔
305
    schema: dict[str, Any],
306
    *,
307
    helper_module: str = "nml_helper",
308
    kind_module: str | None = None,
309
    kind_map: dict[str, str] | None = None,
310
    kind_allowlist: Iterable[str] | None = None,
311
    constants: dict[str, int] | None = None,
312
    dimensions: dict[str, int] | None = None,
313
    errmsg_len: int = 1024,
314
) -> F2pyNamelistSpec:
315
    """Build f2py wrapper metadata for one namelist schema."""
316
    context = _build_context(
1✔
317
        schema,
318
        helper_module=helper_module,
319
        kind_module=kind_module,
320
        kind_map=kind_map,
321
        kind_allowlist=kind_allowlist,
322
        constants=constants,
323
        dimensions=dimensions,
324
        module_doc=None,
325
    )
326
    fields = cast("list[FieldSpec]", context["fields"])
1✔
327
    type_infos = {
1✔
328
        name: type_info
329
        for name, type_info in _iter_field_type_infos(schema, constants, dimensions)
330
    }
331
    required_args: list[F2pyArgumentSpec] = []
1✔
332
    optional_args: list[F2pyArgumentSpec] = []
1✔
333
    argument_list: list[str] = []
1✔
334
    argument_declarations: list[str] = []
1✔
335
    bridge_declarations: list[str] = []
1✔
336
    bridge_assignments: list[str] = []
1✔
337
    set_call_arguments: list[str] = []
1✔
338
    set_dims_argument_list: list[str] = []
1✔
339
    set_dims_argument_declarations: list[str] = []
1✔
340
    set_dims_bridge_declarations: list[str] = []
1✔
341
    set_dims_bridge_assignments: list[str] = []
1✔
342
    set_dims_call_arguments: list[str] = []
1✔
343
    set_dims_args: list[F2pyArgumentSpec] = []
1✔
344
    array_dimensions: list[F2pyArrayDimensionSpec] = []
1✔
345

346
    field_argument_names: set[str] = set()
1✔
347
    for field in fields:
1✔
348
        field_type_info = type_infos[field.name]
1✔
349
        field_args, _ = _f2py_field_arguments(field, field_type_info)
1✔
350
        field_argument_names.update(name.lower() for name in field_args)
1✔
351

352
    argument_names_in_use: set[str] = set(field_argument_names)
1✔
353
    bridge_names_in_use: set[str] = set(field_argument_names) | {
1✔
354
        "handle",
355
        "status",
356
        "errmsg",
357
        "this",
358
    }
359

360
    for field in fields:
1✔
361
        type_info = type_infos[field.name]
1✔
362
        rank = len(type_info.dimensions) if type_info.category == "array" else 0
1✔
363
        has_flag: str | None = None
1✔
364
        if not field.required:
1✔
365
            has_base = f"has_{field.name}"
1✔
366
            has_flag = _unique_generated_name(has_base, argument_names_in_use)
1✔
367
            argument_names_in_use.add(has_flag.lower())
1✔
368
        spec = F2pyArgumentSpec(
1✔
369
            name=field.name,
370
            title=_one_line(field.title),
371
            required=field.required,
372
            rank=rank,
373
            numpy_dtype=_numpy_dtype(type_info),
374
            dummy_value=_python_dummy_value(type_info),
375
            doc_type=_python_doc_type(type_info),
376
            requirement="required" if field.required else "optional",
377
            has_flag=has_flag,
378
        )
379
        if field.required:
1✔
380
            required_args.append(spec)
1✔
381
        else:
382
            optional_args.append(spec)
1✔
383
        field_arguments, field_declarations = _f2py_field_arguments(field, type_info)
1✔
384
        argument_list.extend(field_arguments)
1✔
385
        argument_declarations.extend(field_declarations)
1✔
386
        if rank > 0:
1✔
387
            dim_names = _array_dimension_argument_names(field.name, rank)
1✔
388
            array_dimensions.append(
1✔
389
                F2pyArrayDimensionSpec(field_name=field.name, names=dim_names)
390
            )
391
        if has_flag is not None:
1✔
392
            argument_list.append(has_flag)
1✔
393
            argument_declarations.append(
1✔
394
                f"logical, intent(in) :: {has_flag} !< whether {field.name} was provided"
395
            )
396
            maybe_base = _maybe_bridge_name(field.name)
1✔
397
            maybe_name = _unique_generated_name(maybe_base, bridge_names_in_use)
1✔
398
            bridge_names_in_use.add(maybe_name.lower())
1✔
399
            bridge_declarations.append(
1✔
400
                _optional_bridge_declaration(field.name, type_info, maybe_name)
401
            )
402
            bridge_assignments.append(
1✔
403
                _optional_bridge_assignment(field.name, type_info, has_flag, maybe_name)
404
            )
405
            set_call_arguments.append(f"{field.name}={maybe_name}")
1✔
406
        else:
407
            set_call_arguments.append(f"{field.name}={field.name}")
1✔
408

409
    runtime_dimension_args = cast("list[dict[str, str]]", context["set_dims_arguments"])
1✔
410
    set_dims_argument_names_in_use: set[str] = {
1✔
411
        str(entry["name"]).lower() for entry in runtime_dimension_args
412
    }
413
    set_dims_bridge_names_in_use: set[str] = set(set_dims_argument_names_in_use) | {
1✔
414
        "handle",
415
        "status",
416
        "errmsg",
417
        "this",
418
    }
419

420
    for entry in runtime_dimension_args:
1✔
421
        const_name = entry["name"]
1✔
422
        arg_name = entry["arg_name"]
1✔
423
        python_name = _python_parameter_name(const_name)
1✔
424
        has_base = f"has_{const_name}"
1✔
425
        has_flag = _unique_generated_name(has_base, set_dims_argument_names_in_use)
1✔
426
        set_dims_argument_names_in_use.add(has_flag.lower())
1✔
427
        set_dims_args.append(
1✔
428
            F2pyArgumentSpec(
429
                name=const_name,
430
                title=f"Runtime dimension override for {const_name}",
431
                required=False,
432
                rank=0,
433
                numpy_dtype="int",
434
                dummy_value="0",
435
                doc_type="int",
436
                requirement="optional",
437
                has_flag=has_flag,
438
                fixed_shape=None,
439
                python_name=python_name,
440
            )
441
        )
442
        set_dims_argument_list.append(const_name)
1✔
443
        set_dims_argument_declarations.append(
1✔
444
            f"integer, intent(in) :: {const_name} !< runtime dimension override for {const_name}"
445
        )
446
        set_dims_argument_list.append(has_flag)
1✔
447
        set_dims_argument_declarations.append(
1✔
448
            f"logical, intent(in) :: {has_flag} !< whether {const_name} was provided"
449
        )
450
        maybe_base = _maybe_bridge_name(const_name)
1✔
451
        maybe_name = _unique_generated_name(maybe_base, set_dims_bridge_names_in_use)
1✔
452
        set_dims_bridge_names_in_use.add(maybe_name.lower())
1✔
453
        set_dims_bridge_declarations.append(f"integer, allocatable :: {maybe_name}")
1✔
454
        set_dims_bridge_assignments.append(
1✔
455
            f"if ({has_flag}) then\n"
456
            f"  allocate({maybe_name})\n"
457
            f"  {maybe_name} = {const_name}\n"
458
            "end if"
459
        )
460
        set_dims_call_arguments.append(f"{arg_name}={maybe_name}")
1✔
461

462
    namelist_name = cast("str", context["namelist_name"])
1✔
463
    details = cast("str", context["details_text"])
1✔
464
    return F2pyNamelistSpec(
1✔
465
        namelist_name=namelist_name,
466
        brief=_one_line(cast("str", context["brief_text"])),
467
        details=details,
468
        details_lines=details.splitlines() or [details],
469
        module_name=cast("str", context["module_name"]),
470
        type_name=cast("str", context["type_name"]),
471
        helper_module=helper_module,
472
        kind_module=cast("str", context["kind_module"]),
473
        kind_imports=cast("list[str]", context["kind_imports"]),
474
        f2py_module_name=f"f2py_{namelist_name}",
475
        resolve_handle_name=f"{context['module_name']}_resolve_handle",
476
        handle_ctype="c_intptr_t",
477
        errmsg_len=errmsg_len,
478
        argument_list=argument_list,
479
        argument_declarations=argument_declarations,
480
        bridge_declarations=bridge_declarations,
481
        bridge_assignments=bridge_assignments,
482
        set_call_arguments=set_call_arguments,
483
        set_dims_argument_list=set_dims_argument_list,
484
        set_dims_argument_declarations=set_dims_argument_declarations,
485
        set_dims_bridge_declarations=set_dims_bridge_declarations,
486
        set_dims_bridge_assignments=set_dims_bridge_assignments,
487
        set_dims_call_arguments=set_dims_call_arguments,
488
        set_dims_args=set_dims_args,
489
        array_dimensions=array_dimensions,
490
        required_args=required_args,
491
        optional_args=optional_args,
492
        all_args=required_args + optional_args,
493
    )
494

495

496
def _iter_field_type_infos(
1✔
497
    schema: dict[str, Any],
498
    constants: dict[str, int] | None,
499
    dimensions: dict[str, int] | None = None,
500
) -> list[tuple[str, FieldTypeInfo]]:
501
    properties = _normalized_properties(schema)
1✔
502
    constants = normalize_constant_values(constants)
1✔
503
    runtime_dimension_values = normalize_runtime_dimensions(dimensions)
1✔
504
    reject_constant_dimension_overlap(constants, runtime_dimension_values)
1✔
505
    field_types: list[tuple[str, FieldTypeInfo]] = []
1✔
506
    for name, prop in properties.items():
1✔
507
        _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
508
        field_types.append((name, _field_type_info(prop, constants)))
1✔
509
    return field_types
1✔
510

511

512
def _normalized_properties(schema: dict[str, Any]) -> dict[str, dict[str, Any]]:
1✔
513
    properties = schema.get("properties")
1✔
514
    if not isinstance(properties, dict):
1✔
515
        raise ValueError("schema must define object 'properties'")
×
516
    normalized: dict[str, dict[str, Any]] = {}
1✔
517
    seen: set[str] = set()
1✔
518
    for raw_name, prop in properties.items():
1✔
519
        if not isinstance(raw_name, str):
1✔
520
            raise ValueError("property names must be strings")
×
521
        if not isinstance(prop, dict):
1✔
522
            raise ValueError(f"property '{raw_name}' must be an object")
×
523
        name = raw_name.lower()
1✔
524
        if name in seen:
1✔
525
            raise ValueError(f"duplicate property '{raw_name}'")
×
526
        seen.add(name)
1✔
527
        normalized[name] = prop
1✔
528
    return normalized
1✔
529

530

531
def _class_name(namelist_name: str) -> str:
1✔
532
    parts = [part for part in re.split(r"[^0-9A-Za-z]+", namelist_name) if part]
1✔
533
    if not parts:
1✔
534
        return "Namelist"
×
535
    name = "".join(part[:1].upper() + part[1:] for part in parts)
1✔
536
    if name[0].isdigit():
1✔
537
        name = f"Namelist{name}"
×
538
    if keyword.iskeyword(name):
1✔
539
        name = f"{name}Namelist"
×
540
    return name
1✔
541

542

543
def _python_parameter_name(name: str) -> str:
1✔
544
    if not name.isidentifier():
1✔
545
        raise ValueError(f"name '{name}' is not a valid Python identifier")
×
546
    if keyword.iskeyword(name):
1✔
547
        return f"{name}_"
1✔
548
    return name
1✔
549

550

551
def _numpy_dtype(type_info: FieldTypeInfo) -> str | None:
1✔
552
    category = (
1✔
553
        type_info.element_category
554
        if type_info.category == "array"
555
        else type_info.category
556
    )
557
    if category == "real":
1✔
558
        return "float"
1✔
559
    if category == "integer":
1✔
560
        return "int"
1✔
561
    if category == "boolean":
1✔
562
        return "bool"
×
563
    if category == "string":
1✔
564
        return "str"
1✔
565
    return None
×
566

567

568
def _python_dummy_value(type_info: FieldTypeInfo) -> str:
1✔
569
    category = (
1✔
570
        type_info.element_category
571
        if type_info.category == "array"
572
        else type_info.category
573
    )
574
    if category == "real":
1✔
575
        return "0.0"
1✔
576
    if category == "integer":
1✔
577
        return "0"
1✔
578
    if category == "boolean":
1✔
579
        return "False"
×
580
    if category == "string":
1✔
581
        return '""'
1✔
582
    return "None"
×
583

584

585
def _python_doc_type(type_info: FieldTypeInfo) -> str:
1✔
586
    category = (
1✔
587
        type_info.element_category
588
        if type_info.category == "array"
589
        else type_info.category
590
    )
591
    if category == "real":
1✔
592
        type_name = "float"
1✔
593
    elif category == "integer":
1✔
594
        type_name = "int"
1✔
595
    elif category == "boolean":
1✔
596
        type_name = "bool"
×
597
    elif category == "string":
1✔
598
        type_name = "str"
1✔
599
    else:
600
        type_name = "Any"
×
601
    if type_info.category == "array":
1✔
602
        return f"array_like of {type_name}"
1✔
603
    return type_name
1✔
604

605

606
def _one_line(value: str) -> str:
1✔
607
    return " ".join(value.splitlines()).strip()
1✔
608

609

610
def _array_dimension_argument_names(name: str, rank: int) -> list[str]:
1✔
611
    return [f"{name}_n{idx}" for idx in range(1, rank + 1)]
1✔
612

613

614
def _unique_generated_name(base_name: str, taken_names: set[str]) -> str:
1✔
615
    if base_name.lower() not in taken_names:
1✔
616
        return base_name
1✔
NEW
617
    index = 1
×
NEW
618
    while True:
×
NEW
619
        candidate = f"{base_name}_{index}"
×
NEW
620
        if candidate.lower() not in taken_names:
×
NEW
621
            return candidate
×
NEW
622
        index += 1
×
623

624

625
def _maybe_bridge_name(name: str) -> str:
1✔
626
    return f"maybe_{name}"
1✔
627

628

629
def _f2py_field_arguments(
1✔
630
    field: FieldSpec,
631
    type_info: FieldTypeInfo,
632
) -> tuple[list[str], list[str]]:
633
    requirement = "required" if field.required else "optional"
1✔
634
    if type_info.category != "array":
1✔
635
        return [field.name], [
1✔
636
            f"{type_info.arg_type_spec}, intent(in) :: {field.name} "
637
            f"!< {_one_line(field.title)} ({requirement})"
638
        ]
639

640
    dim_names = _array_dimension_argument_names(field.name, len(type_info.dimensions))
1✔
641
    dims = ", ".join(dim_names)
1✔
642
    declarations = [
1✔
643
        f"integer, intent(in) :: {dim_name} !< extent for {field.name}"
644
        for dim_name in dim_names
645
    ]
646
    declarations.append(
1✔
647
        f"{type_info.arg_type_spec}, dimension({dims}), intent(in) :: {field.name} "
648
        f"!< {_one_line(field.title)} ({requirement})"
649
    )
650
    return [*dim_names, field.name], declarations
1✔
651

652

653
def _optional_bridge_declaration(name: str, type_info: FieldTypeInfo, maybe_name: str) -> str:
1✔
654
    if type_info.category == "array":
1✔
655
        dims = ", ".join(":" for _ in type_info.dimensions)
1✔
656
        if type_info.element_category == "string":
1✔
657
            return f"character(len=:), dimension({dims}), allocatable :: {maybe_name}"
1✔
658
        return f"{type_info.arg_type_spec}, dimension({dims}), allocatable :: {maybe_name}"
1✔
659
    if type_info.category == "string":
1✔
NEW
660
        return f"character(len=:), allocatable :: {maybe_name}"
×
661
    return f"{type_info.arg_type_spec}, allocatable :: {maybe_name}"
1✔
662

663

664
def _optional_bridge_assignment(
1✔
665
    name: str,
666
    type_info: FieldTypeInfo,
667
    has_flag: str,
668
    maybe_name: str,
669
) -> str:
670
    if type_info.category == "array":
1✔
671
        dims = ", ".join(_array_dimension_argument_names(name, len(type_info.dimensions)))
1✔
672
        allocate_stmt = f"allocate({maybe_name}({dims}))"
1✔
673
        if type_info.element_category == "string":
1✔
674
            allocate_stmt = f"allocate(character(len=len({name})) :: {maybe_name}({dims}))"
1✔
675
        return (
1✔
676
            f"if ({has_flag}) then\n"
677
            f"  {allocate_stmt}\n"
678
            f"  {maybe_name} = {name}\n"
679
            "end if"
680
        )
681
    if type_info.category == "string":
1✔
682
        return (
×
683
            f"if ({has_flag}) then\n"
684
            f"  {maybe_name} = {name}\n"
685
            "end if"
686
        )
687
    return (
1✔
688
        f"if ({has_flag}) then\n"
689
        f"  allocate({maybe_name})\n"
690
        f"  {maybe_name} = {name}\n"
691
        "end if"
692
    )
693

694

695
def _validate_python_module_name(module_name: str) -> None:
1✔
696
    if not module_name.isidentifier() or keyword.iskeyword(module_name):
1✔
697
        raise ValueError(
×
698
            f"f2py extension module name '{module_name}' must be a valid Python identifier"
699
        )
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