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

MuellerSeb / nml-tools / 26188801807

20 May 2026 08:42PM UTC coverage: 79.048% (+4.7%) from 74.315%
26188801807

Pull #27

github

MuellerSeb
reorder README sections
Pull Request #27: Add Runtime Array Dimensions

439 of 492 new or added lines in 7 files covered. (89.23%)

3 existing lines in 2 files now uncovered.

2592 of 3279 relevant lines covered (79.05%)

0.79 hits per line

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

93.93
/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 strip_trailing_whitespace
1✔
14
from .codegen_fortran import (
1✔
15
    FieldSpec,
16
    FieldTypeInfo,
17
    _build_context,
18
    _field_type_info,
19
    _normalize_constant_values,
20
    _reject_runtime_dimension_lengths,
21
    _validate_runtime_dimensions,
22
)
23

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

32

33
@dataclass
1✔
34
class F2pyArgumentSpec:
1✔
35
    """Python wrapper argument metadata."""
36

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

49

50
@dataclass
1✔
51
class F2pyArrayDimensionSpec:
1✔
52
    """Dimension arguments for a f2py-visible array dummy."""
53

54
    field_name: str
1✔
55
    names: list[str]
1✔
56

57

58
@dataclass
1✔
59
class F2pyNamelistSpec:
1✔
60
    """Metadata needed for f2py wrapper generation."""
61

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

91

92
@dataclass
1✔
93
class PythonWrapperSpec:
1✔
94
    """Metadata needed for Python wrapper generation."""
95

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

106

107
@dataclass
1✔
108
class F2pyKindUsage:
1✔
109
    """Kind aliases used by f2py wrapper dummy arguments."""
110

111
    real: set[str]
1✔
112
    integer: set[str]
1✔
113

114

115
@dataclass
1✔
116
class F2pyCTypeMap:
1✔
117
    """Explicit C type mapping for f2py kinds."""
118

119
    real: dict[str, str]
1✔
120
    integer: dict[str, str]
1✔
121

122

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

151

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

183

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

196

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

230

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

242

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

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

267

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

291

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

300

301
def build_f2py_namelist_spec(
1✔
302
    schema: dict[str, Any],
303
    *,
304
    helper_module: str = "nml_helper",
305
    kind_module: str | None = None,
306
    kind_map: dict[str, str] | None = None,
307
    kind_allowlist: Iterable[str] | None = None,
308
    constants: dict[str, int] | None = None,
309
    dimensions: dict[str, int] | None = None,
310
    errmsg_len: int = 1024,
311
) -> F2pyNamelistSpec:
312
    """Build f2py wrapper metadata for one namelist schema."""
313
    context = _build_context(
1✔
314
        schema,
315
        helper_module=helper_module,
316
        kind_module=kind_module,
317
        kind_map=kind_map,
318
        kind_allowlist=kind_allowlist,
319
        constants=constants,
320
        dimensions=dimensions,
321
        module_doc=None,
322
    )
323
    fields = cast("list[FieldSpec]", context["fields"])
1✔
324
    type_infos = {
1✔
325
        name: type_info
326
        for name, type_info in _iter_field_type_infos(schema, constants, dimensions)
327
    }
328
    required_args: list[F2pyArgumentSpec] = []
1✔
329
    optional_args: list[F2pyArgumentSpec] = []
1✔
330
    argument_list: list[str] = []
1✔
331
    argument_declarations: list[str] = []
1✔
332
    bridge_declarations: list[str] = []
1✔
333
    bridge_assignments: list[str] = []
1✔
334
    set_call_arguments: list[str] = []
1✔
335
    set_dims_argument_list: list[str] = []
1✔
336
    set_dims_argument_declarations: list[str] = []
1✔
337
    set_dims_bridge_declarations: list[str] = []
1✔
338
    set_dims_bridge_assignments: list[str] = []
1✔
339
    set_dims_call_arguments: list[str] = []
1✔
340
    set_dims_args: list[F2pyArgumentSpec] = []
1✔
341
    array_dimensions: list[F2pyArrayDimensionSpec] = []
1✔
342
    for field in fields:
1✔
343
        type_info = type_infos[field.name]
1✔
344
        rank = len(type_info.dimensions) if type_info.category == "array" else 0
1✔
345
        has_flag = None if field.required else f"has_{field.name}"
