Skip to content

Commit cdf5b6e

Browse files
committed
󰑌 Simplify subcommand integration to one place
Previously, adding a new subcommand required importing it into two separate Python modules, adding them into two mappings, maintaining the same dictionary key string. This is prone to human error and is underintegrated. This patch makes it easier by defining a required subcommand module shape on the typing level. Now, one just need to implement two hook- functions and a constant with specific signatures and import it in a single place.
1 parent 3a9b141 commit cdf5b6e

File tree

6 files changed

+53
-55
lines changed

6 files changed

+53
-55
lines changed

src/pre_commit_terraform/README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ that ends up being installed into `site-packages/` of virtualenvs.
77

88
When the Git repository is `pip install`ed, this [import package] becomes
99
available for use within respective Python interpreter instance. It can be
10-
imported and sub-modules can be imported through the dot-syntax.
11-
Additionally, the modules within are able to import the neighboring ones
12-
using relative imports that have a leading dot in them.
10+
imported and sub-modules can be imported through the dot-syntax. Additionally,
11+
the modules within can import the neighboring ones using relative imports that
12+
have a leading dot in them.
1313

1414
It additionally implements a [runpy interface], meaning that its name can
15-
be passed to `python -m` in order to invoke the CLI. This is the primary method
16-
of integration with the [`pre-commit` framework] and local development/testing.
15+
be passed to `python -m` to invoke the CLI. This is the primary method of
16+
integration with the [`pre-commit` framework] and local development/testing.
1717

1818
The layout allows for having several Python modules wrapping third-party tools,
1919
each having an argument parser and being a subcommand for the main CLI
@@ -32,12 +32,11 @@ subcommand modules.
3232
2. Within that module, define two functions —
3333
`invoke_cli_app(parsed_cli_args: Namespace) -> ReturnCodeType | int` and
3434
`populate_argument_parser(subcommand_parser: ArgumentParser) -> None`.
35-
3. Edit [`_cli_parsing.py`], importing `populate_argument_parser` from
36-
`subcommand_x` and adding it into `PARSER_MAP` with `subcommand-x` as
37-
a key.
38-
5. Edit [`_cli_subcommands.py`] `invoke_cli_app` from `subcommand_x` and
39-
adding it into `SUBCOMMAND_MAP` with `subcommand-x` as a key.
40-
6. Edit [`.pre-commit-hooks.yaml`], adding a new hook that invokes
35+
Additionally, define a module-level constant
36+
`CLI_SUBCOMMAND_NAME: Final[str] = 'subcommand-x'`.
37+
3. Edit [`_cli_subcommands.py`], importing `subcommand_x` as a relative module
38+
and add it into the `SUBCOMMAND_MODULES` list.
39+
4. Edit [`.pre-commit-hooks.yaml`], adding a new hook that invokes
4140
`python -m pre_commit_terraform subcommand-x`.
4241

4342
## Manual testing
@@ -53,7 +52,7 @@ POSIX-inspired CLI app.
5352

5453
## DX/UX considerations
5554

56-
Since it's an app that can be executed outside of the [`pre-commit` framework],
55+
Since it's an app that can be executed outside the [`pre-commit` framework],
5756
it is useful to check out and follow these [CLI guidelines][clig].
5857

5958
[`.pre-commit-hooks.yaml`]: ../../.pre-commit-hooks.yaml

src/pre_commit_terraform/_cli.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from sys import stderr
44

