Skip to content

Commit e7992f1

Browse files
authored
Merge pull request #2628 from strictdoc-project/stanislaw/refactor_cli
refactor(cli): remove CommandParserBuilder, simplify main()
2 parents 3249a67 + 3a2450c commit e7992f1

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)