1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
---|
2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
---|
3 | import re |
---|
4 | import sys |
---|
5 | import os |
---|
6 | import pkg_resources |
---|
7 | from command import Command, BadCommand |
---|
8 | import copydir |
---|
9 | import pluginlib |
---|
10 | import fnmatch |
---|
11 | try: |
---|
12 | set |
---|
13 | except NameError: |
---|
14 | from sets import Set as set |
---|
15 | |
---|
16 | class CreateDistroCommand(Command): |
---|
17 | |
---|
18 | usage = 'PACKAGE_NAME [VAR=VALUE VAR2=VALUE2 ...]' |
---|
19 | summary = "Create the file layout for a Python distribution" |
---|
20 | short_description = summary |
---|
21 | |
---|
22 | description = """\ |
---|
23 | Create a new project. Projects are typically Python packages, |
---|
24 | ready for distribution. Projects are created from templates, and |
---|
25 | represent different kinds of projects -- associated with a |
---|
26 | particular framework for instance. |
---|
27 | """ |
---|
28 | |
---|
29 | parser = Command.standard_parser( |
---|
30 | simulate=True, no_interactive=True, quiet=True, overwrite=True) |
---|
31 | parser.add_option('-t', '--template', |
---|
32 | dest='templates', |
---|
33 | metavar='TEMPLATE', |
---|
34 | action='append', |
---|
35 | help="Add a template to the create process") |
---|
36 | parser.add_option('-o', '--output-dir', |
---|
37 | dest='output_dir', |
---|
38 | metavar='DIR', |
---|
39 | default='.', |
---|
40 | help="Write put the directory into DIR (default current directory)") |
---|
41 | parser.add_option('--svn-repository', |
---|
42 | dest='svn_repository', |
---|
43 | metavar='REPOS', |
---|
44 | help="Create package at given repository location (this will create the standard trunk/ tags/ branches/ hierarchy)") |
---|
45 | parser.add_option('--list-templates', |
---|
46 | dest='list_templates', |
---|
47 | action='store_true', |
---|
48 | help="List all templates available") |
---|
49 | parser.add_option('--list-variables', |
---|
50 | dest="list_variables", |
---|
51 | action="store_true", |
---|
52 | help="List all variables expected by the given template (does not create a package)") |
---|
53 | parser.add_option('--inspect-files', |
---|
54 | dest='inspect_files', |
---|
55 | action='store_true', |
---|
56 | help="Show where the files in the given (already created) directory came from (useful when using multiple templates)") |
---|
57 | parser.add_option('--config', |
---|
58 | action='store', |
---|
59 | dest='config', |
---|
60 | help="Template variables file") |
---|
61 | |
---|
62 | _bad_chars_re = re.compile('[^a-zA-Z0-9_]') |
---|
63 | |
---|
64 | default_verbosity = 1 |
---|
65 | default_interactive = 1 |
---|
66 | |
---|
67 | def command(self): |
---|
68 | if self.options.list_templates: |
---|
69 | return self.list_templates() |
---|
70 | asked_tmpls = self.options.templates or ['basic_package'] |
---|
71 | templates = [] |
---|
72 | for tmpl_name in asked_tmpls: |
---|
73 | self.extend_templates(templates, tmpl_name) |
---|
74 | if self.options.list_variables: |
---|
75 | return self.list_variables(templates) |
---|
76 | if self.verbose: |
---|
77 | print 'Selected and implied templates:' |
---|
78 | max_tmpl_name = max([len(tmpl_name) for tmpl_name, tmpl in templates]) |
---|
79 | for tmpl_name, tmpl in templates: |
---|
80 | print ' %s%s %s' % ( |
---|
81 | tmpl_name, ' '*(max_tmpl_name-len(tmpl_name)), |
---|
82 | tmpl.summary) |
---|
83 | print |
---|
84 | if not self.args: |
---|
85 | if self.interactive: |
---|
86 | dist_name = self.challenge('Enter project name') |
---|
87 | else: |
---|
88 | raise BadCommand('You must provide a PACKAGE_NAME') |
---|
89 | else: |
---|
90 | dist_name = self.args[0].lstrip(os.path.sep) |
---|
91 | |
---|
92 | templates = [tmpl for name, tmpl in templates] |
---|
93 | output_dir = os.path.join(self.options.output_dir, dist_name) |
---|
94 | |
---|
95 | pkg_name = self._bad_chars_re.sub('', dist_name.lower()) |
---|
96 | vars = {'project': dist_name, |
---|
97 | 'package': pkg_name, |
---|
98 | 'egg': pluginlib.egg_name(dist_name), |
---|
99 | } |
---|
100 | vars.update(self.parse_vars(self.args[1:])) |
---|
101 | if self.options.config and os.path.exists(self.options.config): |
---|
102 | for key, value in self.read_vars(self.options.config).items(): |
---|
103 | vars.setdefault(key, value) |
---|
104 | |
---|
105 | if self.verbose: # @@: > 1? |
---|
106 | self.display_vars(vars) |
---|
107 | |
---|
108 | if self.options.inspect_files: |
---|
109 | self.inspect_files( |
---|
110 | output_dir, templates, vars) |
---|
111 | return |
---|
112 | if not os.path.exists(output_dir): |
---|
113 | # We want to avoid asking questions in copydir if the path |
---|
114 | # doesn't exist yet |
---|
115 | copydir.all_answer = 'y' |
---|
116 | |
---|
117 | if self.options.svn_repository: |
---|
118 | self.setup_svn_repository(output_dir, dist_name) |
---|
119 | |
---|
120 | # First we want to make sure all the templates get a chance to |
---|
121 | # set their variables, all at once, with the most specialized |
---|
122 | # template going first (the last template is the most |
---|
123 | # specialized)... |
---|
124 | for template in templates[::-1]: |
---|
125 | vars = template.check_vars(vars, self) |
---|
126 | |
---|
127 | # Gather all the templates egg_plugins into one var |
---|
128 | egg_plugins = set() |
---|
129 | for template in templates: |
---|
130 | egg_plugins.update(template.egg_plugins) |
---|
131 | egg_plugins = list(egg_plugins) |
---|
132 | egg_plugins.sort() |
---|
133 | vars['egg_plugins'] = egg_plugins |
---|
134 | |
---|
135 | for template in templates: |
---|
136 | self.create_template( |
---|
137 | template, output_dir, vars) |
---|
138 | |
---|
139 | found_setup_py = False |
---|
140 | paster_plugins_mtime = None |
---|
141 | if os.path.exists(os.path.join(output_dir, 'setup.py')): |
---|
142 | # Grab paster_plugins.txt's mtime; used to determine if the |
---|
143 | # egg_info command wrote to it |
---|
144 | try: |
---|
145 | egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name) |
---|
146 | except IOError: |
---|
147 | egg_info_dir = None |
---|
148 | if egg_info_dir is not None: |
---|
149 | plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt') |
---|
150 | if os.path.exists(plugins_path): |
---|
151 | paster_plugins_mtime = os.path.getmtime(plugins_path) |
---|
152 | |
---|
153 | self.run_command(sys.executable, 'setup.py', 'egg_info', |
---|
154 | cwd=output_dir, |
---|
155 | # This shouldn't be necessary, but a bug in setuptools 0.6c3 is causing a (not entirely fatal) problem that I don't want to fix right now: |
---|
156 | expect_returncode=True) |
---|
157 | found_setup_py = True |
---|
158 | elif self.verbose > 1: |
---|
159 | print 'No setup.py (cannot run egg_info)' |
---|
160 | |
---|
161 | package_dir = vars.get('package_dir', None) |
---|
162 | if package_dir: |
---|
163 | output_dir = os.path.join(output_dir, package_dir) |
---|
164 | |
---|
165 | # With no setup.py this doesn't make sense: |
---|
166 | if found_setup_py: |
---|
167 | # Only write paster_plugins.txt if it wasn't written by |
---|
168 | # egg_info (the correct way). leaving us to do it is |
---|
169 | # deprecated and you'll get warned |
---|
170 | egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name) |
---|
171 | plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt') |
---|
172 | if len(egg_plugins) and (not os.path.exists(plugins_path) or \ |
---|
173 | os.path.getmtime(plugins_path) == paster_plugins_mtime): |
---|
174 | if self.verbose: |
---|
175 | print >> sys.stderr, \ |
---|
176 | ('Manually creating paster_plugins.txt (deprecated! ' |
---|
177 | 'pass a paster_plugins keyword to setup() instead)') |
---|
178 | for plugin in egg_plugins: |
---|
179 | if self.verbose: |
---|
180 | print 'Adding %s to paster_plugins.txt' % plugin |
---|
181 | if not self.simulate: |
---|
182 | pluginlib.add_plugin(egg_info_dir, plugin) |
---|
183 | |
---|
184 | if self.options.svn_repository: |
---|
185 | self.add_svn_repository(vars, output_dir) |
---|
186 | |
---|
187 | if self.options.config: |
---|
188 | write_vars = vars.copy() |
---|
189 | del write_vars['project'] |
---|
190 | del write_vars['package'] |
---|
191 | self.write_vars(self.options.config, write_vars) |
---|
192 | |
---|
193 | def create_template(self, template, output_dir, vars): |
---|
194 | if self.verbose: |
---|
195 | print 'Creating template %s' % template.name |
---|
196 | template.run(self, output_dir, vars) |
---|
197 | |
---|
198 | def setup_svn_repository(self, output_dir, dist_name): |
---|
199 | # @@: Use subprocess |
---|
200 | svn_repos = self.options.svn_repository |
---|
201 | svn_repos_path = os.path.join(svn_repos, dist_name).replace('\\','/') |
---|
202 | svn_command = 'svn' |
---|
203 | if sys.platform == 'win32': |
---|
204 | svn_command += '.exe' |
---|
205 | # @@: The previous method of formatting this string using \ doesn't work on Windows |
---|
206 | cmd = '%(svn_command)s mkdir %(svn_repos_path)s' + \ |
---|
207 | ' %(svn_repos_path)s/trunk %(svn_repos_path)s/tags' + \ |
---|
208 | ' %(svn_repos_path)s/branches -m "New project %(dist_name)s"' |
---|
209 | cmd = cmd % { |
---|
210 | 'svn_repos_path': svn_repos_path, |
---|
211 | 'dist_name': dist_name, |
---|
212 | 'svn_command':svn_command, |
---|
213 | } |
---|
214 | if self.verbose: |
---|
215 | print "Running:" |
---|
216 | print cmd |
---|
217 | if not self.simulate: |
---|
218 | os.system(cmd) |
---|
219 | svn_repos_path_trunk = os.path.join(svn_repos_path,'trunk').replace('\\','/') |
---|
220 | cmd = svn_command+' co "%s" "%s"' % (svn_repos_path_trunk, output_dir) |
---|
221 | if self.verbose: |
---|
222 | print "Running %s" % cmd |
---|
223 | if not self.simulate: |
---|
224 | os.system(cmd) |
---|
225 | |
---|
226 | ignore_egg_info_files = [ |
---|
227 | 'top_level.txt', |
---|
228 | 'entry_points.txt', |
---|
229 | 'requires.txt', |
---|
230 | 'PKG-INFO', |
---|
231 | 'namespace_packages.txt', |
---|
232 | 'SOURCES.txt', |
---|
233 | 'dependency_links.txt', |
---|
234 | 'not-zip-safe'] |
---|
235 | |
---|
236 | def add_svn_repository(self, vars, output_dir): |
---|
237 | svn_repos = self.options.svn_repository |
---|
238 | egg_info_dir = pluginlib.egg_info_dir(output_dir, vars['project']) |
---|
239 | svn_command = 'svn' |
---|
240 | if sys.platform == 'win32': |
---|
241 | svn_command += '.exe' |
---|
242 | self.run_command(svn_command, 'add', '-N', egg_info_dir) |
---|
243 | paster_plugins_file = os.path.join( |
---|
244 | egg_info_dir, 'paster_plugins.txt') |
---|
245 | if os.path.exists(paster_plugins_file): |
---|
246 | self.run_command(svn_command, 'add', paster_plugins_file) |
---|
247 | self.run_command(svn_command, 'ps', 'svn:ignore', |
---|
248 | '\n'.join(self.ignore_egg_info_files), |
---|
249 | egg_info_dir) |
---|
250 | if self.verbose: |
---|
251 | print ("You must next run 'svn commit' to commit the " |
---|
252 | "files to repository") |
---|
253 | |
---|
254 | def extend_templates(self, templates, tmpl_name): |
---|
255 | if '#' in tmpl_name: |
---|
256 | dist_name, tmpl_name = tmpl_name.split('#', 1) |
---|
257 | else: |
---|
258 | dist_name, tmpl_name = None, tmpl_name |
---|
259 | if dist_name is None: |
---|
260 | for entry in self.all_entry_points(): |
---|
261 | if entry.name == tmpl_name: |
---|
262 | tmpl = entry.load()(entry.name) |
---|
263 | dist_name = entry.dist.project_name |
---|
264 | break |
---|
265 | else: |
---|
266 | raise LookupError( |
---|
267 | 'Template by name %r not found' % tmpl_name) |
---|
268 | else: |
---|
269 | dist = pkg_resources.get_distribution(dist_name) |
---|
270 | entry = dist.get_entry_info( |
---|
271 | 'paste.paster_create_template', tmpl_name) |
---|
272 | tmpl = entry.load()(entry.name) |
---|
273 | full_name = '%s#%s' % (dist_name, tmpl_name) |
---|
274 | for item_full_name, item_tmpl in templates: |
---|
275 | if item_full_name == full_name: |
---|
276 | # Already loaded |
---|
277 | return |
---|
278 | for req_name in tmpl.required_templates: |
---|
279 | self.extend_templates(templates, req_name) |
---|
280 | templates.append((full_name, tmpl)) |
---|
281 | |
---|
282 | def all_entry_points(self): |
---|
283 | if not hasattr(self, '_entry_points'): |
---|
284 | self._entry_points = list(pkg_resources.iter_entry_points( |
---|
285 | 'paste.paster_create_template')) |
---|
286 | return self._entry_points |
---|
287 | |
---|
288 | def display_vars(self, vars): |
---|
289 | vars = vars.items() |
---|
290 | vars.sort() |
---|
291 | print 'Variables:' |
---|
292 | max_var = max([len(n) for n, v in vars]) |
---|
293 | for name, value in vars: |
---|
294 | print ' %s:%s %s' % ( |
---|
295 | name, ' '*(max_var-len(name)), value) |
---|
296 | |
---|
297 | def list_templates(self): |
---|
298 | templates = [] |
---|
299 | for entry in self.all_entry_points(): |
---|
300 | try: |
---|
301 | templates.append(entry.load()(entry.name)) |
---|
302 | except Exception, e: |
---|
303 | # We will not be stopped! |
---|
304 | print 'Warning: could not load entry point %s (%s: %s)' % ( |
---|
305 | entry.name, e.__class__.__name__, e) |
---|
306 | max_name = max([len(t.name) for t in templates]) |
---|
307 | templates.sort(lambda a, b: cmp(a.name, b.name)) |
---|
308 | print 'Available templates:' |
---|
309 | for template in templates: |
---|
310 | # @@: Wrap description |
---|
311 | print ' %s:%s %s' % ( |
---|
312 | template.name, |
---|
313 | ' '*(max_name-len(template.name)), |
---|
314 | template.summary) |
---|
315 | |
---|
316 | def inspect_files(self, output_dir, templates, vars): |
---|
317 | file_sources = {} |
---|
318 | for template in templates: |
---|
319 | self._find_files(template, vars, file_sources) |
---|
320 | self._show_files(output_dir, file_sources) |
---|
321 | self._show_leftovers(output_dir, file_sources) |
---|
322 | |
---|
323 | def _find_files(self, template, vars, file_sources): |
---|
324 | tmpl_dir = template.template_dir() |
---|
325 | self._find_template_files( |
---|
326 | template, tmpl_dir, vars, file_sources) |
---|
327 | |
---|
328 | def _find_template_files(self, template, tmpl_dir, vars, |
---|
329 | file_sources, join=''): |
---|
330 | full_dir = os.path.join(tmpl_dir, join) |
---|
331 | for name in os.listdir(full_dir): |
---|
332 | if name.startswith('.'): |
---|
333 | continue |
---|
334 | if os.path.isdir(os.path.join(full_dir, name)): |
---|
335 | self._find_template_files( |
---|
336 | template, tmpl_dir, vars, file_sources, |
---|
337 | join=os.path.join(join, name)) |
---|
338 | continue |
---|
339 | partial = os.path.join(join, name) |
---|
340 | for name, value in vars.items(): |
---|
341 | partial = partial.replace('+%s+' % name, value) |
---|
342 | if partial.endswith('_tmpl'): |
---|
343 | partial = partial[:-5] |
---|
344 | file_sources.setdefault(partial, []).append(template) |
---|
345 | |
---|
346 | _ignore_filenames = ['.*', '*.pyc', '*.bak*'] |
---|
347 | _ignore_dirs = ['CVS', '_darcs', '.svn'] |
---|
348 | |
---|
349 | def _show_files(self, output_dir, file_sources, join='', indent=0): |
---|
350 | pad = ' '*(2*indent) |
---|
351 | full_dir = os.path.join(output_dir, join) |
---|
352 | names = os.listdir(full_dir) |
---|
353 | dirs = [n for n in names |
---|
354 | if os.path.isdir(os.path.join(full_dir, n))] |
---|
355 | fns = [n for n in names |
---|
356 | if not os.path.isdir(os.path.join(full_dir, n))] |
---|
357 | dirs.sort() |
---|
358 | names.sort() |
---|
359 | for name in names: |
---|
360 | skip_this = False |
---|
361 | for ext in self._ignore_filenames: |
---|
362 | if fnmatch.fnmatch(name, ext): |
---|
363 | if self.verbose > 1: |
---|
364 | print '%sIgnoring %s' % (pad, name) |
---|
365 | skip_this = True |
---|
366 | break |
---|
367 | if skip_this: |
---|
368 | continue |
---|
369 | partial = os.path.join(join, name) |
---|
370 | if partial not in file_sources: |
---|
371 | if self.verbose > 1: |
---|
372 | print '%s%s (not from template)' % (pad, name) |
---|
373 | continue |
---|
374 | templates = file_sources.pop(partial) |
---|
375 | print '%s%s from:' % (pad, name) |
---|
376 | for template in templates: |
---|
377 | print '%s %s' % (pad, template.name) |
---|
378 | for dir in dirs: |
---|
379 | if dir in self._ignore_dirs: |
---|
380 | continue |
---|
381 | print '%sRecursing into %s/' % (pad, dir) |
---|
382 | self._show_files( |
---|
383 | output_dir, file_sources, |
---|
384 | join=os.path.join(join, dir), |
---|
385 | indent=indent+1) |
---|
386 | |
---|
387 | def _show_leftovers(self, output_dir, file_sources): |
---|
388 | if not file_sources: |
---|
389 | return |
---|
390 | print |
---|
391 | print 'These files were supposed to be generated by templates' |
---|
392 | print 'but were not found:' |
---|
393 | file_sources = file_sources.items() |
---|
394 | file_sources.sort() |
---|
395 | for partial, templates in file_sources: |
---|
396 | print ' %s from:' % partial |
---|
397 | for template in templates: |
---|
398 | print ' %s' % template.name |
---|
399 | |
---|
400 | def list_variables(self, templates): |
---|
401 | for tmpl_name, tmpl in templates: |
---|
402 | if not tmpl.read_vars(): |
---|
403 | if self.verbose > 1: |
---|
404 | self._show_template_vars( |
---|
405 | tmpl_name, tmpl, 'No variables found') |
---|
406 | continue |
---|
407 | self._show_template_vars(tmpl_name, tmpl) |
---|
408 | |
---|
409 | def _show_template_vars(self, tmpl_name, tmpl, message=None): |
---|
410 | title = '%s (from %s)' % (tmpl.name, tmpl_name) |
---|
411 | print title |
---|
412 | print '-'*len(title) |
---|
413 | if message is not None: |
---|
414 | print ' %s' % message |
---|
415 | print |
---|
416 | return |
---|
417 | tmpl.print_vars(indent=2) |
---|