5-
from ._cli_subcommands import choose_cli_app
65
from ._cli_parsing import initialize_argument_parser
76
from ._errors import (
87
PreCommitTerraformBaseError,
@@ -21,14 +20,9 @@ def invoke_cli_app(cli_args: list[str]) -> ReturnCodeType:
2120
"""
2221
root_cli_parser = initialize_argument_parser()
2322
parsed_cli_args = root_cli_parser.parse_args(cli_args)
24-
try:
25-
invoke_chosen_app = choose_cli_app(parsed_cli_args.check_name)
26-
except LookupError as lookup_err:
27-
print(f'Sourcing subcommand failed: {lookup_err !s}', file=stderr)
28-
return ReturnCode.ERROR
2923

3024
try:
31-
return invoke_chosen_app(parsed_cli_args)
25+
return parsed_cli_args.invoke_cli_app(parsed_cli_args)
3226
except PreCommitTerraformExit as exit_err:
3327
print(f'App exiting: {exit_err !s}', file=stderr)
3428
raise

src/pre_commit_terraform/_cli_parsing.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@
66

77
from argparse import ArgumentParser
88

9-
from .terraform_docs_replace import (
10-
populate_argument_parser as populate_replace_docs_argument_parser,
11-
)
12-
13-
14-
PARSER_MAP = {
15-
'replace-docs': populate_replace_docs_argument_parser,
16-
}
9+
from ._cli_subcommands import SUBCOMMAND_MODULES
1710

1811

1912
def attach_subcommand_parsers_to(root_cli_parser: ArgumentParser, /) -> None:
@@ -28,9 +21,12 @@ def attach_subcommand_parsers_to(root_cli_parser: ArgumentParser, /) -> None:
2821
help='A check to be performed.',
2922
required=True,
3023
)
31-
for subcommand_name, initialize_subcommand_parser in PARSER_MAP.items():
32-
replace_docs_parser = subcommand_parsers.add_parser(subcommand_name)
33-
initialize_subcommand_parser(replace_docs_parser)
24+
for subcommand_module in SUBCOMMAND_MODULES:
25+
replace_docs_parser = subcommand_parsers.add_parser(subcommand_module.CLI_SUBCOMMAND_NAME)
26+
replace_docs_parser.set_defaults(
27+
invoke_cli_app=subcommand_module.invoke_cli_app,
28+
)
29+
subcommand_module.populate_argument_parser(replace_docs_parser)
3430

3531

3632
def initialize_argument_parser() -> ArgumentParser:
Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,12 @@
11
"""A CLI sub-commands organization module."""
22

3-
from argparse import Namespace
4-
from typing import Callable
3+
from . import terraform_docs_replace
4+
from ._types import CLISubcommandModuleProtocol
55

6-
from ._structs import ReturnCode
7-
from .terraform_docs_replace import (
8-
invoke_cli_app as invoke_replace_docs_cli_app,
9-
)
106

7+
SUBCOMMAND_MODULES: list[CLISubcommandModuleProtocol] = [
8+
terraform_docs_replace,
9+
]
1110

12-
SUBCOMMAND_MAP = {
13-
'replace-docs': invoke_replace_docs_cli_app,
14-
}
1511

16-
17-
def choose_cli_app(
18-
check_name: str,
19-
/,
20-
) -> Callable[[Namespace], ReturnCode | int]:
21-
"""Return a subcommand callable by CLI argument name."""
22-
try:
23-
return SUBCOMMAND_MAP[check_name]
24-
except KeyError as key_err:
25-
raise LookupError(
26-
f'{key_err !s}: Unable to find a callable for '
27-
f'the `{check_name !s}` subcommand',
28-
) from key_err
29-
30-
31-
__all__ = ('choose_cli_app',)
12+
__all__ = ('SUBCOMMAND_MODULES',)

src/pre_commit_terraform/_types.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
"""Composite types for annotating in-project code."""
22

3+
from argparse import ArgumentParser, Namespace
4+
from typing import Final, Protocol
5+
36
from ._structs import ReturnCode
47

58

69
ReturnCodeType = ReturnCode | int
10+
11+
12+
class CLISubcommandModuleProtocol(Protocol):
13+
"""A protocol for the subcommand-implementing module shape."""
14+
15+
CLI_SUBCOMMAND_NAME: Final[str]
16+
"""This constant contains a CLI."""
17+
18+
def populate_argument_parser(
19+
self, subcommand_parser: ArgumentParser,
20+
) -> None:
21+
"""Run a module hook for populating the subcommand parser."""
22+
23+
def invoke_cli_app(
24+
self, parsed_cli_args: Namespace,
25+
) -> ReturnCodeType | int:
26+
"""Run a module hook implementing the subcommand logic."""
27+
... # pylint: disable=unnecessary-ellipsis
28+
29+
30+
__all__ = ('CLISubcommandModuleProtocol', 'ReturnCodeType')

src/pre_commit_terraform/terraform_docs_replace.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
import subprocess
33
import warnings
44
from argparse import ArgumentParser, Namespace
5+
from typing import Final
56

67
from ._structs import ReturnCode
78
from ._types import ReturnCodeType
89

910

11+
CLI_SUBCOMMAND_NAME: Final[str] = 'replace-docs'
12+
13+
1014
def populate_argument_parser(subcommand_parser: ArgumentParser) -> None:
1115
subcommand_parser.description = (
1216
'Run terraform-docs on a set of files. Follows the standard '

0 commit comments

Comments
 (0)