diff options
Diffstat (limited to 'Lib/packaging/create.py')
-rw-r--r-- | Lib/packaging/create.py | 682 |
1 files changed, 0 insertions, 682 deletions
diff --git a/Lib/packaging/create.py b/Lib/packaging/create.py deleted file mode 100644 index 3d45ca9..0000000 --- a/Lib/packaging/create.py +++ /dev/null @@ -1,682 +0,0 @@ -"""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 -from hashlib import md5 -from textwrap import dedent -from tokenize import detect_encoding -from configparser import RawConfigParser - -from packaging import logger -# 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' # FIXME use a section in user .pydistutils.cfg - -_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 = 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 ('y', 'n'): - return answer[0].lower() - - logger.error('You must select "Y" or "N".') - - -# XXX use util.ask -# FIXME: if prompt ends with '?', don't add ':' - - -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: - line = input(prompt).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): - message = ("ERROR: %(name)s.old backup exists, please check " - "that current %(name)s is correct and remove " - "%(name)s.old" % {'name': _FILENAME}) - logger.error(message) - 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']: - # XXX shoud use comma to separate, not space - 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', '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())) - - if self.data.get('package_data'): - fp.write('package_data =\n') - for pkg, spec in sorted(self.data['package_data'].items()): - # put one spec per line, indented under the package name - indent = ' ' * (len(pkg) + 7) - spec = ('\n' + indent).join(spec) - fp.write(' %s = %s\n' % (pkg, spec)) - fp.write('\n') - - if self.data.get('resources'): - fp.write('resources =\n') - for src, dest in self.data['resources']: - fp.write(' %s = %s\n' % (src, dest)) - fp.write('\n') - - os.chmod(_FILENAME, 0o644) - logger.info('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'), - ('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 = sysconfig.get_paths(vars=vars).items() - # sort tokens to use the longest one first - path_tokens = sorted(path_tokens, key=lambda x: len(x[1])) - 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 - data['package_data'] = dist.package_data.copy() - - # 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: - logger.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: - logger.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): - logger.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() |