| 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) |
|---|