Skip to content

Commit b520307

Browse files
authored
Merge pull request #49 from csdms/mcflugen/order-methods
Order methods in generated code by BMI group type
2 parents 18f359a + a33a395 commit b520307

File tree

3 files changed

+81
-16
lines changed

3 files changed

+81
-16
lines changed

src/bmipy/_cmd.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,29 @@
99
from bmipy._version import __version__
1010

1111

12-
def main(args: tuple[str, ...] | None = None) -> int:
12+
def main(argv: tuple[str, ...] | None = None) -> int:
1313
"""Render a template BMI implementation in Python for class NAME."""
1414
parser = argparse.ArgumentParser()
1515
parser.add_argument("--version", action="version", version=f"bmipy {__version__}")
16-
parser.add_argument("name")
17-
18-
parsed_args = parser.parse_args(args)
19-
20-
if parsed_args.name.isidentifier() and not keyword.iskeyword(parsed_args.name):
21-
print(Template(parsed_args.name).render())
16+
parser.add_argument("name", metavar="NAME", help="Name of the generated BMI class")
17+
18+
group = parser.add_mutually_exclusive_group()
19+
group.add_argument(
20+
"--docstring",
21+
action="store_true",
22+
dest="docstring",
23+
default=True,
24+
help="Add docstrings to the generated methods (default: include docstrings)",
25+
)
26+
group.add_argument("--no-docstring", action="store_false", dest="docstring")
27+
28+
args = parser.parse_args(argv)
29+
30+
if args.name.isidentifier() and not keyword.iskeyword(args.name):
31+
print(Template(args.name).render(with_docstring=args.docstring))
2232
else:
2333
print(
24-
f"💥 💔 💥 {parsed_args.name!r} is not a valid class name in Python",
34+
f"💥 💔 💥 {args.name!r} is not a valid class name in Python",
2535
file=sys.stderr,
2636
)
2737
return 1

src/bmipy/_template.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,40 @@
22

33
import inspect
44
import os
5+
import re
56
import textwrap
7+
from collections import defaultdict
8+
from collections import OrderedDict
69

710
from bmipy.bmi import Bmi
811

12+
GROUPS = (
13+
("initialize", "initialize"),
14+
("update", "(update|update_until)"),
15+
("finalize", "finalize"),
16+
("info", r"(get_component_name|\w+_var_names|\w+_item_count)"),
17+
("var", r"get_var_\w+"),
18+
("time", r"get_\w*time\w*"),
19+
("value", r"(get|set)_value\w*"),
20+
("grid", r"get_grid_\w+"),
21+
)
22+
923

1024
class Template:
1125
"""Create template BMI implementations."""
1226

1327
def __init__(self, name: str):
1428
self._name = name
15-
self._funcs = dict(inspect.getmembers(Bmi, inspect.isfunction))
1629

17-
def render(self) -> str:
30+
funcs = dict(inspect.getmembers(Bmi, inspect.isfunction))
31+
32+
names = sort_methods(frozenset(funcs))
33+
34+
self._funcs = OrderedDict(
35+
(name, funcs.pop(name)) for name in names
36+
) | OrderedDict(sorted(funcs.items()))
37+
38+
def render(self, with_docstring: bool = True) -> str:
1839
"""Render a module that defines a class implementing a Bmi."""
1940
prefix = f"""\
2041
from __future__ import annotations
@@ -30,13 +51,15 @@ def render(self) -> str:
3051
class {self._name}(Bmi):
3152
"""
3253
return prefix + (os.linesep * 2).join(
33-
[self._render_func(name) for name in sorted(self._funcs)]
54+
[
55+
self._render_func(name, with_docstring=with_docstring)
56+
for name in self._funcs
57+
]
3458
)
3559

36-
def _render_func(self, name: str) -> str:
60+
def _render_func(self, name: str, with_docstring: bool = True) -> str:
3761
annotations = inspect.get_annotations(self._funcs[name])
3862
signature = inspect.signature(self._funcs[name], eval_str=False)
39-
4063
docstring = textwrap.indent(
4164
'"""' + dedent_docstring(self._funcs[name].__doc__) + '"""', " "
4265
)
@@ -47,14 +70,32 @@ def _render_func(self, name: str) -> str:
4770
tuple(signature.parameters),
4871
annotations,
4972
width=84,
50-
),
51-
docstring,
52-
f" raise NotImplementedError({name!r})".replace("'", '"'),
73+
)
5374
]
75+
parts.append(docstring) if with_docstring else None
76+
parts.append(f" raise NotImplementedError({name!r})".replace("'", '"'))
5477

5578
return textwrap.indent(os.linesep.join(parts), " ")
5679

5780

81+
def sort_methods(funcs: frozenset[str]) -> list[str]:
82+
"""Sort methods by group type."""
83+
unmatched = set(funcs)
84+
matched = defaultdict(set)
85+
86+
for group, regex in GROUPS:
87+
pattern = re.compile(regex)
88+
89+
matched[group] = {name for name in unmatched if pattern.match(name)}
90+
unmatched -= matched[group]
91+
92+
ordered = []
93+
for group, _ in GROUPS:
94+
ordered.extend(sorted(matched[group]))
95+
96+
return ordered + sorted(unmatched)
97+
98+
5899
def dedent_docstring(text: str | None, tabsize: int = 4) -> str:
59100
"""Dedent a docstring, ignoring indentation of the first line.
60101

tests/cli_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,17 @@ def test_cli_with_hints(capsys):
4646
@pytest.mark.parametrize("bad_name", ["True", "0Bmi"])
4747
def test_cli_with_bad_class_name(capsys, bad_name):
4848
assert main([bad_name]) != 0
49+
50+
51+
def test_cli_docstrings(capsys):
52+
assert main(["MyBmiWithDocstrings", "--docstring"]) == 0
53+
output_default = capsys.readouterr().out
54+
55+
assert main(["MyBmiWithDocstrings", "--docstring"]) == 0
56+
output_with_docstrings = capsys.readouterr().out
57+
assert output_with_docstrings == output_default
58+
59+
assert main(["MyBmiWithoutDocstrings", "--no-docstring"]) == 0
60+
output_without_docstrings = capsys.readouterr().out
61+
62+
assert len(output_with_docstrings) > len(output_without_docstrings)

0 commit comments

Comments
 (0)