From 28616faebe66eac32f4192a47a1a2eb062044954 Mon Sep 17 00:00:00 2001 From: Jordan Guymon Date: Mon, 22 Aug 2016 09:59:56 -0700 Subject: [PATCH 1/3] Implemented Wizard delegation --- awsshell/wizard.py | 25 ++++++++++++++++++------- tests/unit/test_app.py | 3 ++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/awsshell/wizard.py b/awsshell/wizard.py index c5534bf..d231361 100644 --- a/awsshell/wizard.py +++ b/awsshell/wizard.py @@ -123,7 +123,7 @@ def load_stage(stage): } creator = self._cached_creator loader = self._interaction_loader - return Stage(env, creator, loader, **stage_attrs) + return Stage(env, creator, loader, self, **stage_attrs) return [load_stage(stage) for stage in stages] @@ -177,8 +177,10 @@ def execute(self): raise WizardException('Stage not found: %s' % current_stage) try: self._push_stage(stage) - stage.execute() + stage_data = stage.execute() current_stage = stage.get_next_stage() + if current_stage is None: + return stage_data except Exception as err: stages = [s.name for (s, _) in self._stage_history] recovery = self._error_handler(err, stages) @@ -199,9 +201,9 @@ def _pop_stages(self, stage_index): class Stage(object): """The Stage object. Contains logic to run all steps of the stage.""" - def __init__(self, env, creator, interaction_loader, name=None, - prompt=None, retrieval=None, next_stage=None, resolution=None, - interaction=None): + def __init__(self, env, creator, interaction_loader, wizard_loader, + name=None, prompt=None, retrieval=None, next_stage=None, + resolution=None, interaction=None): """Construct a new Stage object. :type env: :class:`Environment` @@ -235,6 +237,7 @@ def __init__(self, env, creator, interaction_loader, name=None, """ self._env = env self._cached_creator = creator + self._wizard_loader = wizard_loader self._interaction_loader = interaction_loader self.name = name self.prompt = prompt @@ -270,6 +273,11 @@ def _handle_request_retrieval(self): # execute operation passing all parameters return operation(**parameters) + def _handle_wizard_delegation(self): + wizard_name = self.retrieval['Resource'] + wizard = self._wizard_loader.load_wizard(wizard_name) + return wizard.execute() + def _handle_retrieval(self): # In case of no retrieval, empty dict if not self.retrieval: @@ -278,6 +286,8 @@ def _handle_retrieval(self): data = self._handle_static_retrieval() elif self.retrieval['Type'] == 'Request': data = self._handle_request_retrieval() + elif self.retrieval['Type'] == 'Wizard': + data = self._handle_wizard_delegation() # Apply JMESPath query if given if self.retrieval.get('Path'): data = jmespath.search(self.retrieval['Path'], data) @@ -285,7 +295,6 @@ def _handle_retrieval(self): return data def _handle_interaction(self, data): - # if no interaction step, just forward data if self.interaction is None: return data @@ -299,6 +308,7 @@ def _handle_resolution(self, data): if self.resolution.get('Path'): data = jmespath.search(self.resolution['Path'], data) self._env.store(self.resolution['Key'], data) + return data def get_next_stage(self): """Resolve the next stage name for the stage after this one. @@ -322,7 +332,8 @@ def execute(self): """ retrieved_options = self._handle_retrieval() selected_data = self._handle_interaction(retrieved_options) - self._handle_resolution(selected_data) + resolved_data = self._handle_resolution(selected_data) + return resolved_data class Environment(object): diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 5c5f2ae..c0d7171 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -154,10 +154,11 @@ def test_exit_dot_command_exits_shell(): assert mock_prompter.run.call_count == 1 -def test_wizard_can_load_and_execute(): +def test_wizard_can_load_and_execute(errstream): # Proper dot command syntax should load and run a wizard mock_loader = mock.Mock() mock_wizard = mock_loader.load_wizard.return_value + mock_wizard.execute.return_value = {} handler = app.WizardHandler(err=errstream, loader=mock_loader) handler.run(['.wizard', 'wizname'], None) From c624460b213eefc02f862ab730822815d9944a58 Mon Sep 17 00:00:00 2001 From: Jordan Guymon Date: Mon, 22 Aug 2016 12:59:58 -0700 Subject: [PATCH 2/3] Delegation unit test and improve error handler test --- awsshell/wizard.py | 34 ++++++++++++++---------- tests/unit/test_wizard.py | 54 +++++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/awsshell/wizard.py b/awsshell/wizard.py index d231361..8b99917 100644 --- a/awsshell/wizard.py +++ b/awsshell/wizard.py @@ -51,7 +51,7 @@ class WizardLoader(object): """ def __init__(self, session=None, interaction_loader=None, - error_handler=None): + error_handler=None, delegation_loader=None): """Initialize a wizard factory. :type session: :class:`botocore.session.Session` @@ -71,6 +71,10 @@ def __init__(self, session=None, interaction_loader=None, self._interaction_loader = interaction_loader if interaction_loader is None: self._interaction_loader = InteractionLoader() + if delegation_loader is None: + self._delegation_loader = self + else: + self._delegation_loader = delegation_loader self._error_handler = error_handler if error_handler is None: self._error_handler = stage_error_handler @@ -111,20 +115,22 @@ def create_wizard(self, model): stages = self._load_stages(model.get('Stages'), env) return Wizard(start_stage, stages, env, self._error_handler) + def _load_stage(self, stage, env): + stage_attrs = { + 'name': stage.get('Name'), + 'prompt': stage.get('Prompt'), + 'retrieval': stage.get('Retrieval'), + 'next_stage': stage.get('NextStage'), + 'resolution': stage.get('Resolution'), + 'interaction': stage.get('Interaction'), + } + creator = self._cached_creator + interaction = self._interaction_loader + delegation = self._delegation_loader + return Stage(env, creator, interaction, delegation, **stage_attrs) + def _load_stages(self, stages, env): - def load_stage(stage): - stage_attrs = { - 'name': stage.get('Name'), - 'prompt': stage.get('Prompt'), - 'retrieval': stage.get('Retrieval'), - 'next_stage': stage.get('NextStage'), - 'resolution': stage.get('Resolution'), - 'interaction': stage.get('Interaction'), - } - creator = self._cached_creator - loader = self._interaction_loader - return Stage(env, creator, loader, self, **stage_attrs) - return [load_stage(stage) for stage in stages] + return [self._load_stage(stage, env) for stage in stages] class Wizard(object): diff --git a/tests/unit/test_wizard.py b/tests/unit/test_wizard.py index 98331d3..0680c52 100644 --- a/tests/unit/test_wizard.py +++ b/tests/unit/test_wizard.py @@ -200,20 +200,20 @@ def test_basic_full_execution(wizard_spec, loader): def test_basic_full_execution_error(wizard_spec): # Test that the wizard can handle exceptions in stage execution session = mock.Mock() - error_handler = mock.Mock() - error_handler.return_value = ('TestStage', 0) + error_handler = mock.Mock(side_effect=[('TestStage', 0), None]) loader = WizardLoader(session, error_handler=error_handler) wizard_spec['Stages'][0]['NextStage'] = \ {'Type': 'Name', 'Name': 'StageTwo'} wizard_spec['Stages'][0]['Resolution']['Path'] = '[0].Stage' stage_three = {'Name': 'StageThree', 'Prompt': 'Text'} wizard = loader.create_wizard(wizard_spec) - # force an exception once, let it recover, re-run - error = WizardException() - wizard.stages['StageTwo'].execute = mock.Mock(side_effect=[error, {}]) - wizard.execute() - # assert error handler was called - assert error_handler.call_count == 1 + # force two exceptions, recover once then fail to recover + errors = [WizardException(), TypeError()] + wizard.stages['StageTwo'].execute = mock.Mock(side_effect=errors) + with pytest.raises(TypeError): + wizard.execute() + # assert error handler was called twice + assert error_handler.call_count == 2 assert wizard.stages['StageTwo'].execute.call_count == 2 @@ -288,6 +288,44 @@ def test_wizard_basic_interaction(wizard_spec): create.return_value.execute.assert_called_once_with(data) +def test_wizard_basic_delegation(wizard_spec): + mock_session = mock.Mock(spec=Session) + mock_loader = mock.Mock(spec=WizardLoader) + loader = WizardLoader(mock_session, delegation_loader=mock_loader) + main_spec = { + "StartStage": "One", + "Stages": [ + { + "Name": "One", + "Prompt": "stage one", + "Retrieval": { + "Type": "Wizard", + "Resource": "SubWizard", + "Path": "FromSub" + } + } + ] + } + wizard = loader.create_wizard(main_spec) + sub_spec = { + "StartStage": "SubOne", + "Stages": [ + { + "Name": "SubOne", + "Prompt": "stage one", + "Retrieval": { + "Type": "Static", + "Resource": {"FromSub": "Result from sub"} + } + } + ] + } + mock_loader.load_wizard.return_value = loader.create_wizard(sub_spec) + result = wizard.execute() + mock_loader.load_wizard.assert_called_once_with('SubWizard') + assert result == 'Result from sub' + + exceptions = [ BotoCoreError(), WizardException('error'), From a5e4e8f7b43904f31225f9a2f21c1bd13f547745 Mon Sep 17 00:00:00 2001 From: Jordan Guymon Date: Fri, 26 Aug 2016 13:21:05 -0700 Subject: [PATCH 3/3] Improve WizardLoader interface --- awsshell/app.py | 5 +++-- awsshell/wizard.py | 9 ++------- tests/unit/test_wizard.py | 21 +++++++++++++++------ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/awsshell/app.py b/awsshell/app.py index b4278ad..b470b50 100644 --- a/awsshell/app.py +++ b/awsshell/app.py @@ -149,11 +149,12 @@ def run(self, command, application): class WizardHandler(object): - def __init__(self, output=sys.stdout, err=sys.stderr, - loader=WizardLoader()): + def __init__(self, output=sys.stdout, err=sys.stderr, loader=None): self._output = output self._err = err self._wizard_loader = loader + if self._wizard_loader is None: + self._wizard_loader = WizardLoader() def run(self, command, application): """Run the specified wizard. diff --git a/awsshell/wizard.py b/awsshell/wizard.py index 8b99917..6effa53 100644 --- a/awsshell/wizard.py +++ b/awsshell/wizard.py @@ -51,7 +51,7 @@ class WizardLoader(object): """ def __init__(self, session=None, interaction_loader=None, - error_handler=None, delegation_loader=None): + error_handler=None): """Initialize a wizard factory. :type session: :class:`botocore.session.Session` @@ -71,10 +71,6 @@ def __init__(self, session=None, interaction_loader=None, self._interaction_loader = interaction_loader if interaction_loader is None: self._interaction_loader = InteractionLoader() - if delegation_loader is None: - self._delegation_loader = self - else: - self._delegation_loader = delegation_loader self._error_handler = error_handler if error_handler is None: self._error_handler = stage_error_handler @@ -126,8 +122,7 @@ def _load_stage(self, stage, env): } creator = self._cached_creator interaction = self._interaction_loader - delegation = self._delegation_loader - return Stage(env, creator, interaction, delegation, **stage_attrs) + return Stage(env, creator, interaction, self, **stage_attrs) def _load_stages(self, stages, env): return [self._load_stage(stage, env) for stage in stages] diff --git a/tests/unit/test_wizard.py b/tests/unit/test_wizard.py index 0680c52..28ddef6 100644 --- a/tests/unit/test_wizard.py +++ b/tests/unit/test_wizard.py @@ -1,5 +1,8 @@ import mock import pytest +import botocore.session + +from botocore.loaders import Loader from botocore.session import Session from awsshell.utils import FileReadError from awsshell.wizard import stage_error_handler @@ -289,9 +292,6 @@ def test_wizard_basic_interaction(wizard_spec): def test_wizard_basic_delegation(wizard_spec): - mock_session = mock.Mock(spec=Session) - mock_loader = mock.Mock(spec=WizardLoader) - loader = WizardLoader(mock_session, delegation_loader=mock_loader) main_spec = { "StartStage": "One", "Stages": [ @@ -306,7 +306,6 @@ def test_wizard_basic_delegation(wizard_spec): } ] } - wizard = loader.create_wizard(main_spec) sub_spec = { "StartStage": "SubOne", "Stages": [ @@ -320,9 +319,19 @@ def test_wizard_basic_delegation(wizard_spec): } ] } - mock_loader.load_wizard.return_value = loader.create_wizard(sub_spec) + + mock_loader = mock.Mock(spec=Loader) + mock_loader.list_available_services.return_value = ['wizards'] + mock_load_model = mock_loader.load_service_model + mock_load_model.return_value = sub_spec + + session = botocore.session.get_session() + session.register_component('data_loader', mock_loader) + loader = WizardLoader(session) + wizard = loader.create_wizard(main_spec) + result = wizard.execute() - mock_loader.load_wizard.assert_called_once_with('SubWizard') + mock_load_model.assert_called_once_with('wizards', 'SubWizard') assert result == 'Result from sub'