1✔
346
        spec = F2pyArgumentSpec(
1✔
347
            name=field.name,
348
            title=_one_line(field.title),
349
            required=field.required,
350
            rank=rank,
351
            numpy_dtype=_numpy_dtype(type_info),
352
            dummy_value=_python_dummy_value(type_info),
353
            doc_type=_python_doc_type(type_info),
354
            requirement="required" if field.required else "optional",
355
            has_flag=has_flag,
356
        )
357
        if field.required:
1✔
358
            required_args.append(spec)
1✔
359
        else:
360
            optional_args.append(spec)
1✔
361
        field_arguments, field_declarations = _f2py_field_arguments(field, type_info)
1✔
362
        argument_list.extend(field_arguments)
1✔
363
        argument_declarations.extend(field_declarations)
1✔
364
        if rank > 0:
1✔
365
            dim_names = _array_dimension_argument_names(field.name, rank)
1✔
366
            array_dimensions.append(
1✔
367
                F2pyArrayDimensionSpec(field_name=field.name, names=dim_names)
368
            )
369
        if has_flag is not None:
1✔
370
            argument_list.append(has_flag)
1✔
371
            argument_declarations.append(
1✔
372
                f"logical, intent(in) :: {has_flag} !< whether {field.name} was provided"
373
            )
374
            bridge_declarations.append(_optional_bridge_declaration(field.name, type_info))
1✔
375
            bridge_assignments.append(_optional_bridge_assignment(field.name, type_info))
1✔
376
            set_call_arguments.append(f"{field.name}=maybe_{field.name}")
1✔
377
        else:
378
            set_call_arguments.append(f"{field.name}={field.name}")
1✔
379

380
    runtime_dimension_args = cast("list[dict[str, str]]", context["set_dims_arguments"])
1✔
381
    for entry in runtime_dimension_args:
1✔
382
        const_name = entry["name"]
1✔
383
        arg_name = entry["arg_name"]
1✔
384
        python_name = _python_parameter_name(const_name)
1✔
385
        has_flag = f"has_{const_name}"
1✔
386
        set_dims_args.append(
1✔
387
            F2pyArgumentSpec(
388
                name=const_name,
389
                title=f"Runtime dimension override for {const_name}",
390
                required=False,
391
                rank=0,
392
                numpy_dtype="int",
393
                dummy_value="0",
394
                doc_type="int",
395
                requirement="optional",
396
                has_flag=has_flag,
397
                fixed_shape=None,
398
                python_name=python_name,
399
            )
400
        )
401
        set_dims_argument_list.append(const_name)
1✔
402
        set_dims_argument_declarations.append(
1✔
403
            f"integer, intent(in) :: {const_name} !< runtime dimension override for {const_name}"
404
        )
405
        set_dims_argument_list.append(has_flag)
1✔
406
        set_dims_argument_declarations.append(
1✔
407
            f"logical, intent(in) :: {has_flag} !< whether {const_name} was provided"
408
        )
409
        maybe_name = f"maybe_{const_name}"
1✔
410
        set_dims_bridge_declarations.append(f"integer, allocatable :: {maybe_name}")
1✔
411
        set_dims_bridge_assignments.append(
1✔
412
            f"if ({has_flag}) then\n"
413
            f"  allocate({maybe_name})\n"
414
            f"  {maybe_name} = {const_name}\n"
415
            "end if"
416
        )
417
        set_dims_call_arguments.append(f"{arg_name}={maybe_name}")
1✔
418

419
    namelist_name = cast("str", context["namelist_name"])
1✔
420
    details = cast("str", context["details_text"])
