Skip to content

Commit 3a2450c

Browse files
committed
refactor(cli): remove CommandParserBuilder, simplify main()
WHAT: Based on the refactoring done in the previous PRs, the StrictDoc CLI commands are now modelled using Command pattern. This change refactors the `cli/` files a little further: - Merge the content of CommandParserBuilder into SDocArgsParser. - Declare the registry of valid StrictDoc commands in the `strictdoc/cli/main.py` file directly which gives a good idea of the overall CLI interface. - Simplify the logic of the main()-related functions. WHY: - This change further reduces the number of classes and code lines needed to parse the CLI arguments and call the right StrictDoc commands, improving the readability and maintainability of the code. HOW: Smaller knowledge bits: - The creation of the argument parser is now placed as the very first step of StrictDoc invocation. This is done to avoid any further tool configuration and progress if the arguments are incorrect. - With the simplified code, it is now also clear that printing the total execution time for all commands does not make too much sense. For example, the "about" and "version" commands do not really need the total execution time to be printed. This issue existed before the refactoring but with the reduced code size it became even more obvious. Improving this will be another work item outside this change.
1 parent 3249a67 commit 3a2450c

7 files changed

Lines changed: 124 additions & 147 deletions

File tree

docs/strictdoc_21_L2_StrictDoc_Requirements.sdoc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,9 +1710,6 @@ RELATIONS:
17101710
- TYPE: File
17111711
FORMAT: Sourcecode
17121712
VALUE: strictdoc/cli/main.py
1713-
- TYPE: File
1714-
FORMAT: Sourcecode
1715-
VALUE: strictdoc/cli/command_parser_builder.py
17161713
- TYPE: File
17171714
FORMAT: Sourcecode
17181715
VALUE: strictdoc/cli/cli_arg_parser.py

strictdoc/cli/cli_arg_parser.py

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
11
import argparse
2-
from typing import Any, Dict, Optional
2+
import sys
3+
from typing import Any, Dict, NoReturn
34

4-
from strictdoc.cli.command_parser_builder import (
5-
COMMAND_REGISTRY,
6-
CommandParserBuilder,
7-
)
5+
from strictdoc import __version__
86
from strictdoc.helpers.cast import assert_cast
97
from strictdoc.helpers.parallelizer import Parallelizer
108

119

10+
def formatter(prog: str) -> argparse.RawTextHelpFormatter:
11+
return argparse.RawTextHelpFormatter(
12+
prog, indent_increment=2, max_help_position=4, width=80
13+
)
14+
15+
16+
class SDocArgumentParser(argparse.ArgumentParser):
17+
def error(self, message: str) -> NoReturn:
18+
self.print_usage(sys.stderr)
19+
print(f"{self.prog}: error: {message}", file=sys.stderr) # noqa: T201
20+
print("") # noqa: T201
21+
print("Further help:") # noqa: T201
22+
print( # noqa: T201
23+
"'strictdoc -h/--help' provides a general overview of available commands."
24+
)
25+
print( # noqa: T201
26+
"'strictdoc <command> -h/--help' provides command-specific help."
27+
)
28+
sys.exit(2)
29+
30+
1231
class SDocArgsParser:
32+
@classmethod
33+
def create_sdoc_args_parser(
34+
cls, registry: Dict[str, Any]
35+
) -> "SDocArgsParser":
36+
parser = cls.build_argparse(registry)
37+
args = parser.parse_args()
38+
return cls(args, registry)
39+
1340
def __init__(self, args: argparse.Namespace, registry: Dict[str, Any]):
1441
self.args: argparse.Namespace = args
1542
self.registry: Dict[str, Any] = registry
@@ -31,13 +58,61 @@ def run(self, parallelizer: Parallelizer) -> bool:
3158

3259
return True
3360

61+
@classmethod
62+
def build_argparse(cls, registry: Dict[str, Any]) -> SDocArgumentParser:
63+
# https://stackoverflow.com/a/19476216/598057
64+
main_parser = SDocArgumentParser(
65+
prog="strictdoc",
66+
add_help=True,
67+
epilog=(
68+
"""
69+
Further help: https://strictdoc.readthedocs.io/en/stable/
70+
"""
71+
),
72+
)
3473

