Skip to content

Commit 43988f6

Browse files
committed
Refactored wizard instantiation. Improved test coverage with mock.
1 parent b4dbbfd commit 43988f6

File tree

4 files changed

+250
-82
lines changed

4 files changed

+250
-82
lines changed

awsshell/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, output=sys.stdout, err=sys.stderr, chdir=os.chdir):
5656
def run(self, command, application):
5757
# command is a list of parsed commands
5858
if len(command) != 2:
59-
self._err.write("invalid syntax, must be: .cd dirname\n")
59+
self._err.write("Invalid syntax, must be: .cd dirname\n")
6060
return
6161
dirname = os.path.expandvars(os.path.expanduser(command[1]))
6262
try:
@@ -166,7 +166,7 @@ def run(self, command, application):
166166
wizard.
167167
"""
168168
if len(command) != 2:
169-
self._err.write("invalid syntax, must be: .wizard wizname\n")
169+
self._err.write("Invalid syntax, must be: .wizard wizname\n")
170170
return
171171
wizard = self._wizard_loader.load_wizard(command[1])
172172
wizard.execute()

awsshell/wizard.py

Lines changed: 123 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,127 +7,182 @@
77

88

99
class WizardException(Exception):
10-
"""Base exception class for the Wizards"""
10+
"""Base exception class for the Wizards."""
1111
pass
1212

1313

1414
class WizardLoader(object):
15-
"""This class is responsible for searching various paths to locate wizard
16-
models. Given a wizard name it will return a wizard object representing the
17-
wizard. Delegates to botocore for finding and loading the JSON models.
15+
"""This class is responsible for searching various paths to locate wizards.
16+
17+
Given a wizard name it will return a wizard object representing the wizard.
18+
Delegates to botocore for finding and loading the JSON models.
1819
"""
1920

20-
def __init__(self):
21-
self._session = botocore.session.get_session()
21+
def __init__(self, session=None):
22+
"""Initialize a wizard factory.
23+
24+
:type session: :class:`botocore.session.Session`
25+
:param session: (Optional) The botocore session to be used when loading
26+
models and retrieving clients.
27+
"""
28+
self._session = session
29+
if session is None:
30+
self._session = botocore.session.Session()
2231
self._loader = self._session.get_component('data_loader')
2332

2433
def load_wizard(self, name):
25-
"""Given a wizard's name, returns an instance of that wizard.
34+
"""Given a wizard's name, return an instance of that wizard.
2635
2736
:type name: str
2837
:param name: The name of the desired wizard.
38+
39+
:rtype: :class:`Wizard`
40+
:return: The wizard object loaded.
2941
"""
3042
# TODO possible naming collisions here, always pick first for now
3143
# Need to discuss and specify wizard invocation
3244
services = self._loader.list_available_services(type_name=name)
3345
model = self._loader.load_service_model(services[0], name)
34-
return Wizard(model)
46+
return self.create_wizard(model)
47+
48+
def create_wizard(self, model):
49+
"""Given a wizard specification, return an instance of that wizard.
50+
51+
:type model: dict
52+
:param model: The wizard specification to be used.
53+
54+
:rtype: :class:`Wizard`
55+
:return: The wizard object created.
56+
57+
:raises: :class:`WizardException`
58+
"""
59+
start_stage = model.get('StartStage')
60+
if not start_stage:
61+
raise WizardException('Start stage not specified')
62+
stages = model.get('Stages')
63+
return Wizard(start_stage, stages, self._session)
3564

3665

3766
class Wizard(object):
38-
"""The main wizard object containing all of the stages, the environment,
39-
botocore sessions, and the logic to drive the wizards.
40-
"""
67+
"""Main wizard object. Contains main wizard driving logic."""
68+
69+
def __init__(self, start_stage, stages, session):
70+
"""Construct a new Wizard.
71+
72+
:type start_stage: str
73+
:param start_stage: The name of the starting stage for the wizard.
4174
42-
def __init__(self, spec=None):
43-
"""Constructs a new Wizard
75+
:type stages: array of dict
76+
:param stages: An array of stage models to generate stages from.
4477
45-
:type spec: dict
46-
:param spec: (Optional) Constructs the wizard by loading the given
47-
model specification.
78+
:type session: :class:`botocore.session.Session`
79+
:param session: The botocore session to be used when creating clients.
4880
"""
4981
self.env = Environment()
50-
self._session = botocore.session.get_session()
82+
self._session = session
5183
self._cached_creator = index.CachedClientCreator(self._session)
52-
if spec:
53-
self.load_from_dict(spec)
84+
self.start_stage = start_stage
85+
self._load_stages(stages)
5486