1✔
421
    return F2pyNamelistSpec(
1✔
422
        namelist_name=namelist_name,
423
        brief=_one_line(cast("str", context["brief_text"])),
424
        details=details,
425
        details_lines=details.splitlines() or [details],
426
        module_name=cast("str", context["module_name"]),
427
        type_name=cast("str", context["type_name"]),
428
        helper_module=helper_module,
429
        kind_module=cast("str", context["kind_module"]),
430
        kind_imports=cast("list[str]", context["kind_imports"]),
431
        f2py_module_name=f"f2py_{namelist_name}",
432
        resolve_handle_name=f"{context['module_name']}_resolve_handle",
433
        handle_ctype="c_intptr_t",
434
        errmsg_len=errmsg_len,
435
        argument_list=argument_list,
436
        argument_declarations=argument_declarations,
437
        bridge_declarations=bridge_declarations,
438
        bridge_assignments=bridge_assignments,
439
        set_call_arguments=set_call_arguments,
440
        set_dims_argument_list=set_dims_argument_list,
441
        set_dims_argument_declarations=set_dims_argument_declarations,
442
        set_dims_bridge_declarations=set_dims_bridge_declarations,
443
        set_dims_bridge_assignments=set_dims_bridge_assignments,
444
        set_dims_call_arguments=set_dims_call_arguments,
445
        set_dims_args=set_dims_args,
446
        array_dimensions=array_dimensions,
447
        required_args=required_args,
448
        optional_args=optional_args,
449
        all_args=required_args + optional_args,
450
    )
451

452

453
def _iter_field_type_infos(
1✔
454
    schema: dict[str, Any],
455
    constants: dict[str, int] | None,
456
    dimensions: dict[str, int] | None = None,
457
) -> list[tuple[str, FieldTypeInfo]]:
458
    properties = _normalized_properties(schema)
1✔
459
    constants = _normalize_constant_values(constants)
1✔
460
    runtime_dimension_values = _validate_runtime_dimensions(dimensions)
1✔
461
    overlap = sorted(set(constants) & set(runtime_dimension_values))
1✔
462
    if overlap:
1✔
NEW
463
        raise ValueError(
×
464
            "constants and dimensions must not share names: " + ", ".join(overlap)
465
        )
466
    field_types: list[tuple[str, FieldTypeInfo]] = []
1✔
467
    for name, prop in properties.items():
1✔
468
        _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
469
        field_types.append((name, _field_type_info(prop, constants)))
1✔
470
    return field_types
1✔
471

472

473
def _normalized_properties(schema: dict[str, Any]) -> dict[str, dict[str, Any]]:
1✔
474
    properties = schema.get("properties")
1✔
475
    if not isinstance(properties, dict):
1✔
476
        raise ValueError("schema must define object 'properties'")
×
477
    normalized: dict[str, dict[str, Any]] = {}
1✔
478
    seen: set[str] = set()
1✔
479
    for raw_name, prop in properties.items():
1✔
480
        if not isinstance(raw_name, str):
1✔
481
            raise ValueError("property names must be strings")
×
482
        if not isinstance(prop, dict):
1✔
483
            raise ValueError(f"property '{raw_name}' must be an object")
×
484
        name = raw_name.lower()
1✔
485
        if name in seen:
1✔
486
            raise ValueError(f"duplicate property '{raw_name}'")
×
487
        seen.add(name)
1✔
488
        normalized[name] = prop
1✔
489
    return normalized
1✔
490

491

492
def _class_name(namelist_name: str) -> str:
1✔
493
    parts = [part for part in re.split(r"[^0-9A-Za-z]+", namelist_name) if part]
1✔
494
    if not parts:
1✔
495
        return "Namelist"
×
496
    name = "".join(part[:1].upper() + part[1:] for part in parts)
1✔
497
    if name[0].isdigit():
1✔
498
        name = f"Namelist{name}"
×
499
    if keyword.iskeyword(name):
1✔
500
        name = f"{name}Namelist"
×
501
    return name
1✔
502

503

504
def _python_parameter_name(name: str) -> str:
1✔
505
    if not name.isidentifier():
1✔
NEW
506
        raise ValueError(f"name '{name}' is not a valid Python identifier")
×
507
    if keyword.iskeyword(name):
1✔
508
        return f"{name}_"
1✔
509
    return name
1✔
510

511

512
def _numpy_dtype(type_info: FieldTypeInfo) -> str | None:
1✔
513
    category = (
1✔
514
        type_info.element_category
515
        if type_info.category == "array"
516
        else type_info.category
517
    )
518
    if category == "real":
1✔
519
        return "float"
1✔
520
    if category == "integer":
1✔
521
        return "int"
1✔
522
    if category == "boolean":
1✔
523
        return "bool"
×
524
    if category == "string":
1✔
525
        return "str"
1✔
526
    return None
×
527

528

529
def _python_dummy_value(type_info: FieldTypeInfo) -> str:
1✔
530
    category = (
1✔
531
        type_info.element_category
532
        if type_info.category == "array"
533
        else type_info.category
534
    )