35-
def create_sdoc_args_parser(
36-
testing_args: Optional[argparse.Namespace] = None,
37-
) -> SDocArgsParser:
38-
args = testing_args
39-
if not args:
40-
builder = CommandParserBuilder()
41-
parser = builder.build()
42-
args = parser.parse_args()
43-
return SDocArgsParser(args, COMMAND_REGISTRY)
74+
# The -v/--version has a special behavior that it still works when all
75+
# commands are required == True.
76+
# https://stackoverflow.com/a/12123598/598057
77+
main_parser.add_argument(
78+
"-v", "--version", action="version", version=__version__
79+
)
80+
81+
main_parser.add_argument(
82+
"--debug",
83+
action="store_true",
84+
default=False,
85+
help="Enable more verbose printing of errors when they are encountered.",
86+
)
87+
88+
command_subparsers = main_parser.add_subparsers(
89+
title="command", dest="command"
90+
)
91+
command_subparsers.required = True
92+
93+
# Dynamically add subcommands
94+
for name, cmd in registry.items():
95+
if isinstance(cmd, dict): # command family
96+
family_parser = command_subparsers.add_parser(name)
97+
family_subparsers = family_parser.add_subparsers(
98+
dest="subcommand"
99+
)
100+
family_subparsers.required = True
101+
for subname, subcmd in cmd.items():
102+
sub_parser = family_subparsers.add_parser(
103+
subname,
104+
help=subcmd.HELP,
105+
description=subcmd.DETAILED_HELP,
106+
formatter_class=formatter,
107+
)
108+
subcmd.add_arguments(sub_parser)
109+
else:
110+
cmd_parser = command_subparsers.add_parser(
111+
name,
112+
help=cmd.HELP,
113+
description=cmd.DETAILED_HELP,
114+
formatter_class=formatter,
115+
)
116+
cmd.add_arguments(cmd_parser)
117+
118+
return main_parser

strictdoc/cli/command_parser_builder.py

Lines changed: 0 additions & 102 deletions
This file was deleted.

strictdoc/cli/main.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import multiprocessing
77
import os
88
import sys
9-
from typing import Optional
9+
from typing import Any, Dict, Optional
1010

