ยปCore Development>Code coverage>Lib/packaging/create.py

Python code coverage for Lib/packaging/create.py

#countcontent
1n/a"""Interactive helper used to create a setup.cfg file.
2n/a
3n/aThis script will generate a packaging configuration file by looking at
4n/athe current directory and asking the user questions. It is intended to
5n/abe called as *pysetup create*.
6n/a"""
7n/a
8n/a# Original code by Sean Reifschneider <jafo@tummy.com>
9n/a
10n/a# Original TODO list:
11n/a# Look for a license file and automatically add the category.
12n/a# When a .c file is found during the walk, can we add it as an extension?
13n/a# Ask if there is a maintainer different that the author
14n/a# Ask for the platform (can we detect this via "import win32" or something?)
15n/a# Ask for the dependencies.
16n/a# Ask for the Requires-Dist
17n/a# Ask for the Provides-Dist
18n/a# Ask for a description
19n/a# Detect scripts (not sure how. #! outside of package?)
20n/a
21n/aimport os
22n/aimport re
23n/aimport imp
24n/aimport sys
25n/aimport glob
26n/aimport shutil
27n/aimport sysconfig
28n/afrom hashlib import md5
29n/afrom textwrap import dedent
30n/afrom tokenize import detect_encoding
31n/afrom configparser import RawConfigParser
32n/a
33n/afrom packaging import logger
34n/a# importing this with an underscore as it should be replaced by the
35n/a# dict form or another structures for all purposes
36n/afrom packaging._trove import all_classifiers as _CLASSIFIERS_LIST
37n/afrom packaging.version import is_valid_version
38n/a
39n/a_FILENAME = 'setup.cfg'
40n/a_DEFAULT_CFG = '.pypkgcreate' # FIXME use a section in user .pydistutils.cfg
41n/a
42n/a_helptext = {
43n/a 'name': '''
44n/aThe name of the project to be packaged, usually a single word composed
45n/aof lower-case characters such as "zope.interface", "sqlalchemy" or
46n/a"CherryPy".
47n/a''',
48n/a 'version': '''
49n/aVersion number of the software, typically 2 or 3 numbers separated by
50n/adots such as "1.0", "0.6b3", or "3.2.1". "0.1.0" is recommended for
51n/ainitial development.
52n/a''',
53n/a 'summary': '''
54n/aA one-line summary of what this project is or does, typically a sentence
55n/a80 characters or less in length.
56n/a''',
57n/a 'author': '''
58n/aThe full name of the author (typically you).
59n/a''',
60n/a 'author_email': '''
61n/aEmail address of the project author.
62n/a''',
63n/a 'do_classifier': '''
64n/aTrove classifiers are optional identifiers that allow you to specify the
65n/aintended audience by saying things like "Beta software with a text UI
66n/afor Linux under the PSF license". However, this can be a somewhat
67n/ainvolved process.
68n/a''',
69n/a 'packages': '''
70n/aPython packages included in the project.
71n/a''',
72n/a 'modules': '''
73n/aPure Python modules included in the project.
74n/a''',
75n/a 'extra_files': '''
76n/aYou can provide extra files/dirs contained in your project.
77n/aIt has to follow the template syntax. XXX add help here.
78n/a''',
79n/a
80n/a 'home_page': '''
81n/aThe home page for the project, typically a public Web page.
82n/a''',
83n/a 'trove_license': '''
84n/aOptionally you can specify a license. Type a string that identifies a
85n/acommon license, and then you can select a list of license specifiers.
86n/a''',
87n/a 'trove_generic': '''
88n/aOptionally, you can set other trove identifiers for things such as the
89n/ahuman language, programming language, user interface, etc.
90n/a''',
91n/a 'setup.py found': '''
92n/aThe setup.py script will be executed to retrieve the metadata.
93n/aAn interactive helper will be run if you answer "n",
94n/a''',
95n/a}
96n/a
97n/aPROJECT_MATURITY = ['Development Status :: 1 - Planning',
98n/a 'Development Status :: 2 - Pre-Alpha',
99n/a 'Development Status :: 3 - Alpha',
100n/a 'Development Status :: 4 - Beta',
101n/a 'Development Status :: 5 - Production/Stable',
102n/a 'Development Status :: 6 - Mature',
103n/a 'Development Status :: 7 - Inactive']
104n/a
105n/a# XXX everything needs docstrings and tests (both low-level tests of various
106n/a# methods and functional tests of running the script)
107n/a
108n/a
109n/adef load_setup():
110n/a """run the setup script (i.e the setup.py file)
111n/a
112n/a This function load the setup file in all cases (even if it have already
113n/a been loaded before, because we are monkey patching its setup function with
114n/a a particular one"""
115n/a with open("setup.py", "rb") as f:
116n/a encoding, lines = detect_encoding(f.readline)
117n/a with open("setup.py", encoding=encoding) as f:
118n/a imp.load_module("setup", f, "setup.py", (".py", "r", imp.PY_SOURCE))
119n/a
120n/a
121n/adef ask_yn(question, default=None, helptext=None):
122n/a question += ' (y/n)'
123n/a while True:
124n/a answer = ask(question, default, helptext, required=True)
125n/a if answer and answer[0].lower() in ('y', 'n'):
126n/a return answer[0].lower()
127n/a
128n/a logger.error('You must select "Y" or "N".')
129n/a
130n/a
131n/a# XXX use util.ask
132n/a# FIXME: if prompt ends with '?', don't add ':'
133n/a
134n/a
135n/adef ask(question, default=None, helptext=None, required=True,
136n/a lengthy=False, multiline=False):
137n/a prompt = '%s: ' % (question,)
138n/a if default:
139n/a prompt = '%s [%s]: ' % (question, default)
140n/a if default and len(question) + len(default) > 70:
141n/a prompt = '%s\n [%s]: ' % (question, default)
142n/a if lengthy or multiline:
143n/a prompt += '\n > '
144n/a
145n/a if not helptext:
146n/a helptext = 'No additional help available.'
147n/a
148n/a helptext = helptext.strip("\n")
149n/a
150n/a while True:
151n/a line = input(prompt).strip()
152n/a if line == '?':
153n/a print('=' * 70)
154n/a print(helptext)
155n/a print('=' * 70)
156n/a continue
157n/a if default and not line:
158n/a return default
159n/a if not line and required:
160n/a print('*' * 70)
161n/a print('This value cannot be empty.')
162n/a print('===========================')
163n/a if helptext:
164n/a print(helptext)
165n/a print('*' * 70)
166n/a continue
167n/a return line
168n/a
169n/a
170n/adef convert_yn_to_bool(yn, yes=True, no=False):
171n/a """Convert a y/yes or n/no to a boolean value."""
172n/a if yn.lower().startswith('y'):
173n/a return yes
174n/a else:
175n/a return no
176n/a
177n/a
178n/adef _build_classifiers_dict(classifiers):
179n/a d = {}
180n/a for key in classifiers:
181n/a subdict = d
182n/a for subkey in key.split(' :: '):
183n/a if subkey not in subdict:
184n/a subdict[subkey] = {}
185n/a subdict = subdict[subkey]
186n/a return d
187n/a
188n/aCLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST)
189n/a
190n/a
191n/adef _build_licences(classifiers):
192n/a res = []
193n/a for index, item in enumerate(classifiers):
194n/a if not item.startswith('License :: '):
195n/a continue
196n/a res.append((index, item.split(' :: ')[-1].lower()))
197n/a return res
198n/a
199n/aLICENCES = _build_licences(_CLASSIFIERS_LIST)
200n/a
201n/a
202n/aclass MainProgram:
203n/a """Make a project setup configuration file (setup.cfg)."""
204n/a
205n/a def __init__(self):
206n/a self.configparser = None
207n/a self.classifiers = set()
208n/a self.data = {'name': '',
209n/a 'version': '1.0.0',
210n/a 'classifier': self.classifiers,
211n/a 'packages': [],
212n/a 'modules': [],
213n/a 'platform': [],
214n/a 'resources': [],
215n/a 'extra_files': [],
216n/a 'scripts': [],
217n/a }
218n/a self._load_defaults()
219n/a
220n/a def __call__(self):
221n/a setupcfg_defined = False
222n/a if self.has_setup_py() and self._prompt_user_for_conversion():
223n/a setupcfg_defined = self.convert_py_to_cfg()
224n/a if not setupcfg_defined:
225n/a self.define_cfg_values()
226n/a self._write_cfg()
227n/a
228n/a def has_setup_py(self):
229n/a """Test for the existence of a setup.py file."""
230n/a return os.path.exists('setup.py')
231n/a
232n/a def define_cfg_values(self):
233n/a self.inspect()
234n/a self.query_user()
235n/a
236n/a def _lookup_option(self, key):
237n/a if not self.configparser.has_option('DEFAULT', key):
238n/a return None
239n/a return self.configparser.get('DEFAULT', key)
240n/a
241n/a def _load_defaults(self):
242n/a # Load default values from a user configuration file
243n/a self.configparser = RawConfigParser()
244n/a # TODO replace with section in distutils config file
245n/a default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG))
246n/a self.configparser.read(default_cfg)
247n/a self.data['author'] = self._lookup_option('author')
248n/a self.data['author_email'] = self._lookup_option('author_email')
249n/a
250n/a def _prompt_user_for_conversion(self):
251n/a # Prompt the user about whether they would like to use the setup.py
252n/a # conversion utility to generate a setup.cfg or generate the setup.cfg
253n/a # from scratch
254n/a answer = ask_yn(('A legacy setup.py has been found.\n'
255n/a 'Would you like to convert it to a setup.cfg?'),
256n/a default="y",
257n/a helptext=_helptext['setup.py found'])
258n/a return convert_yn_to_bool(answer)
259n/a
260n/a def _dotted_packages(self, data):
261n/a packages = sorted(data)
262n/a modified_pkgs = []
263n/a for pkg in packages:
264n/a pkg = pkg.lstrip('./')
265n/a pkg = pkg.replace('/', '.')
266n/a modified_pkgs.append(pkg)
267n/a return modified_pkgs
268n/a
269n/a def _write_cfg(self):
270n/a if os.path.exists(_FILENAME):
271n/a if os.path.exists('%s.old' % _FILENAME):
272n/a message = ("ERROR: %(name)s.old backup exists, please check "
273n/a "that current %(name)s is correct and remove "
274n/a "%(name)s.old" % {'name': _FILENAME})
275n/a logger.error(message)
276n/a return
277n/a shutil.move(_FILENAME, '%s.old' % _FILENAME)
278n/a
279n/a with open(_FILENAME, 'w', encoding='utf-8') as fp:
280n/a fp.write('[metadata]\n')
281n/a # TODO use metadata module instead of hard-coding field-specific
282n/a # behavior here
283n/a
284n/a # simple string entries
285n/a for name in ('name', 'version', 'summary', 'download_url'):
286n/a fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN')))
287n/a
288n/a # optional string entries
289n/a if 'keywords' in self.data and self.data['keywords']:
290n/a # XXX shoud use comma to separate, not space
291n/a fp.write('keywords = %s\n' % ' '.join(self.data['keywords']))
292n/a for name in ('home_page', 'author', 'author_email',
293n/a 'maintainer', 'maintainer_email', 'description-file'):
294n/a if name in self.data and self.data[name]:
295n/a fp.write('%s = %s\n' % (name, self.data[name]))
296n/a if 'description' in self.data:
297n/a fp.write(
298n/a 'description = %s\n'
299n/a % '\n |'.join(self.data['description'].split('\n')))
300n/a
301n/a # multiple use string entries
302n/a for name in ('platform', 'supported-platform', 'classifier',
303n/a 'requires-dist', 'provides-dist', 'obsoletes-dist',
304n/a 'requires-external'):
305n/a if not(name in self.data and self.data[name]):
306n/a continue
307n/a fp.write('%s = ' % name)
308n/a fp.write(''.join(' %s\n' % val
309n/a for val in self.data[name]).lstrip())
310n/a
311n/a fp.write('\n[files]\n')
312n/a
313n/a for name in ('packages', 'modules', 'scripts', 'extra_files'):
314n/a if not(name in self.data and self.data[name]):
315n/a continue
316n/a fp.write('%s = %s\n'
317n/a % (name, '\n '.join(self.data[name]).strip()))
318n/a
319n/a if self.data.get('package_data'):
320n/a fp.write('package_data =\n')
321n/a for pkg, spec in sorted(self.data['package_data'].items()):
322n/a # put one spec per line, indented under the package name
323n/a indent = ' ' * (len(pkg) + 7)
324n/a spec = ('\n' + indent).join(spec)
325n/a fp.write(' %s = %s\n' % (pkg, spec))
326n/a fp.write('\n')
327n/a
328n/a if self.data.get('resources'):
329n/a fp.write('resources =\n')
330n/a for src, dest in self.data['resources']:
331n/a fp.write(' %s = %s\n' % (src, dest))
332n/a fp.write('\n')
333n/a
334n/a os.chmod(_FILENAME, 0o644)
335n/a logger.info('Wrote "%s".' % _FILENAME)
336n/a
337n/a def convert_py_to_cfg(self):
338n/a """Generate a setup.cfg from an existing setup.py.
339n/a
340n/a It only exports the distutils metadata (setuptools specific metadata
341n/a is not currently supported).
342n/a """
343n/a data = self.data
344n/a
345n/a def setup_mock(**attrs):
346n/a """Mock the setup(**attrs) in order to retrieve metadata."""
347n/a
348n/a # TODO use config and metadata instead of Distribution
349n/a from distutils.dist import Distribution
350n/a dist = Distribution(attrs)
351n/a dist.parse_config_files()
352n/a
353n/a # 1. retrieve metadata fields that are quite similar in
354n/a # PEP 314 and PEP 345
355n/a labels = (('name',) * 2,
356n/a ('version',) * 2,
357n/a ('author',) * 2,
358n/a ('author_email',) * 2,
359n/a ('maintainer',) * 2,
360n/a ('maintainer_email',) * 2,
361n/a ('description', 'summary'),
362n/a ('long_description', 'description'),
363n/a ('url', 'home_page'),
364n/a ('platforms', 'platform'),
365n/a ('provides', 'provides-dist'),
366n/a ('obsoletes', 'obsoletes-dist'),
367n/a ('requires', 'requires-dist'))
368n/a
369n/a get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
370n/a data.update((new, get(old)) for old, new in labels if get(old))
371n/a
372n/a # 2. retrieve data that requires special processing
373n/a data['classifier'].update(dist.get_classifiers() or [])
374n/a data['scripts'].extend(dist.scripts or [])
375n/a data['packages'].extend(dist.packages or [])
376n/a data['modules'].extend(dist.py_modules or [])
377n/a # 2.1 data_files -> resources
378n/a if dist.data_files:
379n/a if (len(dist.data_files) < 2 or
380n/a isinstance(dist.data_files[1], str)):
381n/a dist.data_files = [('', dist.data_files)]
382n/a # add tokens in the destination paths
383n/a vars = {'distribution.name': data['name']}
384n/a path_tokens = sysconfig.get_paths(vars=vars).items()
385n/a # sort tokens to use the longest one first
386n/a path_tokens = sorted(path_tokens, key=lambda x: len(x[1]))
387n/a for dest, srcs in (dist.data_files or []):
388n/a dest = os.path.join(sys.prefix, dest)
389n/a dest = dest.replace(os.path.sep, '/')
390n/a for tok, path in path_tokens:
391n/a path = path.replace(os.path.sep, '/')
392n/a if not dest.startswith(path):
393n/a continue
394n/a
395n/a dest = ('{%s}' % tok) + dest[len(path):]
396n/a files = [('/ '.join(src.rsplit('/', 1)), dest)
397n/a for src in srcs]
398n/a data['resources'].extend(files)
399n/a
400n/a # 2.2 package_data
401n/a data['package_data'] = dist.package_data.copy()
402n/a
403n/a # Use README file if its content is the desciption
404n/a if "description" in data:
405n/a ref = md5(re.sub('\s', '',
406n/a self.data['description']).lower().encode())
407n/a ref = ref.digest()
408n/a for readme in glob.glob('README*'):
409n/a with open(readme, encoding='utf-8') as fp:
410n/a contents = fp.read()
411n/a contents = re.sub('\s', '', contents.lower()).encode()
412n/a val = md5(contents).digest()
413n/a if val == ref:
414n/a del data['description']
415n/a data['description-file'] = readme
416n/a break
417n/a
418n/a # apply monkey patch to distutils (v1) and setuptools (if needed)
419n/a # (abort the feature if distutils v1 has been killed)
420n/a try:
421n/a from distutils import core
422n/a core.setup # make sure it's not d2 maskerading as d1
423n/a except (ImportError, AttributeError):
424n/a return
425n/a saved_setups = [(core, core.setup)]
426n/a core.setup = setup_mock
427n/a try:
428n/a import setuptools
429n/a except ImportError:
430n/a pass
431n/a else:
432n/a saved_setups.append((setuptools, setuptools.setup))
433n/a setuptools.setup = setup_mock
434n/a # get metadata by executing the setup.py with the patched setup(...)
435n/a success = False # for python < 2.4
436n/a try:
437n/a load_setup()
438n/a success = True
439n/a finally: # revert monkey patches
440n/a for patched_module, original_setup in saved_setups:
441n/a patched_module.setup = original_setup
442n/a if not self.data:
443n/a raise ValueError('Unable to load metadata from setup.py')
444n/a return success
445n/a
446n/a def inspect(self):
447n/a """Inspect the current working diretory for a name and version.
448n/a
449n/a This information is harvested in where the directory is named
450n/a like [name]-[version].
451n/a """
452n/a dir_name = os.path.basename(os.getcwd())
453n/a self.data['name'] = dir_name
454n/a match = re.match(r'(.*)-(\d.+)', dir_name)
455n/a if match:
456n/a self.data['name'] = match.group(1)
457n/a self.data['version'] = match.group(2)
458n/a # TODO needs testing!
459n/a if not is_valid_version(self.data['version']):
460n/a msg = "Invalid version discovered: %s" % self.data['version']
461n/a raise ValueError(msg)
462n/a
463n/a def query_user(self):
464n/a self.data['name'] = ask('Project name', self.data['name'],
465n/a _helptext['name'])
466n/a
467n/a self.data['version'] = ask('Current version number',
468n/a self.data.get('version'), _helptext['version'])
469n/a self.data['summary'] = ask('Project description summary',
470n/a self.data.get('summary'), _helptext['summary'],
471n/a lengthy=True)
472n/a self.data['author'] = ask('Author name',
473n/a self.data.get('author'), _helptext['author'])
474n/a self.data['author_email'] = ask('Author email address',
475n/a self.data.get('author_email'), _helptext['author_email'])
476n/a self.data['home_page'] = ask('Project home page',
477n/a self.data.get('home_page'), _helptext['home_page'],
478n/a required=False)
479n/a
480n/a if ask_yn('Do you want me to automatically build the file list '
481n/a 'with everything I can find in the current directory? '
482n/a 'If you say no, you will have to define them manually.') == 'y':
483n/a self._find_files()
484n/a else:
485n/a while ask_yn('Do you want to add a single module?'
486n/a ' (you will be able to add full packages next)',
487n/a helptext=_helptext['modules']) == 'y':
488n/a self._set_multi('Module name', 'modules')
489n/a
490n/a while ask_yn('Do you want to add a package?',
491n/a helptext=_helptext['packages']) == 'y':
492n/a self._set_multi('Package name', 'packages')
493n/a
494n/a while ask_yn('Do you want to add an extra file?',
495n/a helptext=_helptext['extra_files']) == 'y':
496n/a self._set_multi('Extra file/dir name', 'extra_files')
497n/a
498n/a if ask_yn('Do you want to set Trove classifiers?',
499n/a helptext=_helptext['do_classifier']) == 'y':
500n/a self.set_classifier()
501n/a
502n/a def _find_files(self):
503n/a # we are looking for python modules and packages,
504n/a # other stuff are added as regular files
505n/a pkgs = self.data['packages']
506n/a modules = self.data['modules']
507n/a extra_files = self.data['extra_files']
508n/a
509n/a def is_package(path):
510n/a return os.path.exists(os.path.join(path, '__init__.py'))
511n/a
512n/a curdir = os.getcwd()
513n/a scanned = []
514n/a _pref = ['lib', 'include', 'dist', 'build', '.', '~']
515n/a _suf = ['.pyc']
516n/a
517n/a def to_skip(path):
518n/a path = relative(path)
519n/a
520n/a for pref in _pref:
521n/a if path.startswith(pref):
522n/a return True
523n/a
524n/a for suf in _suf:
525n/a if path.endswith(suf):
526n/a return True
527n/a
528n/a return False
529n/a
530n/a def relative(path):
531n/a return path[len(curdir) + 1:]
532n/a
533n/a def dotted(path):
534n/a res = relative(path).replace(os.path.sep, '.')
535n/a if res.endswith('.py'):
536n/a res = res[:-len('.py')]
537n/a return res
538n/a
539n/a # first pass: packages
540n/a for root, dirs, files in os.walk(curdir):
541n/a if to_skip(root):
542n/a continue
543n/a for dir_ in sorted(dirs):
544n/a if to_skip(dir_):
545n/a continue
546n/a fullpath = os.path.join(root, dir_)
547n/a dotted_name = dotted(fullpath)
548n/a if is_package(fullpath) and dotted_name not in pkgs:
549n/a pkgs.append(dotted_name)
550n/a scanned.append(fullpath)
551n/a
552n/a # modules and extra files
553n/a for root, dirs, files in os.walk(curdir):
554n/a if to_skip(root):
555n/a continue
556n/a
557n/a if any(root.startswith(path) for path in scanned):
558n/a continue
559n/a
560n/a for file in sorted(files):
561n/a fullpath = os.path.join(root, file)
562n/a if to_skip(fullpath):
563n/a continue
564n/a # single module?
565n/a if os.path.splitext(file)[-1] == '.py':
566n/a modules.append(dotted(fullpath))
567n/a else:
568n/a extra_files.append(relative(fullpath))
569n/a
570n/a def _set_multi(self, question, name):
571n/a existing_values = self.data[name]
572n/a value = ask(question, helptext=_helptext[name]).strip()
573n/a if value not in existing_values:
574n/a existing_values.append(value)
575n/a
576n/a def set_classifier(self):
577n/a self.set_maturity_status(self.classifiers)
578n/a self.set_license(self.classifiers)
579n/a self.set_other_classifier(self.classifiers)
580n/a
581n/a def set_other_classifier(self, classifiers):
582n/a if ask_yn('Do you want to set other trove identifiers?', 'n',
583n/a _helptext['trove_generic']) != 'y':
584n/a return
585n/a self.walk_classifiers(classifiers, [CLASSIFIERS], '')
586n/a
587n/a def walk_classifiers(self, classifiers, trovepath, desc):
588n/a trove = trovepath[-1]
589n/a
590n/a if not trove:
591n/a return
592n/a
593n/a for key in sorted(trove):
594n/a if len(trove[key]) == 0:
595n/a if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
596n/a classifiers.add(desc[4:] + ' :: ' + key)
597n/a continue
598n/a
599n/a if ask_yn('Do you want to set items under\n "%s" (%d sub-items)?'
600n/a % (key, len(trove[key])), 'n',
601n/a _helptext['trove_generic']) == 'y':
602n/a self.walk_classifiers(classifiers, trovepath + [trove[key]],
603n/a desc + ' :: ' + key)
604n/a
605n/a def set_license(self, classifiers):
606n/a while True:
607n/a license = ask('What license do you use?',
608n/a helptext=_helptext['trove_license'], required=False)
609n/a if not license:
610n/a return
611n/a
612n/a license_words = license.lower().split(' ')
613n/a found_list = []
614n/a
615n/a for index, licence in LICENCES:
616n/a for word in license_words:
617n/a if word in licence:
618n/a found_list.append(index)
619n/a break
620n/a
621n/a if len(found_list) == 0:
622n/a logger.error('Could not find a matching license for "%s"' %
623n/a license)
624n/a continue
625n/a
626n/a question = 'Matching licenses:\n\n'
627n/a
628n/a for index, list_index in enumerate(found_list):
629n/a question += ' %s) %s\n' % (index + 1,
630n/a _CLASSIFIERS_LIST[list_index])
631n/a
632n/a question += ('\nType the number of the license you wish to use or '
633n/a '? to try again:')
634n/a choice = ask(question, required=False)
635n/a
636n/a if choice == '?':
637n/a continue
638n/a if choice == '':
639n/a return
640n/a
641n/a try:
642n/a index = found_list[int(choice) - 1]
643n/a except ValueError:
644n/a logger.error(
645n/a "Invalid selection, type a number from the list above.")
646n/a
647n/a classifiers.add(_CLASSIFIERS_LIST[index])
648n/a
649n/a def set_maturity_status(self, classifiers):
650n/a maturity_name = lambda mat: mat.split('- ')[-1]
651n/a maturity_question = '''\
652n/a Please select the project status:
653n/a
654n/a %s
655n/a
656n/a Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
657n/a for i, n in enumerate(PROJECT_MATURITY))
658n/a while True:
659n/a choice = ask(dedent(maturity_question), required=False)
660n/a
661n/a if choice:
662n/a try:
663n/a choice = int(choice) - 1
664n/a key = PROJECT_MATURITY[choice]
665n/a classifiers.add(key)
666n/a return
667n/a except (IndexError, ValueError):
668n/a logger.error(
669n/a "Invalid selection, type a single digit number.")
670n/a
671n/a
672n/adef main():
673n/a """Main entry point."""
674n/a program = MainProgram()
675n/a # # uncomment when implemented
676n/a # if not program.load_existing_setup_script():
677n/a # program.inspect_directory()
678n/a # program.query_user()
679n/a # program.update_config_file()
680n/a # program.write_setup_script()
681n/a # packaging.util.cfg_to_args()
682n/a program()