Skip to content

Support CLI arguments for cfn init #574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 2, 2020
58 changes: 44 additions & 14 deletions src/rpdk/core/init.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""This sub command generates IDE and build files for a given language.
"""
import argparse
import logging
import re
from argparse import SUPPRESS
from functools import wraps

from colorama import Fore, Style

from .exceptions import WizardAbortError, WizardValidationError
from .plugin_registry import PLUGIN_CHOICES
from .plugin_registry import get_parsers, get_plugin_choices
from .project import Project

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


def print_error(error):
print(Style.BRIGHT, Fore.RED, str(error), Style.RESET_ALL, sep="")


def input_with_validation(prompt, validate, description=""):
while True:
print(
Expand All @@ -32,8 +36,8 @@ def input_with_validation(prompt, validate, description=""):
response = input()
try:
return validate(response)
except WizardValidationError as e:
print(Style.BRIGHT, Fore.RED, str(e), Style.RESET_ALL, sep="")
except WizardValidationError as error:
print_error(error)


def validate_type_name(value):
Expand All @@ -42,7 +46,7 @@ def validate_type_name(value):
return value
LOG.debug("'%s' did not match '%s'", value, TYPE_NAME_REGEX)
raise WizardValidationError(
"Please enter a value matching '{}'".format(TYPE_NAME_REGEX)
"Please enter a resource type name matching '{}'".format(TYPE_NAME_REGEX)
)


Expand Down Expand Up @@ -77,7 +81,7 @@ def __call__(self, value):


validate_plugin_choice = ValidatePluginChoice( # pylint: disable=invalid-name
PLUGIN_CHOICES
get_plugin_choices()
)


Expand Down Expand Up @@ -139,14 +143,28 @@ def init(args):

check_for_existing_project(project)

type_name = input_typename()
if args.language:
language = args.language
LOG.warning("Language plugin '%s' selected non-interactively", language)
if args.type_name:
try:
type_name = validate_type_name(args.type_name)
except WizardValidationError as error:
print_error(error)
type_name = input_typename()
else:
type_name = input_typename()

if "language" in vars(args):
language = args.language.lower()
else:
language = input_language()

project.init(type_name, language)
settings = {
arg: getattr(args, arg)
for arg in vars(args)
if not callable(getattr(args, arg))
}

project.init(type_name, language, settings)

project.generate()
project.generate_docs()

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

language_subparsers = parser.add_subparsers(dest="subparser_name")
base_subparser = argparse.ArgumentParser(add_help=False)
for language_setup_subparser in get_parsers().values():
language_setup_subparser()(language_subparsers, [base_subparser])

parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force files to be overwritten.",
)

parser.add_argument(
"--force", action="store_true", help="Force files to be overwritten."
"-t",
"--type-name",
help="Select the name of the resource type.",
)
# this is mainly for CI, so suppress it to keep it simple
parser.add_argument("--language", help=SUPPRESS)
17 changes: 16 additions & 1 deletion src/rpdk/core/plugin_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
}

PLUGIN_CHOICES = sorted(PLUGIN_REGISTRY.keys())

def get_plugin_choices():
plugin_choices = [
entry_point.name
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
]
Comment on lines +10 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use set if you dont rely on element position

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my changes the list of plugins was already being sorted which converts the set back to a list. I f this is not required anymore I'll be ok with making it a set.

return sorted(plugin_choices)


def get_parsers():
parsers = {
entry_point.name: entry_point.load
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.parsers")
}

return parsers


def load_plugin(language):
Expand Down
102 changes: 72 additions & 30 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from pathlib import Path
from unittest.mock import ANY, Mock, PropertyMock, patch

import pytest

from rpdk.core.cli import main
from rpdk.core.exceptions import WizardAbortError, WizardValidationError
from rpdk.core.init import (
ValidatePluginChoice,
check_for_existing_project,
ignore_abort,
init,
input_language,
input_typename,
input_with_validation,
Expand All @@ -17,62 +16,105 @@
)
from rpdk.core.project import Project

from .utils import add_dummy_language_plugin, dummy_parser, get_args, get_mock_project

PROMPT = "MECVGD"
ERROR = "TUJFEL"


def test_init_method_interactive_language():
def test_init_method_interactive():
type_name = object()
language = object()

args = Mock(spec_set=["force", "language"])
args.force = False
args.language = None

mock_project = Mock(spec=Project)
mock_project.load_settings.side_effect = FileNotFoundError
mock_project.settings_path = ""
mock_project.root = Path(".")

patch_project = patch("rpdk.core.init.Project", return_value=mock_project)
mock_project, patch_project = get_mock_project()
patch_tn = patch("rpdk.core.init.input_typename", return_value=type_name)
patch_l = patch("rpdk.core.init.input_language", return_value=language)

with patch_project, patch_tn as mock_tn, patch_l as mock_l:
init(args)
main(args_in=["init"])

mock_tn.assert_called_once_with()
mock_l.assert_called_once_with()

mock_project.load_settings.assert_called_once_with()
mock_project.init.assert_called_once_with(type_name, language)
mock_project.init.assert_called_once_with(
type_name,
language,
{
"version": False,
"subparser_name": None,
"verbose": 0,
"force": False,
"type_name": None,
},
)
mock_project.generate.assert_called_once_with()


def test_init_method_noninteractive_language():
type_name = object()
def test_init_method_noninteractive():
add_dummy_language_plugin()

args = Mock(spec_set=["force", "language"])
args.force = False
args.language = "rust1.39"
args = get_args("dummy", "Test::Test::Test")
mock_project, patch_project = get_mock_project()

mock_project = Mock(spec=Project)
mock_project.load_settings.side_effect = FileNotFoundError
mock_project.settings_path = ""
mock_project.root = Path(".")
patch_get_parser = patch(
"rpdk.core.init.get_parsers", return_value={"dummy": dummy_parser}
)

with patch_project, patch_get_parser as mock_parser:
main(args_in=["init", "--type-name", args.type_name, args.language, "--dummy"])

mock_parser.assert_called_once()

mock_project.load_settings.assert_called_once_with()
mock_project.init.assert_called_once_with(
args.type_name,
args.language,
{
"version": False,
"subparser_name": args.language,
"verbose": 0,
"force": False,
"type_name": args.type_name,
"language": args.language,
"dummy": True,
},
)
mock_project.generate.assert_called_once_with()


def test_init_method_noninteractive_invalid_type_name():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confused on this. how does init reach project init method if the typename is invalid?

add_dummy_language_plugin()
type_name = object()

args = get_args("dummy", "invalid_type_name")
mock_project, patch_project = get_mock_project()

patch_project = patch("rpdk.core.init.Project", return_value=mock_project)
patch_tn = patch("rpdk.core.init.input_typename", return_value=type_name)
patch_l = patch("rpdk.core.init.input_language")
patch_get_parser = patch(
"rpdk.core.init.get_parsers", return_value={"dummy": dummy_parser}
)

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

mock_tn.assert_called_once_with()
mock_l.assert_not_called()
mock_parser.assert_called_once()

mock_project.load_settings.assert_called_once_with()
mock_project.init.assert_called_once_with(type_name, args.language)
mock_project.init.assert_called_once_with(
type_name,
args.language,
{
"version": False,
"subparser_name": args.language,
"verbose": 0,
"force": False,
"type_name": args.type_name,
"language": args.language,
"dummy": True,
},
)
mock_project.generate.assert_called_once_with()


Expand Down
63 changes: 63 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import os
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from random import sample
from unittest.mock import Mock, patch

import pkg_resources

from rpdk.core.project import Project

CONTENTS_UTF8 = "💣"

Expand Down Expand Up @@ -67,6 +73,63 @@ def chdir(path):
os.chdir(old)


def add_dummy_language_plugin():
distribution = pkg_resources.Distribution(__file__)
entry_point = pkg_resources.EntryPoint.parse(
"dummy = rpdk.dummy:DummyLanguagePlugin", dist=distribution
)
distribution._ep_map = { # pylint: disable=protected-access
"rpdk.v1.languages": {"dummy": entry_point}
}
pkg_resources.working_set.add(distribution)


def get_mock_project():
mock_project = Mock(spec=Project)
mock_project.load_settings.side_effect = FileNotFoundError
mock_project.settings_path = ""
mock_project.root = Path(".")

patch_project = patch("rpdk.core.init.Project", return_value=mock_project)

return (mock_project, patch_project)


def get_args(language=None, type_name=None):
args = Mock(
spec_set=[
"language",
"type_name",
]
)

args.language = language
args.type_name = type_name

return args


def dummy_parser():
def dummy_subparser(subparsers, parents):
parser = subparsers.add_parser(
"dummy",
description="""This sub command generates IDE and build
files for the Dummy plugin""",
parents=parents,
)
parser.set_defaults(language="dummy")

parser.add_argument(
"-d",
"--dummy",
action="store_true",
help="Dummy boolean to test if parser is loaded correctly",
)
return parser

return dummy_subparser


class UnclosingBytesIO(BytesIO):
_was_closed = False

Expand Down