forked from aws/aws-sam-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtemplate.py
More file actions
195 lines (177 loc) · 9.11 KB
/
Copy pathtemplate.py
File metadata and controls
195 lines (177 loc) · 9.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
"""
This is the core module of the cookiecutter workflow, it defines how to create a template, prompt the user for
values of the context and how to generate a project from the given template and provided context
"""
import logging
from typing import Dict, List, Optional
from cookiecutter.exceptions import RepositoryNotFound, UnknownRepoType
from cookiecutter.main import cookiecutter
from samcli.commands.exceptions import UserException
from samcli.lib.cookiecutter.exceptions import (
GenerateProjectFailedError,
InvalidLocationError,
PostprocessingError,
PreprocessingError,
)
from samcli.lib.cookiecutter.interactive_flow import InteractiveFlow
from samcli.lib.cookiecutter.plugin import Plugin
from samcli.lib.cookiecutter.processor import Processor
from samcli.lib.init.arbitrary_project import generate_non_cookiecutter_project
LOG = logging.getLogger(__name__)
class Template:
"""
A class for project generation based on cookiecutter template
Attributes
----------
location: str
Template location (git, mercurial, http(s), path)
interactive_flows: Optional[List[InteractiveFlow]]
An optional series of interactive questions to be asked to the user where the answers are used to fulfill
the values of the cookiecutter.json keys, i.e. the cookiecutter context
preprocessors: Optional[List[Processor]]
An optional series of processes to be executed before generating the cookiecutter project. These processes
are usually used to manipulate the cookiecutter context or doing some bootstrapping before generating
the cookiecutter project.
preprocessors are different than cookiecutter's hooks in the sense that they can mutate the context while
the pre_gen_project hooks can't.
postprocessors: Optional[List[Processor]]
An optional series of processes to be executed after generating the cookiecutter project. These processes
are usually used to clean up resources and to print messages to the user after successfully generating
the cookiecutter project.
plugins: Optional[List[Plugin]]
An optional series of plugins to be plugged in. A plugin defines its own interactive_flow, preprocessor
and postprocessor. A plugin is a sub-set of the template, if there is a common behavior among multiple
templates, it is better to be extracted to a plugin that can then be plugged in to each of these templates.
metadata: Optional[Dict]
An optional dictionary with extra information about the template
Methods
-------
run_interactive_flows() -> Dict:
runs the interactive_flows(questions) one be one then combines and returns the answers in one dictionary
in the form of {question.key: answer}
generate_project(context) -> None:
runs the preprocessors chain passing in the given context to the first processor which process it then produce
a new context which is passed to the next preprocessor and so on.
After that, it execute the cookiecutter generator to generate the project with the final preprocessed context.
Finally, it runs the postprocessors chain with the same manner like the preprocessors.
"""
def __init__(
self,
location: str,
interactive_flows: Optional[List[InteractiveFlow]] = None,
preprocessors: Optional[List[Processor]] = None,
postprocessors: Optional[List[Processor]] = None,
plugins: Optional[List[Plugin]] = None,
metadata: Optional[Dict] = None,
):
"""
Initialize the class
Parameters
----------
location: str
Template location (git, mercurial, http(s), path)
interactive_flows: Optional[List[InteractiveFlow]]
An optional series of interactive questions to be asked to the user where the answers are used to fulfill
the values of the cookiecutter.json keys, i.e. the cookiecutter context
preprocessors: Optional[List[Processor]]
An optional series of processes to be executed before generating the cookiecutter project. These processes
are usually used to manipulate the cookiecutter context or doing some bootstrapping before generating
the cookiecutter project.
postprocessors: Optional[List[Processor]]
An optional series of processes to be executed after generating the cookiecutter project. These processes
are usually used to clean up resources and to print messages to the user after successfully generating
the cookiecutter project.
plugins: Optional[List[Plugin]]
An optional series of plugins to be plugged in. A plugin defines its own interactive_flow, preprocessor
and postprocessor. A plugin is a sub-set of the template, if there is a common behavior among multiple
templates, it is better to be extracted to a plugin that can then be plugged in to each of these templates.
metadata: Optional[Dict]
An optional dictionary with extra information about the template
"""
self._location = location
self._interactive_flows = interactive_flows or []
self._preprocessors = preprocessors or []
self._postprocessors = postprocessors or []
self._plugins = plugins or []
for plugin in self._plugins:
if plugin.interactive_flow:
self._interactive_flows.append(plugin.interactive_flow)
if plugin.preprocessor:
self._preprocessors.append(plugin.preprocessor)
if plugin.postprocessor:
self._postprocessors.append(plugin.postprocessor)
self.metadata = metadata
def run_interactive_flows(self, context: Optional[Dict] = None) -> Dict:
"""
prompt the user a series of questions' flows and gather the answers to create the cookiecutter context.
The questions are identified by keys. If multiple questions, whether within the same flow or across
different flows, have the same key, the last question will override the others and we will get only the
answer of this question.
Raises:
UserException(ClickException) if anything went wrong.
Returns:
A Dictionary in the form of {question.key: answer} representing user's answers to the flows' questions
"""
try:
context = context if context else {}
context["shared_values"] = "default"
for flow in self._interactive_flows:
context = flow.run(context)
return context
except Exception as e:
raise UserException(str(e), wrapped_from=e.__class__.__name__) from e
def generate_project(self, context: Dict, output_dir: str) -> None:
"""
Generates a project based on this cookiecutter template and the given context. The context is first
processed and manipulated by series of preprocessors(if any) then the project is generated and finally
the series of postprocessors(if any) are executed.
Parameters
----------
context: Dict
the cookiecutter context to fulfill the values of cookiecutter.json keys
output_dir: str
the directory where project will be generated in
Raises
------
PreprocessingError
xxx
InvalidLocationError
if the given location is not from a known repo type
GenerateProjectFailedError
if something else went wrong
PostprocessingError
yyy
"""
try:
LOG.debug("preprocessing the cookiecutter context")
for processor in self._preprocessors:
context = processor.run(context)
except Exception as e:
raise PreprocessingError(template=self._location, provider_error=e) from e
try:
LOG.debug("Baking a new template with cookiecutter with all parameters")
cookiecutter(
template=self._location,
output_dir=output_dir,
no_input=True,
extra_context=context,
overwrite_if_exists=True,
)
except RepositoryNotFound:
# cookiecutter.json is not found in the template. Let's just clone it directly without
# using cookiecutter and call it done.
LOG.debug(
"Unable to find cookiecutter.json in the project. Downloading it directly without treating "
"it as a cookiecutter template"
)
generate_non_cookiecutter_project(location=self._location, output_dir=".")
except UnknownRepoType as e:
raise InvalidLocationError(template=self._location) from e
except Exception as e:
raise GenerateProjectFailedError(template=self._location, provider_error=e) from e
try:
LOG.debug("postprocessing the cookiecutter context")
for processor in self._postprocessors:
context = processor.run(context)
except Exception as e:
raise PostprocessingError(template=self._location, provider_error=e) from e