mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	The example version numbers were invalid and “package” was misused. I also made lines shorter, replaced “e-mail” with “email” (more common in the stdlib and I believe in English generally) and tweaked a few other things.
		
			
				
	
	
		
			689 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			689 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Interactive helper used to create a setup.cfg file.
 | 
						|
 | 
						|
This script will generate a packaging configuration file by looking at
 | 
						|
the current directory and asking the user questions.  It is intended to
 | 
						|
be called as *pysetup create*.
 | 
						|
"""
 | 
						|
 | 
						|
#  Original code by Sean Reifschneider <jafo@tummy.com>
 | 
						|
 | 
						|
#  Original TODO list:
 | 
						|
#  Look for a license file and automatically add the category.
 | 
						|
#  When a .c file is found during the walk, can we add it as an extension?
 | 
						|
#  Ask if there is a maintainer different that the author
 | 
						|
#  Ask for the platform (can we detect this via "import win32" or something?)
 | 
						|
#  Ask for the dependencies.
 | 
						|
#  Ask for the Requires-Dist
 | 
						|
#  Ask for the Provides-Dist
 | 
						|
#  Ask for a description
 | 
						|
#  Detect scripts (not sure how.  #! outside of package?)
 | 
						|
 | 
						|
import os
 | 
						|
import re
 | 
						|
import imp
 | 
						|
import sys
 | 
						|
import glob
 | 
						|
import shutil
 | 
						|
import sysconfig
 | 
						|
import tokenize
 | 
						|
from hashlib import md5
 | 
						|
from textwrap import dedent
 | 
						|
from functools import cmp_to_key
 | 
						|
from configparser import RawConfigParser
 | 
						|
# importing this with an underscore as it should be replaced by the
 | 
						|
# dict form or another structures for all purposes
 | 
						|
from packaging._trove import all_classifiers as _CLASSIFIERS_LIST
 | 
						|
from packaging.version import is_valid_version
 | 
						|
 | 
						|
_FILENAME = 'setup.cfg'
 | 
						|
_DEFAULT_CFG = '.pypkgcreate'
 | 
						|
 | 
						|
_helptext = {
 | 
						|
    'name': '''
 | 
						|
The name of the project to be packaged, usually a single word composed
 | 
						|
of lower-case characters such as "zope.interface", "sqlalchemy" or
 | 
						|
"CherryPy".
 | 
						|
''',
 | 
						|
    'version': '''
 | 
						|
Version number of the software, typically 2 or 3 numbers separated by
 | 
						|
dots such as "1.0", "0.6b3", or "3.2.1".  "0.1.0" is recommended for
 | 
						|
initial development.
 | 
						|
''',
 | 
						|
    'summary': '''
 | 
						|
A one-line summary of what this project is or does, typically a sentence
 | 
						|
80 characters or less in length.
 | 
						|
''',
 | 
						|
    'author': '''
 | 
						|
The full name of the author (typically you).
 | 
						|
''',
 | 
						|
    'author_email': '''
 | 
						|
Email address of the project author.
 | 
						|
''',
 | 
						|
    'do_classifier': '''
 | 
						|
Trove classifiers are optional identifiers that allow you to specify the
 | 
						|
intended audience by saying things like "Beta software with a text UI
 | 
						|
for Linux under the PSF license".  However, this can be a somewhat
 | 
						|
involved process.
 | 
						|
''',
 | 
						|
    'packages': '''
 | 
						|
Python packages included in the project.
 | 
						|
''',
 | 
						|
    'modules': '''
 | 
						|
Pure Python modules included in the project.
 | 
						|
''',
 | 
						|
    'extra_files': '''
 | 
						|
You can provide extra files/dirs contained in your project.
 | 
						|
It has to follow the template syntax. XXX add help here.
 | 
						|
''',
 | 
						|
 | 
						|
    'home_page': '''
 | 
						|
The home page for the project, typically a public Web page.
 | 
						|
''',
 | 
						|
    'trove_license': '''
 | 
						|
Optionally you can specify a license.  Type a string that identifies a
 | 
						|
common license, and then you can select a list of license specifiers.
 | 
						|
''',
 | 
						|
    'trove_generic': '''
 | 
						|
Optionally, you can set other trove identifiers for things such as the
 | 
						|
human language, programming language, user interface, etc.
 | 
						|
''',
 | 
						|
    'setup.py found': '''
 | 
						|
The setup.py script will be executed to retrieve the metadata.
 | 
						|
An interactive helper will be run if you answer "n",
 | 
						|
''',
 | 
						|
}
 | 
						|
 | 
						|
PROJECT_MATURITY = ['Development Status :: 1 - Planning',
 | 
						|
                    'Development Status :: 2 - Pre-Alpha',
 | 
						|
                    'Development Status :: 3 - Alpha',
 | 
						|
                    'Development Status :: 4 - Beta',
 | 
						|
                    'Development Status :: 5 - Production/Stable',
 | 
						|
                    'Development Status :: 6 - Mature',
 | 
						|
                    'Development Status :: 7 - Inactive']
 | 
						|
 | 
						|
# XXX everything needs docstrings and tests (both low-level tests of various
 | 
						|
# methods and functional tests of running the script)
 | 
						|
 | 
						|
 | 
						|
def load_setup():
 | 
						|
    """run the setup script (i.e the setup.py file)
 | 
						|
 | 
						|
    This function load the setup file in all cases (even if it have already
 | 
						|
    been loaded before, because we are monkey patching its setup function with
 | 
						|
    a particular one"""
 | 
						|
    with open("setup.py", "rb") as f:
 | 
						|
        encoding, lines = tokenize.detect_encoding(f.readline)
 | 
						|
    with open("setup.py", encoding=encoding) as f:
 | 
						|
        imp.load_module("setup", f, "setup.py", (".py", "r", imp.PY_SOURCE))
 | 
						|
 | 
						|
 | 
						|
def ask_yn(question, default=None, helptext=None):
 | 
						|
    question += ' (y/n)'
 | 
						|
    while True:
 | 
						|
        answer = ask(question, default, helptext, required=True)
 | 
						|
        if answer and answer[0].lower() in 'yn':
 | 
						|
            return answer[0].lower()
 | 
						|
 | 
						|
        print('\nERROR: You must select "Y" or "N".\n')
 | 
						|
 | 
						|
 | 
						|
def ask(question, default=None, helptext=None, required=True,
 | 
						|
        lengthy=False, multiline=False):
 | 
						|
    prompt = '%s: ' % (question,)
 | 
						|
    if default:
 | 
						|
        prompt = '%s [%s]: ' % (question, default)
 | 
						|
        if default and len(question) + len(default) > 70:
 | 
						|
            prompt = '%s\n    [%s]: ' % (question, default)
 | 
						|
    if lengthy or multiline:
 | 
						|
        prompt += '\n   > '
 | 
						|
 | 
						|
    if not helptext:
 | 
						|
        helptext = 'No additional help available.'
 | 
						|
 | 
						|
    helptext = helptext.strip("\n")
 | 
						|
 | 
						|
    while True:
 | 
						|
        sys.stdout.write(prompt)
 | 
						|
        sys.stdout.flush()
 | 
						|
 | 
						|
        line = sys.stdin.readline().strip()
 | 
						|
        if line == '?':
 | 
						|
            print('=' * 70)
 | 
						|
            print(helptext)
 | 
						|
            print('=' * 70)
 | 
						|
            continue
 | 
						|
        if default and not line:
 | 
						|
            return default
 | 
						|
        if not line and required:
 | 
						|
            print('*' * 70)
 | 
						|
            print('This value cannot be empty.')
 | 
						|
            print('===========================')
 | 
						|
            if helptext:
 | 
						|
                print(helptext)
 | 
						|
            print('*' * 70)
 | 
						|
            continue
 | 
						|
        return line
 | 
						|
 | 
						|
 | 
						|
def convert_yn_to_bool(yn, yes=True, no=False):
 | 
						|
    """Convert a y/yes or n/no to a boolean value."""
 | 
						|
    if yn.lower().startswith('y'):
 | 
						|
        return yes
 | 
						|
    else:
 | 
						|
        return no
 | 
						|
 | 
						|
 | 
						|
def _build_classifiers_dict(classifiers):
 | 
						|
    d = {}
 | 
						|
    for key in classifiers:
 | 
						|
        subdict = d
 | 
						|
        for subkey in key.split(' :: '):
 | 
						|
            if subkey not in subdict:
 | 
						|
                subdict[subkey] = {}
 | 
						|
            subdict = subdict[subkey]
 | 
						|
    return d
 | 
						|
 | 
						|
CLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST)
 | 
						|
 | 
						|
 | 
						|
def _build_licences(classifiers):
 | 
						|
    res = []
 | 
						|
    for index, item in enumerate(classifiers):
 | 
						|
        if not item.startswith('License :: '):
 | 
						|
            continue
 | 
						|
        res.append((index, item.split(' :: ')[-1].lower()))
 | 
						|
    return res
 | 
						|
 | 
						|
LICENCES = _build_licences(_CLASSIFIERS_LIST)
 | 
						|
 | 
						|
 | 
						|
class MainProgram:
 | 
						|
    """Make a project setup configuration file (setup.cfg)."""
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        self.configparser = None
 | 
						|
        self.classifiers = set()
 | 
						|
        self.data = {'name': '',
 | 
						|
                     'version': '1.0.0',
 | 
						|
                     'classifier': self.classifiers,
 | 
						|
                     'packages': [],
 | 
						|
                     'modules': [],
 | 
						|
                     'platform': [],
 | 
						|
                     'resources': [],
 | 
						|
                     'extra_files': [],
 | 
						|
                     'scripts': [],
 | 
						|
                     }
 | 
						|
        self._load_defaults()
 | 
						|
 | 
						|
    def __call__(self):
 | 
						|
        setupcfg_defined = False
 | 
						|
        if self.has_setup_py() and self._prompt_user_for_conversion():
 | 
						|
            setupcfg_defined = self.convert_py_to_cfg()
 | 
						|
        if not setupcfg_defined:
 | 
						|
            self.define_cfg_values()
 | 
						|
        self._write_cfg()
 | 
						|
 | 
						|
    def has_setup_py(self):
 | 
						|
        """Test for the existence of a setup.py file."""
 | 
						|
        return os.path.exists('setup.py')
 | 
						|
 | 
						|
    def define_cfg_values(self):
 | 
						|
        self.inspect()
 | 
						|
        self.query_user()
 | 
						|
 | 
						|
    def _lookup_option(self, key):
 | 
						|
        if not self.configparser.has_option('DEFAULT', key):
 | 
						|
            return None
 | 
						|
        return self.configparser.get('DEFAULT', key)
 | 
						|
 | 
						|
    def _load_defaults(self):
 | 
						|
        # Load default values from a user configuration file
 | 
						|
        self.configparser = RawConfigParser()
 | 
						|
        # TODO replace with section in distutils config file
 | 
						|
        default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG))
 | 
						|
        self.configparser.read(default_cfg)
 | 
						|
        self.data['author'] = self._lookup_option('author')
 | 
						|
        self.data['author_email'] = self._lookup_option('author_email')
 | 
						|
 | 
						|
    def _prompt_user_for_conversion(self):
 | 
						|
        # Prompt the user about whether they would like to use the setup.py
 | 
						|
        # conversion utility to generate a setup.cfg or generate the setup.cfg
 | 
						|
        # from scratch
 | 
						|
        answer = ask_yn(('A legacy setup.py has been found.\n'
 | 
						|
                         'Would you like to convert it to a setup.cfg?'),
 | 
						|
                        default="y",
 | 
						|
                        helptext=_helptext['setup.py found'])
 | 
						|
        return convert_yn_to_bool(answer)
 | 
						|
 | 
						|
    def _dotted_packages(self, data):
 | 
						|
        packages = sorted(data)
 | 
						|
        modified_pkgs = []
 | 
						|
        for pkg in packages:
 | 
						|
            pkg = pkg.lstrip('./')
 | 
						|
            pkg = pkg.replace('/', '.')
 | 
						|
            modified_pkgs.append(pkg)
 | 
						|
        return modified_pkgs
 | 
						|
 | 
						|
    def _write_cfg(self):
 | 
						|
        if os.path.exists(_FILENAME):
 | 
						|
            if os.path.exists('%s.old' % _FILENAME):
 | 
						|
                print("ERROR: %(name)s.old backup exists, please check that "
 | 
						|
                      "current %(name)s is correct and remove %(name)s.old" %
 | 
						|
                      {'name': _FILENAME})
 | 
						|
                return
 | 
						|
            shutil.move(_FILENAME, '%s.old' % _FILENAME)
 | 
						|
 | 
						|
        with open(_FILENAME, 'w', encoding='utf-8') as fp:
 | 
						|
            fp.write('[metadata]\n')
 | 
						|
            # TODO use metadata module instead of hard-coding field-specific
 | 
						|
            # behavior here
 | 
						|
 | 
						|
            # simple string entries
 | 
						|
            for name in ('name', 'version', 'summary', 'download_url'):
 | 
						|
                fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN')))
 | 
						|
 | 
						|
            # optional string entries
 | 
						|
            if 'keywords' in self.data and self.data['keywords']:
 | 
						|
                fp.write('keywords = %s\n' % ' '.join(self.data['keywords']))
 | 
						|
            for name in ('home_page', 'author', 'author_email',
 | 
						|
                         'maintainer', 'maintainer_email', 'description-file'):
 | 
						|
                if name in self.data and self.data[name]:
 | 
						|
                    fp.write('%s = %s\n' % (name, self.data[name]))
 | 
						|
            if 'description' in self.data:
 | 
						|
                fp.write(
 | 
						|
                    'description = %s\n'
 | 
						|
                    % '\n       |'.join(self.data['description'].split('\n')))
 | 
						|
 | 
						|
            # multiple use string entries
 | 
						|
            for name in ('platform', 'supported-platform', 'classifier',
 | 
						|
                         'requires-dist', 'provides-dist', 'obsoletes-dist',
 | 
						|
                         'requires-external'):
 | 
						|
                if not(name in self.data and self.data[name]):
 | 
						|
                    continue
 | 
						|
                fp.write('%s = ' % name)
 | 
						|
                fp.write(''.join('    %s\n' % val
 | 
						|
                                 for val in self.data[name]).lstrip())
 | 
						|
            fp.write('\n[files]\n')
 | 
						|
            for name in ('packages', 'modules', 'scripts',
 | 
						|
                         'package_data', 'extra_files'):
 | 
						|
                if not(name in self.data and self.data[name]):
 | 
						|
                    continue
 | 
						|
                fp.write('%s = %s\n'
 | 
						|
                         % (name, '\n    '.join(self.data[name]).strip()))
 | 
						|
            fp.write('\nresources =\n')
 | 
						|
            for src, dest in self.data['resources']:
 | 
						|
                fp.write('    %s = %s\n' % (src, dest))
 | 
						|
            fp.write('\n')
 | 
						|
 | 
						|
        os.chmod(_FILENAME, 0o644)
 | 
						|
        print('Wrote "%s".' % _FILENAME)
 | 
						|
 | 
						|
    def convert_py_to_cfg(self):
 | 
						|
        """Generate a setup.cfg from an existing setup.py.
 | 
						|
 | 
						|
        It only exports the distutils metadata (setuptools specific metadata
 | 
						|
        is not currently supported).
 | 
						|
        """
 | 
						|
        data = self.data
 | 
						|
 | 
						|
        def setup_mock(**attrs):
 | 
						|
            """Mock the setup(**attrs) in order to retrieve metadata."""
 | 
						|
 | 
						|
            # TODO use config and metadata instead of Distribution
 | 
						|
            from distutils.dist import Distribution
 | 
						|
            dist = Distribution(attrs)
 | 
						|
            dist.parse_config_files()
 | 
						|
 | 
						|
            # 1. retrieve metadata fields that are quite similar in
 | 
						|
            # PEP 314 and PEP 345
 | 
						|
            labels = (('name',) * 2,
 | 
						|
                      ('version',) * 2,
 | 
						|
                      ('author',) * 2,
 | 
						|
                      ('author_email',) * 2,
 | 
						|
                      ('maintainer',) * 2,
 | 
						|
                      ('maintainer_email',) * 2,
 | 
						|
                      ('description', 'summary'),
 | 
						|
                      ('long_description', 'description'),
 | 
						|
                      ('url', 'home_page'),
 | 
						|
                      ('platforms', 'platform'),
 | 
						|
                      # backport only for 2.5+
 | 
						|
                      ('provides', 'provides-dist'),
 | 
						|
                      ('obsoletes', 'obsoletes-dist'),
 | 
						|
                      ('requires', 'requires-dist'))
 | 
						|
 | 
						|
            get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
 | 
						|
            data.update((new, get(old)) for old, new in labels if get(old))
 | 
						|
 | 
						|
            # 2. retrieve data that requires special processing
 | 
						|
            data['classifier'].update(dist.get_classifiers() or [])
 | 
						|
            data['scripts'].extend(dist.scripts or [])
 | 
						|
            data['packages'].extend(dist.packages or [])
 | 
						|
            data['modules'].extend(dist.py_modules or [])
 | 
						|
            # 2.1 data_files -> resources
 | 
						|
            if dist.data_files:
 | 
						|
                if (len(dist.data_files) < 2 or
 | 
						|
                    isinstance(dist.data_files[1], str)):
 | 
						|
                    dist.data_files = [('', dist.data_files)]
 | 
						|
                # add tokens in the destination paths
 | 
						|
                vars = {'distribution.name': data['name']}
 | 
						|
                path_tokens = list(sysconfig.get_paths(vars=vars).items())
 | 
						|
 | 
						|
                # TODO replace this with a key function
 | 
						|
                def length_comparison(x, y):
 | 
						|
                    len_x = len(x[1])
 | 
						|
                    len_y = len(y[1])
 | 
						|
                    if len_x == len_y:
 | 
						|
                        return 0
 | 
						|
                    elif len_x < len_y:
 | 
						|
                        return -1
 | 
						|
                    else:
 | 
						|
                        return 1
 | 
						|
 | 
						|
                # sort tokens to use the longest one first
 | 
						|
                path_tokens.sort(key=cmp_to_key(length_comparison))
 | 
						|
                for dest, srcs in (dist.data_files or []):
 | 
						|
                    dest = os.path.join(sys.prefix, dest)
 | 
						|
                    dest = dest.replace(os.path.sep, '/')
 | 
						|
                    for tok, path in path_tokens:
 | 
						|
                        path = path.replace(os.path.sep, '/')
 | 
						|
                        if not dest.startswith(path):
 | 
						|
                            continue
 | 
						|
 | 
						|
                        dest = ('{%s}' % tok) + dest[len(path):]
 | 
						|
                        files = [('/ '.join(src.rsplit('/', 1)), dest)
 | 
						|
                                 for src in srcs]
 | 
						|
                        data['resources'].extend(files)
 | 
						|
 | 
						|
            # 2.2 package_data -> extra_files
 | 
						|
            package_dirs = dist.package_dir or {}
 | 
						|
            for package, extras in dist.package_data.items() or []:
 | 
						|
                package_dir = package_dirs.get(package, package)
 | 
						|
                for file_ in extras:
 | 
						|
                    if package_dir:
 | 
						|
                        file_ = package_dir + '/' + file_
 | 
						|
                    data['extra_files'].append(file_)
 | 
						|
 | 
						|
            # Use README file if its content is the desciption
 | 
						|
            if "description" in data:
 | 
						|
                ref = md5(re.sub('\s', '',
 | 
						|
                                 self.data['description']).lower().encode())
 | 
						|
                ref = ref.digest()
 | 
						|
                for readme in glob.glob('README*'):
 | 
						|
                    with open(readme, encoding='utf-8') as fp:
 | 
						|
                        contents = fp.read()
 | 
						|
                    contents = re.sub('\s', '', contents.lower()).encode()
 | 
						|
                    val = md5(contents).digest()
 | 
						|
                    if val == ref:
 | 
						|
                        del data['description']
 | 
						|
                        data['description-file'] = readme
 | 
						|
                        break
 | 
						|
 | 
						|
        # apply monkey patch to distutils (v1) and setuptools (if needed)
 | 
						|
        # (abort the feature if distutils v1 has been killed)
 | 
						|
        try:
 | 
						|
            from distutils import core
 | 
						|
            core.setup  # make sure it's not d2 maskerading as d1
 | 
						|
        except (ImportError, AttributeError):
 | 
						|
            return
 | 
						|
        saved_setups = [(core, core.setup)]
 | 
						|
        core.setup = setup_mock
 | 
						|
        try:
 | 
						|
            import setuptools
 | 
						|
        except ImportError:
 | 
						|
            pass
 | 
						|
        else:
 | 
						|
            saved_setups.append((setuptools, setuptools.setup))
 | 
						|
            setuptools.setup = setup_mock
 | 
						|
        # get metadata by executing the setup.py with the patched setup(...)
 | 
						|
        success = False  # for python < 2.4
 | 
						|
        try:
 | 
						|
            load_setup()
 | 
						|
            success = True
 | 
						|
        finally:  # revert monkey patches
 | 
						|
            for patched_module, original_setup in saved_setups:
 | 
						|
                patched_module.setup = original_setup
 | 
						|
        if not self.data:
 | 
						|
            raise ValueError('Unable to load metadata from setup.py')
 | 
						|
        return success
 | 
						|
 | 
						|
    def inspect(self):
 | 
						|
        """Inspect the current working diretory for a name and version.
 | 
						|
 | 
						|
        This information is harvested in where the directory is named
 | 
						|
        like [name]-[version].
 | 
						|
        """
 | 
						|
        dir_name = os.path.basename(os.getcwd())
 | 
						|
        self.data['name'] = dir_name
 | 
						|
        match = re.match(r'(.*)-(\d.+)', dir_name)
 | 
						|
        if match:
 | 
						|
            self.data['name'] = match.group(1)
 | 
						|
            self.data['version'] = match.group(2)
 | 
						|
            # TODO needs testing!
 | 
						|
            if not is_valid_version(self.data['version']):
 | 
						|
                msg = "Invalid version discovered: %s" % self.data['version']
 | 
						|
                raise ValueError(msg)
 | 
						|
 | 
						|
    def query_user(self):
 | 
						|
        self.data['name'] = ask('Project name', self.data['name'],
 | 
						|
              _helptext['name'])
 | 
						|
 | 
						|
        self.data['version'] = ask('Current version number',
 | 
						|
              self.data.get('version'), _helptext['version'])
 | 
						|
        self.data['summary'] = ask('Project description summary',
 | 
						|
              self.data.get('summary'), _helptext['summary'],
 | 
						|
              lengthy=True)
 | 
						|
        self.data['author'] = ask('Author name',
 | 
						|
              self.data.get('author'), _helptext['author'])
 | 
						|
        self.data['author_email'] = ask('Author email address',
 | 
						|
              self.data.get('author_email'), _helptext['author_email'])
 | 
						|
        self.data['home_page'] = ask('Project home page',
 | 
						|
              self.data.get('home_page'), _helptext['home_page'],
 | 
						|
              required=False)
 | 
						|
 | 
						|
        if ask_yn('Do you want me to automatically build the file list '
 | 
						|
              'with everything I can find in the current directory? '
 | 
						|
              'If you say no, you will have to define them manually.') == 'y':
 | 
						|
            self._find_files()
 | 
						|
        else:
 | 
						|
            while ask_yn('Do you want to add a single module?'
 | 
						|
                        ' (you will be able to add full packages next)',
 | 
						|
                    helptext=_helptext['modules']) == 'y':
 | 
						|
                self._set_multi('Module name', 'modules')
 | 
						|
 | 
						|
            while ask_yn('Do you want to add a package?',
 | 
						|
                    helptext=_helptext['packages']) == 'y':
 | 
						|
                self._set_multi('Package name', 'packages')
 | 
						|
 | 
						|
            while ask_yn('Do you want to add an extra file?',
 | 
						|
                        helptext=_helptext['extra_files']) == 'y':
 | 
						|
                self._set_multi('Extra file/dir name', 'extra_files')
 | 
						|
 | 
						|
        if ask_yn('Do you want to set Trove classifiers?',
 | 
						|
                  helptext=_helptext['do_classifier']) == 'y':
 | 
						|
            self.set_classifier()
 | 
						|
 | 
						|
    def _find_files(self):
 | 
						|
        # we are looking for python modules and packages,
 | 
						|
        # other stuff are added as regular files
 | 
						|
        pkgs = self.data['packages']
 | 
						|
        modules = self.data['modules']
 | 
						|
        extra_files = self.data['extra_files']
 | 
						|
 | 
						|
        def is_package(path):
 | 
						|
            return os.path.exists(os.path.join(path, '__init__.py'))
 | 
						|
 | 
						|
        curdir = os.getcwd()
 | 
						|
        scanned = []
 | 
						|
        _pref = ['lib', 'include', 'dist', 'build', '.', '~']
 | 
						|
        _suf = ['.pyc']
 | 
						|
 | 
						|
        def to_skip(path):
 | 
						|
            path = relative(path)
 | 
						|
 | 
						|
            for pref in _pref:
 | 
						|
                if path.startswith(pref):
 | 
						|
                    return True
 | 
						|
 | 
						|
            for suf in _suf:
 | 
						|
                if path.endswith(suf):
 | 
						|
                    return True
 | 
						|
 | 
						|
            return False
 | 
						|
 | 
						|
        def relative(path):
 | 
						|
            return path[len(curdir) + 1:]
 | 
						|
 | 
						|
        def dotted(path):
 | 
						|
            res = relative(path).replace(os.path.sep, '.')
 | 
						|
            if res.endswith('.py'):
 | 
						|
                res = res[:-len('.py')]
 | 
						|
            return res
 | 
						|
 | 
						|
        # first pass: packages
 | 
						|
        for root, dirs, files in os.walk(curdir):
 | 
						|
            if to_skip(root):
 | 
						|
                continue
 | 
						|
            for dir_ in sorted(dirs):
 | 
						|
                if to_skip(dir_):
 | 
						|
                    continue
 | 
						|
                fullpath = os.path.join(root, dir_)
 | 
						|
                dotted_name = dotted(fullpath)
 | 
						|
                if is_package(fullpath) and dotted_name not in pkgs:
 | 
						|
                    pkgs.append(dotted_name)
 | 
						|
                    scanned.append(fullpath)
 | 
						|
 | 
						|
        # modules and extra files
 | 
						|
        for root, dirs, files in os.walk(curdir):
 | 
						|
            if to_skip(root):
 | 
						|
                continue
 | 
						|
 | 
						|
            if any(root.startswith(path) for path in scanned):
 | 
						|
                continue
 | 
						|
 | 
						|
            for file in sorted(files):
 | 
						|
                fullpath = os.path.join(root, file)
 | 
						|
                if to_skip(fullpath):
 | 
						|
                    continue
 | 
						|
                # single module?
 | 
						|
                if os.path.splitext(file)[-1] == '.py':
 | 
						|
                    modules.append(dotted(fullpath))
 | 
						|
                else:
 | 
						|
                    extra_files.append(relative(fullpath))
 | 
						|
 | 
						|
    def _set_multi(self, question, name):
 | 
						|
        existing_values = self.data[name]
 | 
						|
        value = ask(question, helptext=_helptext[name]).strip()
 | 
						|
        if value not in existing_values:
 | 
						|
            existing_values.append(value)
 | 
						|
 | 
						|
    def set_classifier(self):
 | 
						|
        self.set_maturity_status(self.classifiers)
 | 
						|
        self.set_license(self.classifiers)
 | 
						|
        self.set_other_classifier(self.classifiers)
 | 
						|
 | 
						|
    def set_other_classifier(self, classifiers):
 | 
						|
        if ask_yn('Do you want to set other trove identifiers?', 'n',
 | 
						|
                  _helptext['trove_generic']) != 'y':
 | 
						|
            return
 | 
						|
        self.walk_classifiers(classifiers, [CLASSIFIERS], '')
 | 
						|
 | 
						|
    def walk_classifiers(self, classifiers, trovepath, desc):
 | 
						|
        trove = trovepath[-1]
 | 
						|
 | 
						|
        if not trove:
 | 
						|
            return
 | 
						|
 | 
						|
        for key in sorted(trove):
 | 
						|
            if len(trove[key]) == 0:
 | 
						|
                if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
 | 
						|
                    classifiers.add(desc[4:] + ' :: ' + key)
 | 
						|
                continue
 | 
						|
 | 
						|
            if ask_yn('Do you want to set items under\n   "%s" (%d sub-items)?'
 | 
						|
                      % (key, len(trove[key])), 'n',
 | 
						|
                      _helptext['trove_generic']) == 'y':
 | 
						|
                self.walk_classifiers(classifiers, trovepath + [trove[key]],
 | 
						|
                                      desc + ' :: ' + key)
 | 
						|
 | 
						|
    def set_license(self, classifiers):
 | 
						|
        while True:
 | 
						|
            license = ask('What license do you use?',
 | 
						|
                          helptext=_helptext['trove_license'], required=False)
 | 
						|
            if not license:
 | 
						|
                return
 | 
						|
 | 
						|
            license_words = license.lower().split(' ')
 | 
						|
            found_list = []
 | 
						|
 | 
						|
            for index, licence in LICENCES:
 | 
						|
                for word in license_words:
 | 
						|
                    if word in licence:
 | 
						|
                        found_list.append(index)
 | 
						|
                        break
 | 
						|
 | 
						|
            if len(found_list) == 0:
 | 
						|
                print('ERROR: Could not find a matching license for "%s"' %
 | 
						|
                      license)
 | 
						|
                continue
 | 
						|
 | 
						|
            question = 'Matching licenses:\n\n'
 | 
						|
 | 
						|
            for index, list_index in enumerate(found_list):
 | 
						|
                question += '   %s) %s\n' % (index + 1,
 | 
						|
                                             _CLASSIFIERS_LIST[list_index])
 | 
						|
 | 
						|
            question += ('\nType the number of the license you wish to use or '
 | 
						|
                         '? to try again:')
 | 
						|
            choice = ask(question, required=False)
 | 
						|
 | 
						|
            if choice == '?':
 | 
						|
                continue
 | 
						|
            if choice == '':
 | 
						|
                return
 | 
						|
 | 
						|
            try:
 | 
						|
                index = found_list[int(choice) - 1]
 | 
						|
            except ValueError:
 | 
						|
                print("ERROR: Invalid selection, type a number from the list "
 | 
						|
                      "above.")
 | 
						|
 | 
						|
            classifiers.add(_CLASSIFIERS_LIST[index])
 | 
						|
 | 
						|
    def set_maturity_status(self, classifiers):
 | 
						|
        maturity_name = lambda mat: mat.split('- ')[-1]
 | 
						|
        maturity_question = '''\
 | 
						|
            Please select the project status:
 | 
						|
 | 
						|
            %s
 | 
						|
 | 
						|
            Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
 | 
						|
                                  for i, n in enumerate(PROJECT_MATURITY))
 | 
						|
        while True:
 | 
						|
            choice = ask(dedent(maturity_question), required=False)
 | 
						|
 | 
						|
            if choice:
 | 
						|
                try:
 | 
						|
                    choice = int(choice) - 1
 | 
						|
                    key = PROJECT_MATURITY[choice]
 | 
						|
                    classifiers.add(key)
 | 
						|
                    return
 | 
						|
                except (IndexError, ValueError):
 | 
						|
                    print("ERROR: Invalid selection, type a single digit "
 | 
						|
                          "number.")
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    """Main entry point."""
 | 
						|
    program = MainProgram()
 | 
						|
    # # uncomment when implemented
 | 
						|
    # if not program.load_existing_setup_script():
 | 
						|
    #     program.inspect_directory()
 | 
						|
    #     program.query_user()
 | 
						|
    #     program.update_config_file()
 | 
						|
    # program.write_setup_script()
 | 
						|
    # packaging.util.cfg_to_args()
 | 
						|
    program()
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    main()
 |