0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003import re
0004import sys
0005import os
0006import pkg_resources
0007from command import Command, BadCommand
0008import copydir
0009import pluginlib
0010import fnmatch
0011
0012class CreateDistroCommand(Command):
0013
0014    usage = 'PACKAGE_NAME [VAR=VALUE VAR2=VALUE2 ...]'
0015    summary = "Create the file layout for a Python distribution"
0016    short_description = summary
0017
0018    description = """\
0019    Create a new project.  Projects are typically Python packages,
0020    ready for distribution.  Projects are created from templates, and
0021    represent different kinds of projects -- associated with a
0022    particular framework for instance.
0023    """
0024
0025    parser = Command.standard_parser(
0026        simulate=True, no_interactive=True, quiet=True, overwrite=True)
0027    parser.add_option('-t', '--template',
0028                      dest='templates',
0029                      metavar='TEMPLATE',
0030                      action='append',
0031                      help="Add a template to the create process")
0032    parser.add_option('-o', '--output-dir',
0033                      dest='output_dir',
0034                      metavar='DIR',
0035                      default='.',
0036                      help="Write put the directory into DIR (default current directory)")
0037    parser.add_option('--svn-repository',
0038                      dest='svn_repository',
0039                      metavar='REPOS',
0040                      help="Create package at given repository location (this will create the standard trunk/ tags/ branches/ hierarchy)")
0041    parser.add_option('--list-templates',
0042                      dest='list_templates',
0043                      action='store_true',
0044                      help="List all templates available")
0045    parser.add_option('--list-variables',
0046                      dest="list_variables",
0047                      action="store_true",
0048                      help="List all variables expected by the given template (does not create a package)")
0049    parser.add_option('--inspect-files',
0050                      dest='inspect_files',
0051                      action='store_true',
0052                      help="Show where the files in the given (already created) directory came from (useful when using multiple templates)")
0053    parser.add_option('--config',
0054                      action='store',
0055                      dest='config',
0056                      help="Template variables file")
0057
0058    _bad_chars_re = re.compile('[^a-zA-Z0-9_]')
0059
0060    default_verbosity = 1
0061    default_interactive = 1
0062
0063    def command(self):
0064        if self.options.list_templates:
0065            return self.list_templates()
0066        asked_tmpls = self.options.templates or ['basic_package']
0067        templates = []
0068        for tmpl_name in asked_tmpls:
0069            self.extend_templates(templates, tmpl_name)
0070        if self.options.list_variables:
0071            return self.list_variables(templates)
0072        if self.verbose:
0073            print 'Selected and implied templates:'
0074            max_tmpl_name = max([len(tmpl_name) for tmpl_name, tmpl in templates])
0075            for tmpl_name, tmpl in templates:
0076                print '  %s%s  %s' % (
0077                    tmpl_name, ' '*(max_tmpl_name-len(tmpl_name)),
0078                    tmpl.summary)
0079            print
0080        if not self.args:
0081            if self.interactive:
0082                dist_name = self.challenge('Enter project name')
0083            else:
0084                raise BadCommand('You must provide a PACKAGE_NAME')
0085        else:
0086            dist_name = self.args[0].lstrip(os.path.sep)
0087
0088        templates = [tmpl for name, tmpl in templates]
0089        output_dir = os.path.join(self.options.output_dir, dist_name)
0090
0091        pkg_name = self._bad_chars_re.sub('', dist_name.lower())
0092        vars = {'project': dist_name,
0093                'package': pkg_name,
0094                'egg': pluginlib.egg_name(dist_name),
0095                }
0096        vars.update(self.parse_vars(self.args[1:]))
0097        if self.options.config and os.path.exists(self.options.config):
0098            for key, value in self.read_vars(self.options.config).items():
0099                vars.setdefault(key, value)
0100
0101        if self.verbose: # @@: > 1?
0102            self.display_vars(vars)
0103
0104        if self.options.inspect_files:
0105            self.inspect_files(
0106                output_dir, templates, vars)
0107            return
0108        if not os.path.exists(output_dir):
0109            # We want to avoid asking questions in copydir if the path
0110            # doesn't exist yet
0111            copydir.all_answer = 'y'
0112
0113        if self.options.svn_repository:
0114            self.setup_svn_repository(output_dir, dist_name)
0115
0116        # First we want to make sure all the templates get a chance to
0117        # set their variables, all at once, with the most specialized
0118        # template going first (the last template is the most
0119        # specialized)...
0120        for template in templates[::-1]:
0121            vars = template.check_vars(vars, self)
0122
0123        for template in templates:
0124            self.create_template(
0125                template, output_dir, vars)
0126
0127        found_setup_py = False
0128        if os.path.exists(os.path.join(output_dir, 'setup.py')):
0129            self.run_command(sys.executable, 'setup.py', 'egg_info',
0130                             cwd=output_dir,
0131                             # 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:
0132                             expect_returncode=True)
0133            found_setup_py = True
0134        elif self.verbose > 1:
0135            print 'No setup.py (cannot run egg_info)'
0136
0137        package_dir = vars.get('package_dir', None)
0138        if package_dir:
0139            output_dir = os.path.join(output_dir, package_dir)
0140
0141        # With no setup.py this doesn't make sense:
0142        if found_setup_py:
0143            egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name)
0144            for template in templates:
0145                for spec in template.egg_plugins:
0146                    if self.verbose:
0147                        print 'Adding %s to paster_plugins.txt' % spec
0148                    if not self.simulate:
0149                        pluginlib.add_plugin(egg_info_dir, spec)
0150            if not self.simulate:
0151                # We'll include this by default, but you can remove
0152                # it later if you want:
0153                pluginlib.add_plugin(egg_info_dir, 'PasteScript')
0154
0155        if self.options.svn_repository:
0156            self.add_svn_repository(vars, output_dir)
0157
0158        if self.options.config:
0159            write_vars = vars.copy()
0160            del write_vars['project']
0161            del write_vars['package']
0162            self.write_vars(self.options.config, write_vars)
0163
0164    def create_template(self, template, output_dir, vars):
0165        if self.verbose:
0166            print 'Creating template %s' % template.name
0167        template.run(self, output_dir, vars)
0168
0169    def setup_svn_repository(self, output_dir, dist_name):
0170        # @@: Use subprocess
0171        svn_repos = self.options.svn_repository
0172        svn_repos_path = os.path.join(svn_repos, dist_name).replace('\\','/')
0173        svn_command = 'svn'
0174        if sys.platform == 'win32':
0175            svn_command += '.exe'
0176        # @@: The previous method of formatting this string using \ doesn't work on Windows
0177        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"'
0180        cmd = cmd % {
0181            'svn_repos_path': svn_repos_path,
0182            'dist_name': dist_name,
0183            'svn_command':svn_command,
0184        }
0185        if self.verbose:
0186            print "Running:"
0187            print cmd
0188        if not self.simulate:
0189            os.system(cmd)
0190        svn_repos_path_trunk = os.path.join(svn_repos_path,'trunk').replace('\\','/')
0191        cmd = svn_command+' co "%s" "%s"' % (svn_repos_path_trunk, output_dir)
0192        if self.verbose:
0193            print "Running %s" % cmd
0194        if not self.simulate:
0195            os.system(cmd)
0196
0197    ignore_egg_info_files = [
0198        'top_level.txt',
0199        'entry_points.txt',
0200        'requires.txt',
0201        'PKG-INFO',
0202        'namespace_packages.txt',
0203        'SOURCES.txt',
0204        'dependency_links.txt',
0205        'not-zip-safe']
0206
0207    def add_svn_repository(self, vars, output_dir):
0208        svn_repos = self.options.svn_repository
0209        egg_info_dir = pluginlib.egg_info_dir(output_dir, vars['project'])
0210        svn_command = 'svn'
0211        if sys.platform == 'win32':
0212            svn_command += '.exe'
0213        self.run_command(svn_command, 'add', '-N', egg_info_dir)
0214        paster_plugins_file = os.path.join(
0215            egg_info_dir, 'paster_plugins.txt')
0216        if os.path.exists(paster_plugins_file):
0217            self.run_command(svn_command, 'add', paster_plugins_file)
0218        self.run_command(svn_command, 'ps', 'svn:ignore',
0219                         '\n'.join(self.ignore_egg_info_files),
0220                         egg_info_dir)
0221        if self.verbose:
0222            print ("You must next run 'svn commit' to commit the "
0223                   "files to repository")
0224
0225    def extend_templates(self, templates, tmpl_name):
0226        if '#' in tmpl_name:
0227            dist_name, tmpl_name = tmpl_name.split('#', 1)
0228        else:
0229            dist_name, tmpl_name = None, tmpl_name
0230        if dist_name is None:
0231            for entry in self.all_entry_points():
0232                if entry.name == tmpl_name:
0233                    tmpl = entry.load()(entry.name)
0234                    dist_name = entry.dist.project_name
0235                    break
0236            else:
0237                raise LookupError(
0238                    'Template by name %r not found' % tmpl_name)
0239        else:
0240            dist = pkg_resources.get_distribution(dist_name)
0241            entry = dist.get_entry_info(
0242                'paste.paster_create_template', tmpl_name)
0243            tmpl = entry.load()(entry.name)
0244        full_name = '%s#%s' % (dist_name, tmpl_name)
0245        for item_full_name, item_tmpl in templates:
0246            if item_full_name == full_name:
0247                # Already loaded
0248                return
0249        for req_name in tmpl.required_templates:
0250            self.extend_templates(templates, req_name)
0251        templates.append((full_name, tmpl))
0252
0253    def all_entry_points(self):
0254        if not hasattr(self, '_entry_points'):
0255            self._entry_points = list(pkg_resources.iter_entry_points(
0256            'paste.paster_create_template'))
0257        return self._entry_points
0258
0259    def display_vars(self, vars):
0260        vars = vars.items()
0261        vars.sort()
0262        print 'Variables:'
0263        max_var = max([len(n) for n, v in vars])
0264        for name, value in vars:
0265            print '  %s:%s  %s' % (
0266                name, ' '*(max_var-len(name)), value)
0267
0268    def list_templates(self):
0269        templates = []
0270        for entry in self.all_entry_points():
0271            try:
0272                templates.append(entry.load()(entry.name))
0273            except Exception, e:
0274                # We will not be stopped!
0275                print 'Warning: could not load entry point %s (%s: %s)' % (
0276                    entry.name, e.__class__.__name__, e)
0277        max_name = max([len(t.name) for t in templates])
0278        templates.sort(lambda a, b: cmp(a.name, b.name))
0279        print 'Available templates:'
0280        for template in templates:
0281            # @@: Wrap description
0282            print '  %s:%s  %s' % (
0283                template.name,
0284                ' '*(max_name-len(template.name)),
0285                template.summary)
0286
0287    def inspect_files(self, output_dir, templates, vars):
0288        file_sources = {}
0289        for template in templates:
0290            self._find_files(template, vars, file_sources)
0291        self._show_files(output_dir, file_sources)
0292        self._show_leftovers(output_dir, file_sources)
0293
0294    def _find_files(self, template, vars, file_sources):
0295        tmpl_dir = template.template_dir()
0296        self._find_template_files(
0297            template, tmpl_dir, vars, file_sources)
0298
0299    def _find_template_files(self, template, tmpl_dir, vars,
0300                             file_sources, join=''):
0301        full_dir = os.path.join(tmpl_dir, join)
0302        for name in os.listdir(full_dir):
0303            if name.startswith('.'):
0304                continue
0305            if os.path.isdir(os.path.join(full_dir, name)):
0306                self._find_template_files(
0307                    template, tmpl_dir, vars, file_sources,
0308                    join=os.path.join(join, name))
0309                continue
0310            partial = os.path.join(join, name)
0311            for name, value in vars.items():
0312                partial = partial.replace('+%s+' % name, value)
0313            if partial.endswith('_tmpl'):
0314                partial = partial[:-5]
0315            file_sources.setdefault(partial, []).append(template)
0316
0317    _ignore_filenames = ['.*', '*.pyc', '*.bak*']
0318    _ignore_dirs = ['CVS', '_darcs', '.svn']
0319
0320    def _show_files(self, output_dir, file_sources, join='', indent=0):
0321        pad = ' '*(2*indent)
0322        full_dir = os.path.join(output_dir, join)
0323        names = os.listdir(full_dir)
0324        dirs = [n for n in names
0325                if os.path.isdir(os.path.join(full_dir, n))]
0326        fns = [n for n in names
0327               if not os.path.isdir(os.path.join(full_dir, n))]
0328        dirs.sort()
0329        names.sort()
0330        for name in names:
0331            skip_this = False
0332            for ext in self._ignore_filenames:
0333                if fnmatch.fnmatch(name, ext):
0334                    if self.verbose > 1:
0335                        print '%sIgnoring %s' % (pad, name)
0336                    skip_this = True
0337                    break
0338            if skip_this:
0339                continue
0340            partial = os.path.join(join, name)
0341            if partial not in file_sources:
0342                if self.verbose > 1:
0343                    print '%s%s (not from template)' % (pad, name)
0344                continue
0345            templates = file_sources.pop(partial)
0346            print '%s%s from:' % (pad, name)
0347            for template in templates:
0348                print '%s  %s' % (pad, template.name)
0349        for dir in dirs:
0350            if dir in self._ignore_dirs:
0351                continue
0352            print '%sRecursing into %s/' % (pad, dir)
0353            self._show_files(
0354                output_dir, file_sources,
0355                join=os.path.join(join, dir),
0356                indent=indent+1)
0357
0358    def _show_leftovers(self, output_dir, file_sources):
0359        if not file_sources:
0360            return
0361        print
0362        print 'These files were supposed to be generated by templates'
0363        print 'but were not found:'
0364        file_sources = file_sources.items()
0365        file_sources.sort()
0366        for partial, templates in file_sources:
0367            print '  %s from:' % partial
0368            for template in templates:
0369                print '    %s' % template.name
0370
0371    def list_variables(self, templates):
0372        for tmpl_name, tmpl in templates:
0373            if not tmpl.read_vars():
0374                if self.verbose > 1:
0375                    self._show_template_vars(
0376                        tmpl_name, tmpl, 'No variables found')
0377                continue
0378            self._show_template_vars(tmpl_name, tmpl)
0379
0380    def _show_template_vars(self, tmpl_name, tmpl, message=None):
0381        title = '%s (from %s)' % (tmpl.name, tmpl_name)
0382        print title
0383        print '-'*len(title)
0384        if message is not None:
0385            print '  %s' % message
0386            print
0387            return
0388        tmpl.print_vars(indent=2)