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