diff --git a/src/rpdk/core/init.py b/src/rpdk/core/init.py index 28bca5d4..bb758977 100644 --- a/src/rpdk/core/init.py +++ b/src/rpdk/core/init.py @@ -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__) @@ -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( @@ -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): @@ -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) ) @@ -77,7 +81,7 @@ def __call__(self, value): validate_plugin_choice = ValidatePluginChoice( # pylint: disable=invalid-name - PLUGIN_CHOICES + get_plugin_choices() ) @@ -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() @@ -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) diff --git a/src/rpdk/core/plugin_registry.py b/src/rpdk/core/plugin_registry.py index 391a4a0b..8862ca4a 100644 --- a/src/rpdk/core/plugin_registry.py +++ b/src/rpdk/core/plugin_registry.py @@ -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") + ] + 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): diff --git a/tests/test_init.py b/tests/test_init.py index b79c098d..9fbf2db8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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, @@ -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(): + 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() diff --git a/tests/utils.py b/tests/utils.py index 97823f00..79d63d59 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -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 = "💣" @@ -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