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