418 lines
17 KiB
Python
Executable File
418 lines
17 KiB
Python
Executable File
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
import re
|
|
import sys
|
|
import os
|
|
import pkg_resources
|
|
from command import Command, BadCommand
|
|
import copydir
|
|
import pluginlib
|
|
import fnmatch
|
|
try:
|
|
set
|
|
except NameError:
|
|
from sets import Set as set
|
|
|
|
class CreateDistroCommand(Command):
|
|
|
|
usage = 'PACKAGE_NAME [VAR=VALUE VAR2=VALUE2 ...]'
|
|
summary = "Create the file layout for a Python distribution"
|
|
short_description = summary
|
|
|
|
description = """\
|
|
Create a new project. Projects are typically Python packages,
|
|
ready for distribution. Projects are created from templates, and
|
|
represent different kinds of projects -- associated with a
|
|
particular framework for instance.
|
|
"""
|
|
|
|
parser = Command.standard_parser(
|
|
simulate=True, no_interactive=True, quiet=True, overwrite=True)
|
|
parser.add_option('-t', '--template',
|
|
dest='templates',
|
|
metavar='TEMPLATE',
|
|
action='append',
|
|
help="Add a template to the create process")
|
|
parser.add_option('-o', '--output-dir',
|
|
dest='output_dir',
|
|
metavar='DIR',
|
|
default='.',
|
|
help="Write put the directory into DIR (default current directory)")
|
|
parser.add_option('--svn-repository',
|
|
dest='svn_repository',
|
|
metavar='REPOS',
|
|
help="Create package at given repository location (this will create the standard trunk/ tags/ branches/ hierarchy)")
|
|
parser.add_option('--list-templates',
|
|
dest='list_templates',
|
|
action='store_true',
|
|
help="List all templates available")
|
|
parser.add_option('--list-variables',
|
|
dest="list_variables",
|
|
action="store_true",
|
|
help="List all variables expected by the given template (does not create a package)")
|
|
parser.add_option('--inspect-files',
|
|
dest='inspect_files',
|
|
action='store_true',
|
|
help="Show where the files in the given (already created) directory came from (useful when using multiple templates)")
|
|
parser.add_option('--config',
|
|
action='store',
|
|
dest='config',
|
|
help="Template variables file")
|
|
|
|
_bad_chars_re = re.compile('[^a-zA-Z0-9_]')
|
|
|
|
default_verbosity = 1
|
|
default_interactive = 1
|
|
|
|
def command(self):
|
|
if self.options.list_templates:
|
|
return self.list_templates()
|
|
asked_tmpls = self.options.templates or ['basic_package']
|
|
templates = []
|
|
for tmpl_name in asked_tmpls:
|
|
self.extend_templates(templates, tmpl_name)
|
|
if self.options.list_variables:
|
|
return self.list_variables(templates)
|
|
if self.verbose:
|
|
print 'Selected and implied templates:'
|
|
max_tmpl_name = max([len(tmpl_name) for tmpl_name, tmpl in templates])
|
|
for tmpl_name, tmpl in templates:
|
|
print ' %s%s %s' % (
|
|
tmpl_name, ' '*(max_tmpl_name-len(tmpl_name)),
|
|
tmpl.summary)
|
|
print
|
|
if not self.args:
|
|
if self.interactive:
|
|
dist_name = self.challenge('Enter project name')
|
|
else:
|
|
raise BadCommand('You must provide a PACKAGE_NAME')
|
|
else:
|
|
dist_name = self.args[0].lstrip(os.path.sep)
|
|
|
|
templates = [tmpl for name, tmpl in templates]
|
|
output_dir = os.path.join(self.options.output_dir, dist_name)
|
|
|
|
pkg_name = self._bad_chars_re.sub('', dist_name.lower())
|
|
vars = {'project': dist_name,
|
|
'package': pkg_name,
|
|
'egg': pluginlib.egg_name(dist_name),
|
|
}
|
|
vars.update(self.parse_vars(self.args[1:]))
|
|
if self.options.config and os.path.exists(self.options.config):
|
|
for key, value in self.read_vars(self.options.config).items():
|
|
vars.setdefault(key, value)
|
|
|
|
if self.verbose: # @@: > 1?
|
|
self.display_vars(vars)
|
|
|
|
if self.options.inspect_files:
|
|
self.inspect_files(
|
|
output_dir, templates, vars)
|
|
return
|
|
if not os.path.exists(output_dir):
|
|
# We want to avoid asking questions in copydir if the path
|
|
# doesn't exist yet
|
|
copydir.all_answer = 'y'
|
|
|
|
if self.options.svn_repository:
|
|
self.setup_svn_repository(output_dir, dist_name)
|
|
|
|
# First we want to make sure all the templates get a chance to
|
|
# set their variables, all at once, with the most specialized
|
|
# template going first (the last template is the most
|
|
# specialized)...
|
|
for template in templates[::-1]:
|
|
vars = template.check_vars(vars, self)
|
|
|
|
# Gather all the templates egg_plugins into one var
|
|
egg_plugins = set()
|
|
for template in templates:
|
|
egg_plugins.update(template.egg_plugins)
|
|
egg_plugins = list(egg_plugins)
|
|
egg_plugins.sort()
|
|
vars['egg_plugins'] = egg_plugins
|
|
|
|
for template in templates:
|
|
self.create_template(
|
|
template, output_dir, vars)
|
|
|
|
found_setup_py = False
|
|
paster_plugins_mtime = None
|
|
if os.path.exists(os.path.join(output_dir, 'setup.py')):
|
|
# Grab paster_plugins.txt's mtime; used to determine if the
|
|
# egg_info command wrote to it
|
|
try:
|
|
egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name)
|
|
except IOError:
|
|
egg_info_dir = None
|
|
if egg_info_dir is not None:
|
|
plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt')
|
|
if os.path.exists(plugins_path):
|
|
paster_plugins_mtime = os.path.getmtime(plugins_path)
|
|
|
|
self.run_command(sys.executable, 'setup.py', 'egg_info',
|
|
cwd=output_dir,
|
|
# 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:
|
|
expect_returncode=True)
|
|
found_setup_py = True
|
|
elif self.verbose > 1:
|
|
print 'No setup.py (cannot run egg_info)'
|
|
|
|
package_dir = vars.get('package_dir', None)
|
|
if package_dir:
|
|
output_dir = os.path.join(output_dir, package_dir)
|
|
|
|
# With no setup.py this doesn't make sense:
|
|
if found_setup_py:
|
|
# Only write paster_plugins.txt if it wasn't written by
|
|
# egg_info (the correct way). leaving us to do it is
|
|
# deprecated and you'll get warned
|
|
egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name)
|
|
plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt')
|
|
if len(egg_plugins) and (not os.path.exists(plugins_path) or \
|
|
os.path.getmtime(plugins_path) == paster_plugins_mtime):
|
|
if self.verbose:
|
|
print >> sys.stderr, \
|
|
('Manually creating paster_plugins.txt (deprecated! '
|
|
'pass a paster_plugins keyword to setup() instead)')
|
|
for plugin in egg_plugins:
|
|
if self.verbose:
|
|
print 'Adding %s to paster_plugins.txt' % plugin
|
|
if not self.simulate:
|
|
pluginlib.add_plugin(egg_info_dir, plugin)
|
|
|
|
if self.options.svn_repository:
|
|
self.add_svn_repository(vars, output_dir)
|
|
|
|
if self.options.config:
|
|
write_vars = vars.copy()
|
|
del write_vars['project']
|
|
del write_vars['package']
|
|
self.write_vars(self.options.config, write_vars)
|
|
|
|
def create_template(self, template, output_dir, vars):
|
|
if self.verbose:
|
|
print 'Creating template %s' % template.name
|
|
template.run(self, output_dir, vars)
|
|
|
|
def setup_svn_repository(self, output_dir, dist_name):
|
|
# @@: Use subprocess
|
|
svn_repos = self.options.svn_repository
|
|
svn_repos_path = os.path.join(svn_repos, dist_name).replace('\\','/')
|
|
svn_command = 'svn'
|
|
if sys.platform == 'win32':
|
|
svn_command += '.exe'
|
|
# @@: The previous method of formatting this string using \ doesn't work on Windows
|
|
cmd = '%(svn_command)s mkdir %(svn_repos_path)s' + \
|
|
' %(svn_repos_path)s/trunk %(svn_repos_path)s/tags' + \
|
|
' %(svn_repos_path)s/branches -m "New project %(dist_name)s"'
|
|
cmd = cmd % {
|
|
'svn_repos_path': svn_repos_path,
|
|
'dist_name': dist_name,
|
|
'svn_command':svn_command,
|
|
}
|
|
if self.verbose:
|
|
print "Running:"
|
|
print cmd
|
|
if not self.simulate:
|
|
os.system(cmd)
|
|
svn_repos_path_trunk = os.path.join(svn_repos_path,'trunk').replace('\\','/')
|
|
cmd = svn_command+' co "%s" "%s"' % (svn_repos_path_trunk, output_dir)
|
|
if self.verbose:
|
|
print "Running %s" % cmd
|
|
if not self.simulate:
|
|
os.system(cmd)
|
|
|
|
ignore_egg_info_files = [
|
|
'top_level.txt',
|
|
'entry_points.txt',
|
|
'requires.txt',
|
|
'PKG-INFO',
|
|
'namespace_packages.txt',
|
|
'SOURCES.txt',
|
|
'dependency_links.txt',
|
|
'not-zip-safe']
|
|
|
|
def add_svn_repository(self, vars, output_dir):
|
|
svn_repos = self.options.svn_repository
|
|
egg_info_dir = pluginlib.egg_info_dir(output_dir, vars['project'])
|
|
svn_command = 'svn'
|
|
if sys.platform == 'win32':
|
|
svn_command += '.exe'
|
|
self.run_command(svn_command, 'add', '-N', egg_info_dir)
|
|
paster_plugins_file = os.path.join(
|
|
egg_info_dir, 'paster_plugins.txt')
|
|
if os.path.exists(paster_plugins_file):
|
|
self.run_command(svn_command, 'add', paster_plugins_file)
|
|
self.run_command(svn_command, 'ps', 'svn:ignore',
|
|
'\n'.join(self.ignore_egg_info_files),
|
|
egg_info_dir)
|
|
if self.verbose:
|
|
print ("You must next run 'svn commit' to commit the "
|
|
"files to repository")
|
|
|
|
def extend_templates(self, templates, tmpl_name):
|
|
if '#' in tmpl_name:
|
|
dist_name, tmpl_name = tmpl_name.split('#', 1)
|
|
else:
|
|
dist_name, tmpl_name = None, tmpl_name
|
|
if dist_name is None:
|
|
for entry in self.all_entry_points():
|
|
if entry.name == tmpl_name:
|
|
tmpl = entry.load()(entry.name)
|
|
dist_name = entry.dist.project_name
|
|
break
|
|
else:
|
|
raise LookupError(
|
|
'Template by name %r not found' % tmpl_name)
|
|
else:
|
|
dist = pkg_resources.get_distribution(dist_name)
|
|
entry = dist.get_entry_info(
|
|
'paste.paster_create_template', tmpl_name)
|
|
tmpl = entry.load()(entry.name)
|
|
full_name = '%s#%s' % (dist_name, tmpl_name)
|
|
for item_full_name, item_tmpl in templates:
|
|
if item_full_name == full_name:
|
|
# Already loaded
|
|
return
|
|
for req_name in tmpl.required_templates:
|
|
self.extend_templates(templates, req_name)
|
|
templates.append((full_name, tmpl))
|
|
|
|
def all_entry_points(self):
|
|
if not hasattr(self, '_entry_points'):
|
|
self._entry_points = list(pkg_resources.iter_entry_points(
|
|
'paste.paster_create_template'))
|
|
return self._entry_points
|
|
|
|
def display_vars(self, vars):
|
|
vars = vars.items()
|
|
vars.sort()
|
|
print 'Variables:'
|
|
max_var = max([len(n) for n, v in vars])
|
|
for name, value in vars:
|
|
print ' %s:%s %s' % (
|
|
name, ' '*(max_var-len(name)), value)
|
|
|
|
def list_templates(self):
|
|
templates = []
|
|
for entry in self.all_entry_points():
|
|
try:
|
|
templates.append(entry.load()(entry.name))
|
|
except Exception, e:
|
|
# We will not be stopped!
|
|
print 'Warning: could not load entry point %s (%s: %s)' % (
|
|
entry.name, e.__class__.__name__, e)
|
|
max_name = max([len(t.name) for t in templates])
|
|
templates.sort(lambda a, b: cmp(a.name, b.name))
|
|
print 'Available templates:'
|
|
for template in templates:
|
|
# @@: Wrap description
|
|
print ' %s:%s %s' % (
|
|
template.name,
|
|
' '*(max_name-len(template.name)),
|
|
template.summary)
|
|
|
|
def inspect_files(self, output_dir, templates, vars):
|
|
file_sources = {}
|
|
for template in templates:
|
|
self._find_files(template, vars, file_sources)
|
|
self._show_files(output_dir, file_sources)
|
|
self._show_leftovers(output_dir, file_sources)
|
|
|
|
def _find_files(self, template, vars, file_sources):
|
|
tmpl_dir = template.template_dir()
|
|
self._find_template_files(
|
|
template, tmpl_dir, vars, file_sources)
|
|
|
|
def _find_template_files(self, template, tmpl_dir, vars,
|
|
file_sources, join=''):
|
|
full_dir = os.path.join(tmpl_dir, join)
|
|
for name in os.listdir(full_dir):
|
|
if name.startswith('.'):
|
|
continue
|
|
if os.path.isdir(os.path.join(full_dir, name)):
|
|
self._find_template_files(
|
|
template, tmpl_dir, vars, file_sources,
|
|
join=os.path.join(join, name))
|
|
continue
|
|
partial = os.path.join(join, name)
|
|
for name, value in vars.items():
|
|
partial = partial.replace('+%s+' % name, value)
|
|
if partial.endswith('_tmpl'):
|
|
partial = partial[:-5]
|
|
file_sources.setdefault(partial, []).append(template)
|
|
|
|
_ignore_filenames = ['.*', '*.pyc', '*.bak*']
|
|
_ignore_dirs = ['CVS', '_darcs', '.svn']
|
|
|
|
def _show_files(self, output_dir, file_sources, join='', indent=0):
|
|
pad = ' '*(2*indent)
|
|
full_dir = os.path.join(output_dir, join)
|
|
names = os.listdir(full_dir)
|
|
dirs = [n for n in names
|
|
if os.path.isdir(os.path.join(full_dir, n))]
|
|
fns = [n for n in names
|
|
if not os.path.isdir(os.path.join(full_dir, n))]
|
|
dirs.sort()
|
|
names.sort()
|
|
for name in names:
|
|
skip_this = False
|
|
for ext in self._ignore_filenames:
|
|
if fnmatch.fnmatch(name, ext):
|
|
if self.verbose > 1:
|
|
print '%sIgnoring %s' % (pad, name)
|
|
skip_this = True
|
|
break
|
|
if skip_this:
|
|
continue
|
|
partial = os.path.join(join, name)
|
|
if partial not in file_sources:
|
|
if self.verbose > 1:
|
|
print '%s%s (not from template)' % (pad, name)
|
|
continue
|
|
templates = file_sources.pop(partial)
|
|
print '%s%s from:' % (pad, name)
|
|
for template in templates:
|
|
print '%s %s' % (pad, template.name)
|
|
for dir in dirs:
|
|
if dir in self._ignore_dirs:
|
|
continue
|
|
print '%sRecursing into %s/' % (pad, dir)
|
|
self._show_files(
|
|
output_dir, file_sources,
|
|
join=os.path.join(join, dir),
|
|
indent=indent+1)
|
|
|
|
def _show_leftovers(self, output_dir, file_sources):
|
|
if not file_sources:
|
|
return
|
|
print
|
|
print 'These files were supposed to be generated by templates'
|
|
print 'but were not found:'
|
|
file_sources = file_sources.items()
|
|
file_sources.sort()
|
|
for partial, templates in file_sources:
|
|
print ' %s from:' % partial
|
|
for template in templates:
|
|
print ' %s' % template.name
|
|
|
|
def list_variables(self, templates):
|
|
for tmpl_name, tmpl in templates:
|
|
if not tmpl.read_vars():
|
|
if self.verbose > 1:
|
|
self._show_template_vars(
|
|
tmpl_name, tmpl, 'No variables found')
|
|
continue
|
|
self._show_template_vars(tmpl_name, tmpl)
|
|
|
|
def _show_template_vars(self, tmpl_name, tmpl, message=None):
|
|
title = '%s (from %s)' % (tmpl.name, tmpl_name)
|
|
print title
|
|
print '-'*len(title)
|
|
if message is not None:
|
|
print ' %s' % message
|
|
print
|
|
return
|
|
tmpl.print_vars(indent=2)
|