55-
def load_from_dict(self, spec):
56-
"""Loads the model specification from a dict, populating the
57-
wizard object.
87+
def _load_stages(self, stages):
88+
"""Load the stages dictionary from the given array of stage models.
5889
59-
:type spec: dict
60-
:param spec: The model specification.
61-
62-
:raises: :class:`WizardException`
90+
:type stages: array of dict
91+
:param stages: An array of stage models to be converted into objects.
6392
"""
64-
self.start_stage = spec.get('StartStage', None)
65-
if not self.start_stage:
66-
raise WizardException("Start stage not specified")
6793
self.stages = {}
68-
for s in spec['Stages']:
69-
stage = Stage(s, self)
94+
for stage_model in stages:
95+
stage_attrs = {
96+
'name': stage_model.get('Name'),
97+
'prompt': stage_model.get('Prompt'),
98+
'retrieval': stage_model.get('Retrieval'),
99+
'next_stage': stage_model.get('NextStage'),
100+
'resolution': stage_model.get('Resolution'),
101+
'interaction': stage_model.get('Interaction'),
102+
}
103+
stage = Stage(self.env, self._cached_creator, **stage_attrs)
70104
self.stages[stage.name] = stage
71105

72106
def execute(self):
73-
"""Runs the wizard. Executes Stages until a final stage is reached.
107+
"""Run the wizard. Execute Stages until a final stage is reached.
74108
75-
:raises: :class:WizardException
109+
:raises: :class:`WizardException`
76110
"""
77111
current_stage = self.start_stage
78112
while current_stage:
79-
stage = self.stages.get(current_stage, None)
113+
stage = self.stages.get(current_stage)
80114
if not stage:
81-
raise WizardException("Stage not found: %s" % current_stage)
115+
raise WizardException('Stage not found: %s' % current_stage)
82116
stage.execute()
83117
current_stage = stage.get_next_stage()
84118
# TODO decouple wizard from all I/O
85-
sys.stdout.write(str(self.env)+'\n')
119+
sys.stdout.write(str(self.env) + '\n')
120+
sys.stdout.flush()
86121

87122

