[3] | 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) |
---|