Skip to content

Commit d14467e

Browse files
authored
Support CLI arguments for cfn init (#574)
Adds a plugin-friendly interface to add CLI arguments for cfn init.
1 parent ca5df48 commit d14467e

File tree

4 files changed

+195
-45
lines changed

4 files changed

+195
-45
lines changed

src/rpdk/core/init.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""This sub command generates IDE and build files for a given language.
22
"""
3+
import argparse
34
import logging
45
import re
5-
from argparse import SUPPRESS
66
from functools import wraps
77

88
from colorama import Fore, Style
99

1010
from .exceptions import WizardAbortError, WizardValidationError
11-
from .plugin_registry import PLUGIN_CHOICES
11+
from .plugin_registry import get_parsers, get_plugin_choices
1212
from .project import Project
1313

1414
LOG = logging.getLogger(__name__)
@@ -17,6 +17,10 @@
1717
TYPE_NAME_REGEX = r"^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$"
1818

1919

20+
def print_error(error):
21+
print(Style.BRIGHT, Fore.RED, str(error), Style.RESET_ALL, sep="")
22+
23+
2024
def input_with_validation(prompt, validate, description=""):
2125
while True:
2226
print(
@@ -32,8 +36,8 @@ def input_with_validation(prompt, validate, description=""):
3236
response = input()
3337
try:
3438
return validate(response)
35-
except WizardValidationError as e:
36-
print(Style.BRIGHT, Fore.RED, str(e), Style.RESET_ALL, sep="")
39+
except WizardValidationError as error:
40+
print_error(error)
3741

3842

3943
def validate_type_name(value):
@@ -42,7 +46,7 @@ def validate_type_name(value):
4246
return value
4347
LOG.debug("'%s' did not match '%s'", value, TYPE_NAME_REGEX)
4448
raise WizardValidationError(
45-
"Please enter a value matching '{}'".format(TYPE_NAME_REGEX)
49+
"Please enter a resource type name matching '{}'".format(TYPE_NAME_REGEX)
4650
)
4751

4852

@@ -77,7 +81,7 @@ def __call__(self, value):
7781

7882

7983
validate_plugin_choice = ValidatePluginChoice( # pylint: disable=invalid-name
80-
PLUGIN_CHOICES
84+
get_plugin_choices()
8185
)
8286

8387

@@ -139,14 +143,28 @@ def init(args):
139143

140144
check_for_existing_project(project)
141145

142-
type_name = input_typename()
143-
if args.language:
144-
language = args.language
145-
LOG.warning("Language plugin '%s' selected non-interactively", language)
146+
if args.type_name:
147+
try:
148+
type_name = validate_type_name(args.type_name)
149+
except WizardValidationError as error:
150+
print_error(error)
151+
type_name = input_typename()
152+
else:
153+
type_name = input_typename()
154+
155+
if "language" in vars(args):
156+
language = args.language.lower()
146157
else:
147158
language = input_language()
148159

149-
project.init(type_name, language)
160+
settings = {
161+
arg: getattr(args, arg)
162+
for arg in vars(args)
163+
if not callable(getattr(args, arg))
164+
}
165+
166+
project.init(type_name, language, settings)
167+
150168
project.generate()
151169
project.generate_docs()
152170

@@ -171,8 +189,20 @@ def setup_subparser(subparsers, parents):
171189
parser = subparsers.add_parser("init", description=__doc__, parents=parents)
172190
parser.set_defaults(command=ignore_abort(init))
173191

192+
language_subparsers = parser.add_subparsers(dest="subparser_name")
193+
base_subparser = argparse.ArgumentParser(add_help=False)
194+
for language_setup_subparser in get_parsers().values():
195+
language_setup_subparser()(language_subparsers, [base_subparser])
196+
197+
parser.add_argument(
198+
"-f",
199+
"--force",
200+
action="store_true",
201+
help="Force files to be overwritten.",
202+
)
203+
174204
parser.add_argument(
175-
"--force", action="store_true", help="Force files to be overwritten."
205+
"-t",
206+
"--type-name",
207+
help="Select the name of the resource type.",
176208
)
177-
# this is mainly for CI, so suppress it to keep it simple
178-
parser.add_argument("--language", help=SUPPRESS)

src/rpdk/core/plugin_registry.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@
55
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
66
}
77

8-
PLUGIN_CHOICES = sorted(PLUGIN_REGISTRY.keys())
8+
9+
def get_plugin_choices():
10+
plugin_choices = [
11+
entry_point.name
12+
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
13+
]
14+
return sorted(plugin_choices)
15+
16+
17+
def get_parsers():
18+
parsers = {
19+
entry_point.name: entry_point.load
20+
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.parsers")
21+
}
22+
23+
return parsers
924

1025

1126
def load_plugin(language):

tests/test_init.py

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from pathlib import Path
21
from unittest.mock import ANY, Mock, PropertyMock, patch
32

43
import pytest
54

5+
from rpdk.core.cli import main
66
from rpdk.core.exceptions import WizardAbortError, WizardValidationError
77
from rpdk.core.init import (
88
ValidatePluginChoice,
99
check_for_existing_project,
1010
ignore_abort,
11-
init,
1211
input_language,
1312
input_typename,
1413
input_with_validation,
@@ -17,62 +16,105 @@
1716
)
1817
from rpdk.core.project import Project
1918

19+
from .utils import add_dummy_language_plugin, dummy_parser, get_args, get_mock_project
20+
2021
PROMPT = "MECVGD"
2122
ERROR = "TUJFEL"
2223

2324

24-
def test_init_method_interactive_language():
25+
def test_init_method_interactive():
2526
type_name = object()
2627
language = object()
2728

28-
args = Mock(spec_set=["force", "language"])
29-
args.force = False
30-
args.language = None
31-
32-
mock_project = Mock(spec=Project)
33-
mock_project.load_settings.side_effect = FileNotFoundError
34-
mock_project.settings_path = ""
35-
mock_project.root = Path(".")
36-
37-
patch_project = patch("rpdk.core.init.Project", return_value=mock_project)
29+
mock_project, patch_project = get_mock_project()
3830
patch_tn = patch("rpdk.core.init.input_typename", return_value=type_name)
3931
patch_l = patch("rpdk.core.init.input_language", return_value=language)
4032

4133
with patch_project, patch_tn as mock_tn, patch_l as mock_l:
42-
init(args)
34+
main(args_in=["init"])
4335

4436
mock_tn.assert_called_once_with()
4537
mock_l.assert_called_once_with()
4638

4739
mock_project.load_settings.assert_called_once_with()
48-
mock_project.init.assert_called_once_with(type_name, language)
40+
mock_project.init.assert_called_once_with(
41+
type_name,
42+
language,
43+
{
44+
"version": False,
45+
"subparser_name": None,
46+
"verbose": 0,
47+
"force": False,
48+
"type_name": None,
49+
},
50+
)
4951
mock_project.generate.assert_called_once_with()
5052

5153

52-
def test_init_method_noninteractive_language():
53-
type_name = object()
54+
def test_init_method_noninteractive():
55+
add_dummy_language_plugin()
5456

55-
args = Mock(spec_set=["force", "language"])
56-
args.force = False
57-
args.language = "rust1.39"
57+
args = get_args("dummy", "Test::Test::Test")
58+
mock_project, patch_project = get_mock_project()
5859

59-
mock_project = Mock(spec=Project)
60-
mock_project.load_settings.side_effect = FileNotFoundError
61-
mock_project.settings_path = ""
62-
mock_project.root = Path(".")
60+
patch_get_parser = patch(
61+
"rpdk.core.init.get_parsers", return_value={"dummy": dummy_parser}
62+
)
63+
64+
with patch_project, patch_get_parser as mock_parser:
65+
main(args_in=["init", "--type-name", args.type_name, args.language, "--dummy"])
66+
67+
mock_parser.assert_called_once()
68+
69+
mock_project.load_settings.assert_called_once_with()
70+
mock_project.init.assert_called_once_with(
71+
args.type_name,
72+
args.language,
73+
{
74+
"version": False,
75+
"subparser_name": args.language,
76+
"verbose": 0,
77+
"force": False,
78+
"type_name": args.type_name,
79+
"language": args.language,
80+
"dummy": True,
81+
},
82+
)
83+
mock_project.generate.assert_called_once_with()
84+
85+
86+
def test_init_method_noninteractive_invalid_type_name():
87+
add_dummy_language_plugin()
88+
type_name = object()
89+
90+
args = get_args("dummy", "invalid_type_name")
91+
mock_project, patch_project = get_mock_project()
6392

64-
patch_project = patch("rpdk.core.init.Project", return_value=mock_project)
6593
patch_tn = patch("rpdk.core.init.input_typename", return_value=type_name)
66-
patch_l = patch("rpdk.core.init.input_language")
94+
patch_get_parser = patch(
95+
"rpdk.core.init.get_parsers", return_value={"dummy": dummy_parser}
96+
)
6797

68-
with patch_project, patch_tn as mock_tn, patch_l as mock_l:
69-
init(args)
98+
with patch_project, patch_tn as mock_tn, patch_get_parser as mock_parser:
99+
main(args_in=["init", "-t", args.type_name, args.language, "--dummy"])
70100

71101
mock_tn.assert_called_once_with()
72-
mock_l.assert_not_called()
102+
mock_parser.assert_called_once()
73103

74104
mock_project.load_settings.assert_called_once_with()
75-
mock_project.init.assert_called_once_with(type_name, args.language)
105+
mock_project.init.assert_called_once_with(
106+
type_name,
107+
args.language,
108+
{
109+
"version": False,
110+
"subparser_name": args.language,
111+
"verbose": 0,
112+
"force": False,
113+
"type_name": args.type_name,
114+
"language": args.language,
115+
"dummy": True,
116+
},
117+
)
76118
mock_project.generate.assert_called_once_with()
77119

78120

tests/utils.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import os
22
from contextlib import contextmanager
33
from io import BytesIO
4+
from pathlib import Path
45
from random import sample
6+
from unittest.mock import Mock, patch
7+
8+
import pkg_resources
9+
10+
from rpdk.core.project import Project
511

612
CONTENTS_UTF8 = "💣"
713

@@ -67,6 +73,63 @@ def chdir(path):
6773
os.chdir(old)
6874

6975

76+
def add_dummy_language_plugin():
77+
distribution = pkg_resources.Distribution(__file__)
78+
entry_point = pkg_resources.EntryPoint.parse(
79+
"dummy = rpdk.dummy:DummyLanguagePlugin", dist=distribution
80+
)
81+
distribution._ep_map = { # pylint: disable=protected-access
82+
"rpdk.v1.languages": {"dummy": entry_point}
83+
}
84+
pkg_resources.working_set.add(distribution)
85+
86+
87+
def get_mock_project():
88+
mock_project = Mock(spec=Project)
89+
mock_project.load_settings.side_effect = FileNotFoundError
90+
mock_project.settings_path = ""
91+
mock_project.root = Path(".")
92+
93+
patch_project = patch("rpdk.core.init.Project", return_value=mock_project)
94+
95+
return (mock_project, patch_project)
96+
97+
98+
def get_args(language=None, type_name=None):
99+
args = Mock(
100+
spec_set=[
101+
"language",
102+
"type_name",
103+
]
104+
)
105+
106+
args.language = language
107+
args.type_name = type_name
108+
109+
return args
110+
111+
112+
def dummy_parser():
113+
def dummy_subparser(subparsers, parents):
114+
parser = subparsers.add_parser(
115+
"dummy",
116+
description="""This sub command generates IDE and build
117+
files for the Dummy plugin""",
118+
parents=parents,
119+
)
120+
parser.set_defaults(language="dummy")
121+
122+
parser.add_argument(
123+
"-d",
124+
"--dummy",
125+
action="store_true",
126+
help="Dummy boolean to test if parser is loaded correctly",
127+
)
128+
return parser
129+
130+
return dummy_subparser
131+
132+
70133
class UnclosingBytesIO(BytesIO):
71134
_was_closed = False
72135

0 commit comments

Comments
 (0)