Skip to content

Commit 9a88a85

Browse files
authored
Merge pull request #2624 from strictdoc-project/stanislaw/cli_clean_up
refactor(cli): improve the handling of cli using command pattern
2 parents 62473a9 + 033366c commit 9a88a85

28 files changed

Lines changed: 589 additions & 605 deletions

File tree

strictdoc/backend/sdoc/models/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@relation(SDOC-SRS-18, scope=file)
33
"""
44

5+
from typing import Any, List
6+
57
from strictdoc.backend.sdoc.models.anchor import Anchor
68
from strictdoc.backend.sdoc.models.document import SDocDocument
79
from strictdoc.backend.sdoc.models.document_config import (
@@ -58,7 +60,7 @@
5860
FileEntry,
5961
]
6062

61-
GRAMMAR_MODELS = [
63+
GRAMMAR_MODELS: List[Any] = [
6264
DocumentGrammar,
6365
GrammarElement,
6466
GrammarElementFieldString,
@@ -70,7 +72,7 @@
7072
GrammarElementRelationFile,
7173
]
7274

73-
DOCUMENT_MODELS = [
75+
DOCUMENT_MODELS: List[Any] = [
7476
DocumentConfig,
7577
DocumentCustomMetadata,
7678
DocumentCustomMetadataKeyValuePair,

strictdoc/cli/base_command.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import argparse
2+
from abc import ABC, abstractmethod
3+
4+
from strictdoc.helpers.parallelizer import Parallelizer
5+
6+
7+
class CLIValidationError(Exception):
8+
pass
9+
10+
11+
class BaseCommand(ABC):
12+
"""Abstract base class for all StrictDoc CLI commands."""
13+
14+
@classmethod
15+
@abstractmethod
16+
def add_arguments(cls, parser: argparse.ArgumentParser) -> None:
17+
"""Add command-specific arguments to the parser."""
18+
pass
19+
20+
@abstractmethod
21+
def run(self, parallelizer: Parallelizer) -> None:
22+
"""Execute the command."""
23+
pass

strictdoc/cli/cli_arg_parser.py

Lines changed: 19 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import argparse
22
import os
3-
from typing import Any, List, Optional, Tuple
3+
from typing import Any, Dict, Optional
44

5+
from strictdoc.cli.base_command import CLIValidationError
56
from strictdoc.cli.command_parser_builder import (
7+
COMMAND_REGISTRY,
68
CommandParserBuilder,
79
SDocArgumentParser,
810
)
9-
from strictdoc.helpers.auto_described import auto_described
1011
from strictdoc.helpers.cast import assert_cast
11-
from strictdoc.helpers.net import is_valid_host
12-
13-
14-
class CLIValidationError(Exception):
15-
pass
12+
from strictdoc.helpers.parallelizer import Parallelizer
1613

1714

1815
class ImportReqIFCommandConfig:
@@ -73,149 +70,27 @@ def __init__(
7370
self.parser: SDocArgumentParser = parser
7471

7572

76-
@auto_described
77-
class ServerCommandConfig:
78-
def __init__(
79-
self,
80-
*,
81-
input_path: str,
82-
output_path: Optional[str],
83-
config_path: Optional[str],
84-
reload: bool,
85-
host: Optional[str],
86-
port: Optional[int],
87-
):
88-
self._input_path: str = input_path
89-
self.output_path: Optional[str] = output_path
90-
self._config_path: Optional[str] = config_path
91-
self.reload: bool = reload
92-
self.host: Optional[str] = host
93-
self.port: Optional[int] = port
94-
95-
def get_full_input_path(self) -> str:
96-
return os.path.abspath(self._input_path)
97-
98-
def get_path_to_config(self) -> str:
99-
return (
100-
self._config_path
101-
if self._config_path is not None
102-
else self._input_path
103-
)
104-
105-
def validate(self) -> None:
106-
if not os.path.exists(self._input_path):
107-
raise CLIValidationError(
108-
f"Provided input path does not exist: {self._input_path}"
109-
)
110-
111-
if self._config_path is not None and not os.path.exists(
112-
self._config_path
113-
):
114-
raise CLIValidationError(
115-
"Provided path to a configuration file does not exist: "
116-
f"{self._config_path}"
117-
)
118-
119-
if (host_ := self.host) is not None:
120-
if not is_valid_host(host_):
121-
raise CLIValidationError(
122-
f"Provided 'host' argument is not a valid host: {host_}"
123-
)
124-
125-
126-
@auto_described
127-
class ExportCommandConfig:
128-
def __init__(
129-
self,
130-
*,
131-
input_paths: List[str],
132-
output_dir: Optional[str],
133-
config_path: Optional[str],
134-
project_title: Optional[str],
135-
formats: List[str],
136-
fields: List[str],
137-
generate_bundle_document: bool,
138-
no_parallelization: bool,
139-
enable_mathjax: bool,
140-
included_documents: bool,
141-
filter_nodes: Optional[str],
142-
reqif_profile: Optional[str],
143-
reqif_multiline_is_xhtml: bool,
144-
reqif_enable_mid: bool,
145-
view: Optional[str],
146-
generate_diff_git: Optional[str],
147-
generate_diff_dirs: Optional[Tuple[str, str]],
148-
chromedriver: Optional[str],
149-
):
150-
assert isinstance(input_paths, list), f"{input_paths}"
151-
self.input_paths: List[str] = input_paths
152-
self.output_dir: Optional[str] = output_dir
153-
self._config_path: Optional[str] = config_path
154-
self.project_title: Optional[str] = project_title
155-
self.formats: List[str] = formats
156-
self.fields: List[str] = fields
157-
self.generate_bundle_document: bool = generate_bundle_document
158-
self.no_parallelization: bool = no_parallelization
159-
self.enable_mathjax: bool = enable_mathjax
160-
self.included_documents: bool = included_documents
161-
self.filter_nodes: Optional[str] = filter_nodes
162-
self.reqif_profile: Optional[str] = reqif_profile
163-
self.reqif_multiline_is_xhtml: bool = reqif_multiline_is_xhtml
164-
self.reqif_enable_mid: bool = reqif_enable_mid
165-
self.view: Optional[str] = view
166-
self.generate_diff_git: Optional[str] = generate_diff_git
167-
self.generate_diff_dirs: Optional[Tuple[str, str]] = generate_diff_dirs
168-
self.chromedriver: Optional[str] = chromedriver
169-
170-
def get_path_to_config(self) -> str:
171-
# FIXME: The control flow can be improved.
172-
path_to_input_dir: str = self.input_paths[0]
173-
if os.path.isfile(path_to_input_dir):
174-
path_to_input_dir = os.path.dirname(path_to_input_dir)
175-
path_to_config = (
176-
self._config_path
177-
if self._config_path is not None
178-
else path_to_input_dir
179-
)
180-
return path_to_config
181-
182-
def validate(self) -> None:
183-
for idx_, input_path_ in enumerate(self.input_paths.copy()):
184-
if not os.path.exists(input_path_):
185-
raise CLIValidationError(
186-
f"Provided input path does not exist: {input_path_}"
187-
)
188-
if not os.path.isabs(input_path_):
189-
self.input_paths[idx_] = os.path.abspath(input_path_).rstrip(
190-
"/"
191-
)
192-
if self._config_path is not None:
193-
if not os.path.exists(self._config_path):
194-
raise CLIValidationError(
195-
"Provided path to a configuration file does not exist: "
196-
f"{self._config_path}"
197-
)
198-
199-
200-
class DumpGrammarCommandConfig:
201-
def __init__(self, output_file: str):
202-
self.output_file: str = output_file
203-
204-
20573
class SDocArgsParser:
206-
def __init__(self, args: argparse.Namespace):
74+
def __init__(self, args: argparse.Namespace, registry: Dict[str, Any]):
20775
self.args: argparse.Namespace = args
76+
self.registry: Dict[str, Any] = registry
20877

20978
def is_debug_mode(self) -> bool:
21079
return assert_cast(self.args.debug, bool)
21180

212-
@property
213-
def is_about_command(self) -> bool:
214-
return str(self.args.command) == "about"
81+
def run(self, parallelizer: Parallelizer) -> bool:
82+
if self.args.command not in self.registry:
83+
return False
21584

216-
@property
217-
def is_export_command(self) -> bool:
218-
return str(self.args.command) == "export"
85+
cmd = self.registry[self.args.command]
86+
if isinstance(cmd, dict):
87+
assert self.args.subcommand in cmd
88+
command_instance = cmd[self.args.subcommand](self.args)
89+
else:
90+
command_instance = cmd(self.args)
91+
command_instance.run(parallelizer)
92+
93+
return True
21994

22095
@property
22196
def is_import_command_reqif(self) -> bool:
@@ -235,45 +110,13 @@ def is_import_command_excel(self) -> bool:
235110
def is_server_command(self) -> bool:
236111
return str(self.args.command) == "server"
237112

238-
@property
239-
def is_dump_grammar_command(self) -> bool:
240-
return str(self.args.command) == "dump-grammar"
241-
242-
@property
243-
def is_version_command(self) -> bool:
244-
return str(self.args.command) == "version"
245-
246113
@property
247114
def is_manage_autouid_command(self) -> bool:
248115
return (
249116
str(self.args.command) == "manage"
250117
and str(self.args.subcommand) == "auto-uid"
251118
)
252119

253-
def get_export_config(self) -> ExportCommandConfig:
254-
project_title: Optional[str] = self.args.project_title
255-
256-
return ExportCommandConfig(
257-
input_paths=self.args.input_paths,
258-
output_dir=self.args.output_dir,
259-
config_path=self.args.config,
260-
project_title=project_title,
261-
formats=self.args.formats,
262-
fields=self.args.fields,
263-
generate_bundle_document=self.args.generate_bundle_document,
264-
no_parallelization=self.args.no_parallelization,
265-
enable_mathjax=self.args.enable_mathjax,
266-
included_documents=self.args.included_documents,
267-
filter_nodes=self.args.filter_nodes,
268-
reqif_profile=self.args.reqif_profile,
269-
reqif_multiline_is_xhtml=self.args.reqif_multiline_is_xhtml,
270-
reqif_enable_mid=self.args.reqif_enable_mid,
271-
view=self.args.view,
272-
generate_diff_git=self.args.generate_diff_git,
273-
generate_diff_dirs=self.args.generate_diff_dirs,
274-
chromedriver=self.args.chromedriver,
275-
)
276-
277120
def get_import_config_reqif(self, _: Any) -> ImportReqIFCommandConfig:
278121
return ImportReqIFCommandConfig(
279122
self.args.input_path,
@@ -295,19 +138,6 @@ def get_import_config_excel(self, _: Any) -> ImportExcelCommandConfig:
295138
self.args.input_path, self.args.output_path, self.args.parser
296139
)
297140

298-
def get_server_config(self) -> ServerCommandConfig:
299-
return ServerCommandConfig(
300-
input_path=self.args.input_path,
301-
output_path=self.args.output_path,
302-
config_path=self.args.config,
303-
reload=self.args.reload,
304-
host=self.args.host,
305-
port=self.args.port,
306-
)
307-
308-
def get_dump_grammar_config(self) -> DumpGrammarCommandConfig:
309-
return DumpGrammarCommandConfig(output_file=self.args.output_file)
310-
311141

312142
def create_sdoc_args_parser(
313143
testing_args: Optional[argparse.Namespace] = None,
@@ -317,4 +147,4 @@ def create_sdoc_args_parser(
317147
builder = CommandParserBuilder()
318148
parser = builder.build()
319149
args = parser.parse_args()
320-
return SDocArgsParser(args)
150+
return SDocArgsParser(args, COMMAND_REGISTRY)

0 commit comments

Comments
 (0)