88123
class Stage(object):
89-
"""The Stage object contains the meta information for a stage and logic
90-
required to perform all steps present.
91-
"""
124+
"""The Stage object. Contains logic to run all steps of the stage."""
125+
126+
def __init__(self, env, creator, name=None, prompt=None, retrieval=None,
127+
next_stage=None, resolution=None, interaction=None):
128+
"""Construct a new Stage object.
129+
130+
:type env: :class:`Environment`
131+
:param env: The environment this stage is based in.
132+
133+
:type creator: :class:`CachedClientCreator`
134+
:param creator: A botocore client creator that supports caching.
92135
93-
def __init__(self, spec, wizard):
94-
"""Constructs a new Stage object.
136+
:type name: str
137+
:param name: A unique identifier for the stage.
138+
139+
:type prompt: str
140+
:param prompt: A simple message on the overall goal of the stage.
141+
142+
:type retrieval: dict
143+
:param retrieval: The source of data for this stage.
144+
145+
:type next_stage: dict
146+
:param next_stage: Describes what stage comes after this one.
95147
96-
:type spec: dict
97-
:param spec: The stage specification model.
148+
:type resolution: dict
149+
:param resolution: Describes what data to store in the environment.
98150
99-
:type wizard: :class:`Wizard`
100-
:param wizard: The wizard that this stage is a part of.
151+
:type interaction: dict
152+
:param interaction: Describes what type of screen is to be used for
153+
interaction.
101154
"""
102-
self._wizard = wizard
103-
self.name = spec.get('Name', None)
104-
self.prompt = spec.get('Prompt', None)
105-
self.retrieval = spec.get('Retrieval', None)
106-
self.next_stage = spec.get('NextStage', None)
107-
self.resolution = spec.get('Resolution', None)
108-
self.interaction = spec.get('Interaction', None)
155+
self._env = env
156+
self._cached_creator = creator
157+
self.name = name
158+
self.prompt = prompt
159+
self.retrieval = retrieval
160+
self.next_stage = next_stage
161+
self.resolution = resolution
162+
self.interaction = interaction
109163

110164
def __handle_static_retrieval(self):
111165
return self.retrieval.get('Resource')
112166

113167
def __handle_request_retrieval(self):
114168
req = self.retrieval['Resource']
115169
# get client from wizard's cache
116-
client = self._wizard._cached_creator.create_client(req['Service'])
170+
client = self._cached_creator.create_client(req['Service'])
117171
# get the operation from the client
118172
operation = getattr(client, xform_name(req['Operation']))
119173
# get any parameters
120174
parameters = req.get('Parameters', {})
121175
env_parameters = \
122-
self._wizard.env.resolve_parameters(req.get('EnvParameters', {}))
176+
self._env.resolve_parameters(req.get('EnvParameters', {}))
123177
# union of parameters and env_parameters, conflicts favor env_params
124178
parameters = dict(parameters, **env_parameters)
125179
# execute operation passing all parameters
126180
return operation(**parameters)
127181

128182
def _handle_retrieval(self):
129183
# TODO decouple wizard from all I/O
130-
sys.stdout.write(self.prompt+'\n')
184+
sys.stdout.write(self.prompt + '\n')
185+
sys.stdout.flush()
131186
# In case of no retrieval, empty dict
132187
if not self.retrieval:
133188
return {}
@@ -156,10 +211,10 @@ def _handle_resolution(self, data):
156211
if self.resolution:
157212
if self.resolution.get('Path'):
158213
data = jmespath.search(self.resolution['Path'], data)
159-
self._wizard.env.store(self.resolution['Key'], data)
214+
self._env.store(self.resolution['Key'], data)
160215

161216
def get_next_stage(self):
162-
"""Resolves the next stage name for the stage after this one.
217+
"""Resolve the next stage name for the stage after this one.
163218
164219
:rtype: str
165220
:return: The name of the next stage.
@@ -169,11 +224,12 @@ def get_next_stage(self):
169224
elif self.next_stage['Type'] == 'Name':
170225
return self.next_stage['Name']
171226
elif self.next_stage['Type'] == 'Variable':
172-
return self._wizard.env.retrieve(self.next_stage['Name'])
227+
return self._env.retrieve(self.next_stage['Name'])
173228

174229
# Executes all three steps of the stage
175230
def execute(self):
176-
"""Executes all steps in the stage if they are present.
231+
"""Execute all steps in the stage if they are present.
232+
177233
1) Perform Retrieval.
178234
2) Perform Interaction on retrieved data.
179235
3) Perform Resolution to store data in the environment.
@@ -184,9 +240,7 @@ def execute(self):
184240

185241

186242
class Environment(object):
187-
"""This class is used to store variables into a dict and retrieve them
188-
via JMESPath queries instead of normal keys.
189-
"""
243+
"""Store vars into a dict and retrives them with JMESPath queries."""
190244

191245
def __init__(self):
192246
self._variables = {}
@@ -195,7 +249,7 @@ def __str__(self):
195249
return json.dumps(self._variables, indent=4, sort_keys=True)
196250

197251
def store(self, key, val):
198-
"""Stores a variable under the given key.
252+
"""Store a variable under the given key.
199253
200254
:type key: str
201255
:param key: The key to store the value as.
@@ -206,18 +260,19 @@ def store(self, key, val):
206260
self._variables[key] = val
207261

208262
def retrieve(self, path):
209-
"""Retrieves the variable corresponding to the given JMESPath query.
263+
"""Retrieve the variable corresponding to the given JMESPath query.
210264
211265
:type path: str
212266
:param path: The JMESPath query to be used when locating the variable.
213267
"""
214268
return jmespath.search(path, self._variables)
215269

216270
def resolve_parameters(self, keys):
217-
"""Resolves all keys in the given keys dict. Expects all values in the
218-
keys dict to be JMESPath queries to be used when retrieving from the
219-
environment. Interpolates all values from their path to the actual
220-
value stored in the environment.
271+
"""Resolve all keys in the given keys dict.
272+
273+
Expects all values in the keys dict to be JMESPath queries to be used
274+
when retrieving from the environment. Interpolates all values from
275+
their path to the actual value stored in the environment.
221276
222277
:type keys: dict
223278
:param keys: A dict of keys to paths that need to be resolved.

tests/unit/test_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_chdir_syntax_error_prints_err_msg(errstream):
138138
chdir = mock.Mock()
139139
handler = app.ChangeDirHandler(err=errstream, chdir=chdir)
140140
handler.run(['.cd'], None)
141-
assert 'invalid syntax' in errstream.getvalue()
141+
assert 'Invalid syntax' in errstream.getvalue()
142142
assert not chdir.called
143143

144144

0 commit comments

Comments
 (0)