535
    if category == "real":
1✔
536
        return "0.0"
1✔
537
    if category == "integer":
1✔
538
        return "0"
1✔
539
    if category == "boolean":
1✔
540
        return "False"
×
541
    if category == "string":
1✔
542
        return '""'
1✔
543
    return "None"
×
544

545

546
def _python_doc_type(type_info: FieldTypeInfo) -> str:
1✔
547
    category = (
1✔
548
        type_info.element_category
549
        if type_info.category == "array"
550
        else type_info.category
551
    )
552
    if category == "real":
1✔
553
        type_name = "float"
1✔
554
    elif category == "integer":
1✔
555
        type_name = "int"
1✔
556
    elif category == "boolean":
1✔
557
        type_name = "bool"
×
558
    elif category == "string":
1✔
559
        type_name = "str"
1✔
560
    else:
561
        type_name = "Any"
×
562
    if type_info.category == "array":
1✔
563
        return f"array_like of {type_name}"
1✔
564
    return type_name
1✔
565

566

567
def _one_line(value: str) -> str:
1✔
568
    return " ".join(value.splitlines()).strip()
1✔
569

570

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

574

575
def _f2py_field_arguments(
1✔
576
    field: FieldSpec,
577
    type_info: FieldTypeInfo,
578
) -> tuple[list[str], list[str]]:
579
    requirement = "required" if field.required else "optional"
1✔
580
    if type_info.category != "array":
1✔
581
        return [field.name], [
1✔
582
            f"{type_info.arg_type_spec}, intent(in) :: {field.name} "
583
            f"!< {_one_line(field.title)} ({requirement})"
584
        ]
585

586
    dim_names = _array_dimension_argument_names(field.name, len(type_info.dimensions))
1✔
587
    dims = ", ".join(dim_names)
1✔
588
    declarations = [
1✔
589
        f"integer, intent(in) :: {dim_name} !< extent for {field.name}"
590
        for dim_name in dim_names
591
    ]
592
    declarations.append(
1✔
593
        f"{type_info.arg_type_spec}, dimension({dims}), intent(in) :: {field.name} "
594
        f"!< {_one_line(field.title)} ({requirement})"
595
    )
596
    return [*dim_names, field.name], declarations
1✔
597

598

599
def _optional_bridge_declaration(name: str, type_info: FieldTypeInfo) -> str:
1✔
600
    if type_info.category == "array":
1✔
601
        dims = ", ".join(":" for _ in type_info.dimensions)
1✔
602
        if type_info.element_category == "string":
1✔
603
            return f"character(len=:), dimension({dims}), allocatable :: maybe_{name}"
1✔
604
        return f"{type_info.arg_type_spec}, dimension({dims}), allocatable :: maybe_{name}"
1✔
605
    if type_info.category == "string":
1✔
606
        return f"character(len=:), allocatable :: maybe_{name}"
×
607
    return f"{type_info.arg_type_spec}, allocatable :: maybe_{name}"
1✔
608

609

610
def _optional_bridge_assignment(name: str, type_info: FieldTypeInfo) -> str:
1✔
611
    has_flag = f"has_{name}"
1✔
612
    maybe_name = f"maybe_{name}"
1✔
613
    if type_info.category == "array":
1✔
614
        dims = ", ".join(_array_dimension_argument_names(name, len(type_info.dimensions)))
1✔
615
        allocate_stmt = f"allocate({maybe_name}({dims}))"
1✔
616
        if type_info.element_category == "string":
1✔
617
            allocate_stmt = f"allocate(character(len=len({name})) :: {maybe_name}({dims}))"
1✔
618
        return (
1✔
619
            f"if ({has_flag}) then\n"
620
            f"  {allocate_stmt}\n"
621
            f"  {maybe_name} = {name}\n"
622
            "end if"
623
        )
624
    if type_info.category == "string":
1✔
625
        return (
×
626
            f"if ({has_flag}) then\n"
627
            f"  {maybe_name} = {name}\n"
628
            "end if"
629
        )
630
    return (
1✔
631
        f"if ({has_flag}) then\n"
632
        f"  allocate({maybe_name})\n"
633
        f"  {maybe_name} = {name}\n"
634
        "end if"
635
    )
636

637

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