From 9fd975b7214c24fddd936bd40f654ea88d37d2dc Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 15 Jan 2024 11:29:03 -0700 Subject: [PATCH 1/6] sort methods into groups --- src/bmipy/_template.py | 55 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py index acb9981..925ca75 100644 --- a/src/bmipy/_template.py +++ b/src/bmipy/_template.py @@ -2,19 +2,38 @@ import inspect import os +import re import textwrap +from collections import defaultdict +from collections import OrderedDict from bmipy.bmi import Bmi +GROUPS = ( + ("control", "(initialize|update|update_until|finalize)"), + ("info", r"(get_component_name|\w+_var_names|\w+_item_count)"), + ("var", r"get_var_\w+"), + ("time", r"get_\w*time\w*"), + ("value", r"(get|set)_value\w*"), + ("grid", r"get_grid_\w+"), +) + class Template: """Create template BMI implementations.""" def __init__(self, name: str): self._name = name - self._funcs = dict(inspect.getmembers(Bmi, inspect.isfunction)) - def render(self) -> str: + funcs = dict(inspect.getmembers(Bmi, inspect.isfunction)) + + names = sort_methods(frozenset(funcs)) + + self._funcs = OrderedDict( + (name, funcs.pop(name)) for name in names + ) | OrderedDict(sorted(funcs.items())) + + def render(self, with_docstring: bool = True) -> str: """Render a module that defines a class implementing a Bmi.""" prefix = f"""\ from __future__ import annotations @@ -30,13 +49,15 @@ def render(self) -> str: class {self._name}(Bmi): """ return prefix + (os.linesep * 2).join( - [self._render_func(name) for name in sorted(self._funcs)] + [ + self._render_func(name, with_docstring=with_docstring) + for name in self._funcs + ] ) - def _render_func(self, name: str) -> str: + def _render_func(self, name: str, with_docstring: bool = True) -> str: annotations = inspect.get_annotations(self._funcs[name]) signature = inspect.signature(self._funcs[name], eval_str=False) - docstring = textwrap.indent( '"""' + dedent_docstring(self._funcs[name].__doc__) + '"""', " " ) @@ -47,14 +68,32 @@ def _render_func(self, name: str) -> str: tuple(signature.parameters), annotations, width=84, - ), - docstring, - f" raise NotImplementedError({name!r})".replace("'", '"'), + ) ] + parts.append(docstring) if with_docstring else None + parts.append(f" raise NotImplementedError({name!r})".replace("'", '"')) return textwrap.indent(os.linesep.join(parts), " ") +def sort_methods(funcs: frozenset[str]) -> list[str]: + """Sort methods by group type.""" + unmatched = set(funcs) + matched = defaultdict(set) + + for group, regex in GROUPS: + pattern = re.compile(regex) + + matched[group] = {name for name in unmatched if pattern.match(name)} + unmatched -= matched[group] + + ordered = [] + for group, _ in GROUPS: + ordered.extend(sorted(matched[group])) + + return ordered + sorted(unmatched) + + def dedent_docstring(text: str | None, tabsize: int = 4) -> str: """Dedent a docstring, ignoring indentation of the first line. From 41e620caadcc5bda0ed334eac8e79777cd5173c6 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 15 Jan 2024 11:30:28 -0700 Subject: [PATCH 2/6] add --docstring/--no-docstring option to cli --- src/bmipy/_cmd.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/bmipy/_cmd.py b/src/bmipy/_cmd.py index 0f859ab..24936b1 100644 --- a/src/bmipy/_cmd.py +++ b/src/bmipy/_cmd.py @@ -9,19 +9,29 @@ from bmipy._version import __version__ -def main(args: tuple[str, ...] | None = None) -> int: +def main(argv: tuple[str, ...] | None = None) -> int: """Render a template BMI implementation in Python for class NAME.""" parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", version=f"bmipy {__version__}") parser.add_argument("name") - parsed_args = parser.parse_args(args) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--docstring", + action="store_true", + dest="docstring", + default=True, + help="Add docstrings to the generated methods", + ) + group.add_argument("--no-docstring", action="store_false", dest="docstring") - if parsed_args.name.isidentifier() and not keyword.iskeyword(parsed_args.name): - print(Template(parsed_args.name).render()) + args = parser.parse_args(argv) + + if args.name.isidentifier() and not keyword.iskeyword(args.name): + print(Template(args.name).render(with_docstring=args.docstring)) else: print( - f"💥 💔 💥 {parsed_args.name!r} is not a valid class name in Python", + f"💥 💔 💥 {args.name!r} is not a valid class name in Python", file=sys.stderr, ) return 1 From ad8162c11da2fd0f3f0856b5047862da4974ac89 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 15 Jan 2024 11:36:12 -0700 Subject: [PATCH 3/6] add test for --docstring option --- tests/cli_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/cli_test.py b/tests/cli_test.py index c4f4700..5dfdef9 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -46,3 +46,17 @@ def test_cli_with_hints(capsys): @pytest.mark.parametrize("bad_name", ["True", "0Bmi"]) def test_cli_with_bad_class_name(capsys, bad_name): assert main([bad_name]) != 0 + + +def test_cli_docstrings(capsys): + assert main(["MyBmiWithDocstrings", "--docstring"]) == 0 + output_default = capsys.readouterr().out + + assert main(["MyBmiWithDocstrings", "--docstring"]) == 0 + output_with_docstrings = capsys.readouterr().out + assert output_with_docstrings == output_default + + assert main(["MyBmiWithoutDocstrings", "--no-docstring"]) == 0 + output_without_docstrings = capsys.readouterr().out + + assert len(output_with_docstrings) > len(output_without_docstrings) From cdf368b50739c85f85f7d7fec5598de49fb985a4 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Jan 2024 12:17:31 -0700 Subject: [PATCH 4/6] add default is to include docstrings --- src/bmipy/_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bmipy/_cmd.py b/src/bmipy/_cmd.py index 24936b1..9a78125 100644 --- a/src/bmipy/_cmd.py +++ b/src/bmipy/_cmd.py @@ -21,7 +21,7 @@ def main(argv: tuple[str, ...] | None = None) -> int: action="store_true", dest="docstring", default=True, - help="Add docstrings to the generated methods", + help="Add docstrings to the generated methods (default: include docstrings)", ) group.add_argument("--no-docstring", action="store_false", dest="docstring") From d7d6e8bdc4b6d41bbf56c05c90aadc37cbc342d1 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Jan 2024 12:18:02 -0700 Subject: [PATCH 5/6] order initialize first, then update, then finalize --- src/bmipy/_template.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py index 925ca75..5353563 100644 --- a/src/bmipy/_template.py +++ b/src/bmipy/_template.py @@ -10,7 +10,9 @@ from bmipy.bmi import Bmi GROUPS = ( - ("control", "(initialize|update|update_until|finalize)"), + ("initialize", "initialize"), + ("update", "(update|update_until)"), + ("finalize", "finalize"), ("info", r"(get_component_name|\w+_var_names|\w+_item_count)"), ("var", r"get_var_\w+"), ("time", r"get_\w*time\w*"), From a33a39554846395e9300b669a182c3df37277edb Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Jan 2024 13:02:32 -0700 Subject: [PATCH 6/6] add a description for the class name argument --- src/bmipy/_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bmipy/_cmd.py b/src/bmipy/_cmd.py index 9a78125..ddd2947 100644 --- a/src/bmipy/_cmd.py +++ b/src/bmipy/_cmd.py @@ -13,7 +13,7 @@ def main(argv: tuple[str, ...] | None = None) -> int: """Render a template BMI implementation in Python for class NAME.""" parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", version=f"bmipy {__version__}") - parser.add_argument("name") + parser.add_argument("name", metavar="NAME", help="Name of the generated BMI class") group = parser.add_mutually_exclusive_group() group.add_argument(