1111
strictdoc_root_path = os.path.abspath(
1212
os.path.join(os.path.dirname(__file__), "..", "..")
@@ -17,8 +17,14 @@
1717
from strictdoc import environment
1818
from strictdoc.cli.cli_arg_parser import (
1919
SDocArgsParser,
20-
create_sdoc_args_parser,
2120
)
21+
from strictdoc.commands.about_command import AboutCommand
22+
from strictdoc.commands.export import ExportCommand
23+
from strictdoc.commands.import_excel import ImportExcelCommand
24+
from strictdoc.commands.import_reqif import ImportReqIFCommand
25+
from strictdoc.commands.manage_autouid_command import ManageAutoUIDCommand
26+
from strictdoc.commands.server import ServerCommand
27+
from strictdoc.commands.version_command import VersionCommand
2228
from strictdoc.helpers.coverage import register_code_coverage_hook
2329
from strictdoc.helpers.exception import (
2430
ExceptionInfo,
@@ -27,17 +33,27 @@
2733
from strictdoc.helpers.parallelizer import Parallelizer
2834
from strictdoc.helpers.timing import measure_performance
2935

36+
COMMAND_REGISTRY: Dict[str, Any] = {
37+
"about": AboutCommand,
38+
"export": ExportCommand,
39+
"import": {"excel": ImportExcelCommand, "reqif": ImportReqIFCommand},
40+
"manage": {"auto-uid": ManageAutoUIDCommand},
41+
"server": ServerCommand,
42+
"version": VersionCommand,
43+
}
3044

31-
def _main_internal(parallelizer: Parallelizer, parser: SDocArgsParser) -> None:
32-
register_code_coverage_hook()
33-
34-
if parser.run(parallelizer):
35-
return
3645

37-
raise NotImplementedError
46+
def _main() -> None:
47+
# The parser can raise when no arguments or incorrect arguments are provided.
48+
try:
49+
parser = SDocArgsParser.create_sdoc_args_parser(COMMAND_REGISTRY)
50+
except Exception as exception_:
51+
print(f"error: {str(exception_)}", flush=True) # noqa: T201
52+
sys.exit(1)
3853

54+
if parser.is_debug_mode():
55+
environment.is_debug_mode = True
3956

40-
def _main() -> None:
4157
# Ensure that multiprocessing.freeze_support() is called in a frozen
4258
# application
4359
# https://github.com/pyinstaller/pyinstaller/issues/7438
@@ -63,25 +79,14 @@ def _main() -> None:
6379
1, "w", encoding="utf-8", closefd=False
6480
)
6581

66-
enable_parallelization = "--no-parallelization" not in sys.argv
67-
68-
# NOTE: The parser can exit before the _main starts when no arguments
69-
# or incorrect arguments are provided. In those cases, it is still
70-
# important that the parallelizer is correctly shut down.
71-
try:
72-
parser = create_sdoc_args_parser()
73-
except Exception as exception_:
74-
print(f"error: {str(exception_)}", flush=True) # noqa: T201
75-
sys.exit(1)
76-
77-
if parser.is_debug_mode():
78-
environment.is_debug_mode = True
82+
register_code_coverage_hook()
7983

84+
enable_parallelization = "--no-parallelization" not in sys.argv
8085
parallelizer = Parallelizer.create(enable_parallelization)
8186

8287
exception_info: Optional[ExceptionInfo] = None
8388
try:
84-
_main_internal(parallelizer, parser)
89+
parser.run(parallelizer)
8590
except StrictDocChildProcessException as exception_info_:
8691
exception_info = exception_info_.exception_info
8792
except Exception as exception_:

strictdoc/commands/about_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ def run(self, parallelizer: Parallelizer) -> None: # noqa: ARG002
3232
"Docs: https://strictdoc.readthedocs.io/en/stable/"
3333
)
3434
print( # noqa: T201
35-
"Github: https://github.com/strictdoc-project/strictdoc"
35+
"GitHub: https://github.com/strictdoc-project/strictdoc"
3636
)
3737
print("License: Apache 2") # noqa: T201

tests/integration/scripting_examples/questionnaires/02_user_provided_example/export_questionnaires.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from strictdoc.backend.sdoc.models.document import SDocDocument
1010
from strictdoc.backend.sdoc.models.node import SDocNode
1111
from strictdoc.cli.cli_arg_parser import (
12-
create_sdoc_args_parser,
12+
SDocArgsParser,
1313
)
14+
from strictdoc.cli.main import COMMAND_REGISTRY
1415
from strictdoc.commands.export_config import ExportCommandConfig
1516
from strictdoc.core.document_iterator import SDocDocumentIterator
1617
from strictdoc.core.graph.abstract_bucket import ALL_EDGES
@@ -119,7 +120,7 @@ def find_doc(self, name):
119120

120121

121122
if __name__ == "__main__":
122-
parser = create_sdoc_args_parser()
123+
parser = SDocArgsParser.create_sdoc_args_parser(COMMAND_REGISTRY)
123124
project_config: ProjectConfig
124125

125126
export_config: ExportCommandConfig = ExportCommandConfig(**vars(parser.args))

tests/unit/strictdoc/cli/test_cli_arg_parser.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from strictdoc.cli.cli_arg_parser import (
2-
CommandParserBuilder,
2+
SDocArgsParser,
33
)
4+
from strictdoc.cli.main import COMMAND_REGISTRY
45

56
FAKE_STRICTDOC_ROOT_PATH = "/tmp/strictdoc-123"
67

78

89
def cli_args_parser():
9-
return CommandParserBuilder().build()
10+
return SDocArgsParser.build_argparse(COMMAND_REGISTRY)
1011

1112

1213
def test_export_01_minimal():

0 commit comments

Comments
 (0)