blob: 1e84e2e547021106a23cc06c40d9e861f38080b4 [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
Éric Araujo35a4d012011-06-04 22:24:59 +020031from configparser import RawConfigParser
Tarek Ziade1231a4e2011-05-19 13:07:25 +020032# importing this with an underscore as it should be replaced by the
33# dict form or another structures for all purposes
34from packaging._trove import all_classifiers as _CLASSIFIERS_LIST
35from packaging.version import is_valid_version
36
37_FILENAME = 'setup.cfg'
Éric Araujo95fc53f2011-09-01 05:11:29 +020038_DEFAULT_CFG = '.pypkgcreate' # FIXME use a section in user .pydistutils.cfg
Tarek Ziade1231a4e2011-05-19 13:07:25 +020039
40_helptext = {
41 'name': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020042The name of the project to be packaged, usually a single word composed
43of lower-case characters such as "zope.interface", "sqlalchemy" or
44"CherryPy".
Tarek Ziade1231a4e2011-05-19 13:07:25 +020045''',
46 'version': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020047Version number of the software, typically 2 or 3 numbers separated by
48dots such as "1.0", "0.6b3", or "3.2.1". "0.1.0" is recommended for
49initial development.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020050''',
51 'summary': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020052A one-line summary of what this project is or does, typically a sentence
5380 characters or less in length.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020054''',
55 'author': '''
56The full name of the author (typically you).
57''',
58 'author_email': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020059Email address of the project author.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020060''',
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
Éric Araujo50e516a2011-08-19 00:56:57 +020064for Linux under the PSF license". However, this can be a somewhat
65involved process.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020066''',
67 'packages': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020068Python packages included in the project.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020069''',
70 'modules': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020071Pure Python modules included in the project.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020072''',
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': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020079The home page for the project, typically a public Web page.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020080''',
81 'trove_license': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020082Optionally you can specify a license. Type a string that identifies a
83common license, and then you can select a list of license specifiers.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020084''',
85 'trove_generic': '''
86Optionally, you can set other trove identifiers for things such as the
Éric Araujo50e516a2011-08-19 00:56:57 +020087human language, programming language, user interface, etc.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020088''',
89 'setup.py found': '''
90The setup.py script will be executed to retrieve the metadata.
Éric Araujo45593832011-06-04 22:37:57 +020091An interactive helper will be run if you answer "n",
Tarek Ziade1231a4e2011-05-19 13:07:25 +020092''',
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
Éric Araujo95fc53f2011-09-01 05:11:29 +0200129# XXX use util.ask
130# FIXME: if prompt ends with '?', don't add ':'
131
132
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200133def ask(question, default=None, helptext=None, required=True,
134 lengthy=False, multiline=False):
135 prompt = '%s: ' % (question,)
136 if default:
137 prompt = '%s [%s]: ' % (question, default)
138 if default and len(question) + len(default) > 70:
139 prompt = '%s\n [%s]: ' % (question, default)
140 if lengthy or multiline:
141 prompt += '\n > '
142
143 if not helptext:
144 helptext = 'No additional help available.'
145
146 helptext = helptext.strip("\n")
147
148 while True:
149 sys.stdout.write(prompt)
150 sys.stdout.flush()
151
152 line = sys.stdin.readline().strip()
153 if line == '?':
154 print('=' * 70)
155 print(helptext)
156 print('=' * 70)
157 continue
158 if default and not line:
159 return default
160 if not line and required:
161 print('*' * 70)
162 print('This value cannot be empty.')
163 print('===========================')
164 if helptext:
165 print(helptext)
166 print('*' * 70)
167 continue
168 return line
169
170
171def convert_yn_to_bool(yn, yes=True, no=False):
172 """Convert a y/yes or n/no to a boolean value."""
173 if yn.lower().startswith('y'):
174 return yes
175 else:
176 return no
177
178
179def _build_classifiers_dict(classifiers):
180 d = {}
181 for key in classifiers:
Éric Araujodf8ef022011-06-08 04:47:13 +0200182 subdict = d
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200183 for subkey in key.split(' :: '):
Éric Araujodf8ef022011-06-08 04:47:13 +0200184 if subkey not in subdict:
185 subdict[subkey] = {}
186 subdict = subdict[subkey]
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200187 return d
188
189CLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST)
190
191
192def _build_licences(classifiers):
193 res = []
194 for index, item in enumerate(classifiers):
195 if not item.startswith('License :: '):
196 continue
197 res.append((index, item.split(' :: ')[-1].lower()))
198 return res
199
200LICENCES = _build_licences(_CLASSIFIERS_LIST)
201
202
203class MainProgram:
204 """Make a project setup configuration file (setup.cfg)."""
205
206 def __init__(self):
207 self.configparser = None
208 self.classifiers = set()
209 self.data = {'name': '',
210 'version': '1.0.0',
211 'classifier': self.classifiers,
212 'packages': [],
213 'modules': [],
214 'platform': [],
215 'resources': [],
216 'extra_files': [],
217 'scripts': [],
218 }
219 self._load_defaults()
220
221 def __call__(self):
222 setupcfg_defined = False
223 if self.has_setup_py() and self._prompt_user_for_conversion():
224 setupcfg_defined = self.convert_py_to_cfg()
225 if not setupcfg_defined:
226 self.define_cfg_values()
227 self._write_cfg()
228
229 def has_setup_py(self):
Éric Araujo35a4d012011-06-04 22:24:59 +0200230 """Test for the existence of a setup.py file."""
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200231 return os.path.exists('setup.py')
232
233 def define_cfg_values(self):
234 self.inspect()
235 self.query_user()
236
237 def _lookup_option(self, key):
238 if not self.configparser.has_option('DEFAULT', key):
239 return None
240 return self.configparser.get('DEFAULT', key)
241
242 def _load_defaults(self):
243 # Load default values from a user configuration file
244 self.configparser = RawConfigParser()
245 # TODO replace with section in distutils config file
246 default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG))
247 self.configparser.read(default_cfg)
248 self.data['author'] = self._lookup_option('author')
249 self.data['author_email'] = self._lookup_option('author_email')
250
251 def _prompt_user_for_conversion(self):
252 # Prompt the user about whether they would like to use the setup.py
253 # conversion utility to generate a setup.cfg or generate the setup.cfg
254 # from scratch
255 answer = ask_yn(('A legacy setup.py has been found.\n'
256 'Would you like to convert it to a setup.cfg?'),
257 default="y",
258 helptext=_helptext['setup.py found'])
259 return convert_yn_to_bool(answer)
260
261 def _dotted_packages(self, data):
262 packages = sorted(data)
263 modified_pkgs = []
264 for pkg in packages:
265 pkg = pkg.lstrip('./')
266 pkg = pkg.replace('/', '.')
267 modified_pkgs.append(pkg)
268 return modified_pkgs
269
270 def _write_cfg(self):
271 if os.path.exists(_FILENAME):
272 if os.path.exists('%s.old' % _FILENAME):
273 print("ERROR: %(name)s.old backup exists, please check that "
274 "current %(name)s is correct and remove %(name)s.old" %
275 {'name': _FILENAME})
276 return
277 shutil.move(_FILENAME, '%s.old' % _FILENAME)
278
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200279 with open(_FILENAME, 'w', encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200280 fp.write('[metadata]\n')
Éric Araujo8f66f612011-06-04 22:36:40 +0200281 # TODO use metadata module instead of hard-coding field-specific
282 # behavior here
283
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200284 # simple string entries
285 for name in ('name', 'version', 'summary', 'download_url'):
286 fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN')))
Éric Araujo8f66f612011-06-04 22:36:40 +0200287
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200288 # optional string entries
289 if 'keywords' in self.data and self.data['keywords']:
290 fp.write('keywords = %s\n' % ' '.join(self.data['keywords']))
291 for name in ('home_page', 'author', 'author_email',
292 'maintainer', 'maintainer_email', 'description-file'):
293 if name in self.data and self.data[name]:
294 fp.write('%s = %s\n' % (name, self.data[name]))
295 if 'description' in self.data:
296 fp.write(
297 'description = %s\n'
298 % '\n |'.join(self.data['description'].split('\n')))
Éric Araujo8f66f612011-06-04 22:36:40 +0200299
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200300 # multiple use string entries
301 for name in ('platform', 'supported-platform', 'classifier',
302 'requires-dist', 'provides-dist', 'obsoletes-dist',
303 'requires-external'):
304 if not(name in self.data and self.data[name]):
305 continue
306 fp.write('%s = ' % name)
307 fp.write(''.join(' %s\n' % val
308 for val in self.data[name]).lstrip())
309 fp.write('\n[files]\n')
310 for name in ('packages', 'modules', 'scripts',
311 'package_data', 'extra_files'):
312 if not(name in self.data and self.data[name]):
313 continue
314 fp.write('%s = %s\n'
315 % (name, '\n '.join(self.data[name]).strip()))
316 fp.write('\nresources =\n')
317 for src, dest in self.data['resources']:
318 fp.write(' %s = %s\n' % (src, dest))
319 fp.write('\n')
320
321 os.chmod(_FILENAME, 0o644)
322 print('Wrote "%s".' % _FILENAME)
323
324 def convert_py_to_cfg(self):
325 """Generate a setup.cfg from an existing setup.py.
326
327 It only exports the distutils metadata (setuptools specific metadata
328 is not currently supported).
329 """
330 data = self.data
331
332 def setup_mock(**attrs):
333 """Mock the setup(**attrs) in order to retrieve metadata."""
Éric Araujo8f66f612011-06-04 22:36:40 +0200334
335 # TODO use config and metadata instead of Distribution
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200336 from distutils.dist import Distribution
337 dist = Distribution(attrs)
338 dist.parse_config_files()
339
340 # 1. retrieve metadata fields that are quite similar in
341 # PEP 314 and PEP 345
342 labels = (('name',) * 2,
343 ('version',) * 2,
344 ('author',) * 2,
345 ('author_email',) * 2,
346 ('maintainer',) * 2,
347 ('maintainer_email',) * 2,
348 ('description', 'summary'),
349 ('long_description', 'description'),
350 ('url', 'home_page'),
351 ('platforms', 'platform'),
352 # backport only for 2.5+
353 ('provides', 'provides-dist'),
354 ('obsoletes', 'obsoletes-dist'),
355 ('requires', 'requires-dist'))
356
357 get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
358 data.update((new, get(old)) for old, new in labels if get(old))
359
360 # 2. retrieve data that requires special processing
361 data['classifier'].update(dist.get_classifiers() or [])
362 data['scripts'].extend(dist.scripts or [])
363 data['packages'].extend(dist.packages or [])
364 data['modules'].extend(dist.py_modules or [])
365 # 2.1 data_files -> resources
366 if dist.data_files:
Éric Araujo8f66f612011-06-04 22:36:40 +0200367 if (len(dist.data_files) < 2 or
368 isinstance(dist.data_files[1], str)):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200369 dist.data_files = [('', dist.data_files)]
370 # add tokens in the destination paths
371 vars = {'distribution.name': data['name']}
Éric Araujof30b5ae2011-09-18 21:03:24 +0200372 path_tokens = sysconfig.get_paths(vars=vars).items()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200373 # sort tokens to use the longest one first
Éric Araujof30b5ae2011-09-18 21:03:24 +0200374 path_tokens = sorted(path_tokens, key=lambda x: len(x[1]))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200375 for dest, srcs in (dist.data_files or []):
376 dest = os.path.join(sys.prefix, dest)
Tarek Ziade2db56742011-05-21 14:24:14 +0200377 dest = dest.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200378 for tok, path in path_tokens:
Tarek Ziade2db56742011-05-21 14:24:14 +0200379 path = path.replace(os.path.sep, '/')
380 if not dest.startswith(path):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200381 continue
Tarek Ziade2db56742011-05-21 14:24:14 +0200382
383 dest = ('{%s}' % tok) + dest[len(path):]
384 files = [('/ '.join(src.rsplit('/', 1)), dest)
Éric Araujo8f66f612011-06-04 22:36:40 +0200385 for src in srcs]
Tarek Ziade2db56742011-05-21 14:24:14 +0200386 data['resources'].extend(files)
387
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200388 # 2.2 package_data -> extra_files
389 package_dirs = dist.package_dir or {}
Éric Araujo8f66f612011-06-04 22:36:40 +0200390 for package, extras in dist.package_data.items() or []:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200391 package_dir = package_dirs.get(package, package)
Tarek Ziade2db56742011-05-21 14:24:14 +0200392 for file_ in extras:
393 if package_dir:
394 file_ = package_dir + '/' + file_
395 data['extra_files'].append(file_)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200396
397 # Use README file if its content is the desciption
398 if "description" in data:
399 ref = md5(re.sub('\s', '',
400 self.data['description']).lower().encode())
401 ref = ref.digest()
402 for readme in glob.glob('README*'):
Victor Stinner35de5ac2011-05-19 15:09:57 +0200403 with open(readme, encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200404 contents = fp.read()
Victor Stinner35de5ac2011-05-19 15:09:57 +0200405 contents = re.sub('\s', '', contents.lower()).encode()
406 val = md5(contents).digest()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200407 if val == ref:
408 del data['description']
409 data['description-file'] = readme
410 break
411
412 # apply monkey patch to distutils (v1) and setuptools (if needed)
413 # (abort the feature if distutils v1 has been killed)
414 try:
415 from distutils import core
416 core.setup # make sure it's not d2 maskerading as d1
417 except (ImportError, AttributeError):
418 return
419 saved_setups = [(core, core.setup)]
420 core.setup = setup_mock
421 try:
422 import setuptools
423 except ImportError:
424 pass
425 else:
426 saved_setups.append((setuptools, setuptools.setup))
427 setuptools.setup = setup_mock
428 # get metadata by executing the setup.py with the patched setup(...)
429 success = False # for python < 2.4
430 try:
431 load_setup()
432 success = True
433 finally: # revert monkey patches
434 for patched_module, original_setup in saved_setups:
435 patched_module.setup = original_setup
436 if not self.data:
437 raise ValueError('Unable to load metadata from setup.py')
438 return success
439
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200440 def inspect(self):
441 """Inspect the current working diretory for a name and version.
442
443 This information is harvested in where the directory is named
444 like [name]-[version].
445 """
446 dir_name = os.path.basename(os.getcwd())
447 self.data['name'] = dir_name
448 match = re.match(r'(.*)-(\d.+)', dir_name)
449 if match:
450 self.data['name'] = match.group(1)
451 self.data['version'] = match.group(2)
Éric Araujo8f66f612011-06-04 22:36:40 +0200452 # TODO needs testing!
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200453 if not is_valid_version(self.data['version']):
454 msg = "Invalid version discovered: %s" % self.data['version']
Éric Araujo8f66f612011-06-04 22:36:40 +0200455 raise ValueError(msg)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200456
457 def query_user(self):
458 self.data['name'] = ask('Project name', self.data['name'],
459 _helptext['name'])
460
461 self.data['version'] = ask('Current version number',
462 self.data.get('version'), _helptext['version'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200463 self.data['summary'] = ask('Project description summary',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200464 self.data.get('summary'), _helptext['summary'],
465 lengthy=True)
466 self.data['author'] = ask('Author name',
467 self.data.get('author'), _helptext['author'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200468 self.data['author_email'] = ask('Author email address',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200469 self.data.get('author_email'), _helptext['author_email'])
Éric Araujo45593832011-06-04 22:37:57 +0200470 self.data['home_page'] = ask('Project home page',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200471 self.data.get('home_page'), _helptext['home_page'],
472 required=False)
473
474 if ask_yn('Do you want me to automatically build the file list '
Éric Araujo45593832011-06-04 22:37:57 +0200475 'with everything I can find in the current directory? '
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200476 'If you say no, you will have to define them manually.') == 'y':
477 self._find_files()
478 else:
Éric Araujo45593832011-06-04 22:37:57 +0200479 while ask_yn('Do you want to add a single module?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200480 ' (you will be able to add full packages next)',
481 helptext=_helptext['modules']) == 'y':
482 self._set_multi('Module name', 'modules')
483
Éric Araujo45593832011-06-04 22:37:57 +0200484 while ask_yn('Do you want to add a package?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200485 helptext=_helptext['packages']) == 'y':
486 self._set_multi('Package name', 'packages')
487
Éric Araujo45593832011-06-04 22:37:57 +0200488 while ask_yn('Do you want to add an extra file?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200489 helptext=_helptext['extra_files']) == 'y':
490 self._set_multi('Extra file/dir name', 'extra_files')
491
492 if ask_yn('Do you want to set Trove classifiers?',
493 helptext=_helptext['do_classifier']) == 'y':
494 self.set_classifier()
495
496 def _find_files(self):
497 # we are looking for python modules and packages,
498 # other stuff are added as regular files
499 pkgs = self.data['packages']
500 modules = self.data['modules']
501 extra_files = self.data['extra_files']
502
503 def is_package(path):
504 return os.path.exists(os.path.join(path, '__init__.py'))
505
506 curdir = os.getcwd()
507 scanned = []
508 _pref = ['lib', 'include', 'dist', 'build', '.', '~']
509 _suf = ['.pyc']
510
511 def to_skip(path):
512 path = relative(path)
513
514 for pref in _pref:
515 if path.startswith(pref):
516 return True
517
518 for suf in _suf:
519 if path.endswith(suf):
520 return True
521
522 return False
523
524 def relative(path):
525 return path[len(curdir) + 1:]
526
527 def dotted(path):
528 res = relative(path).replace(os.path.sep, '.')
529 if res.endswith('.py'):
530 res = res[:-len('.py')]
531 return res
532
533 # first pass: packages
534 for root, dirs, files in os.walk(curdir):
535 if to_skip(root):
536 continue
537 for dir_ in sorted(dirs):
538 if to_skip(dir_):
539 continue
540 fullpath = os.path.join(root, dir_)
541 dotted_name = dotted(fullpath)
542 if is_package(fullpath) and dotted_name not in pkgs:
543 pkgs.append(dotted_name)
544 scanned.append(fullpath)
545
546 # modules and extra files
547 for root, dirs, files in os.walk(curdir):
548 if to_skip(root):
549 continue
550
551 if any(root.startswith(path) for path in scanned):
552 continue
553
554 for file in sorted(files):
555 fullpath = os.path.join(root, file)
556 if to_skip(fullpath):
557 continue
558 # single module?
559 if os.path.splitext(file)[-1] == '.py':
560 modules.append(dotted(fullpath))
561 else:
562 extra_files.append(relative(fullpath))
563
564 def _set_multi(self, question, name):
565 existing_values = self.data[name]
566 value = ask(question, helptext=_helptext[name]).strip()
567 if value not in existing_values:
568 existing_values.append(value)
569
570 def set_classifier(self):
571 self.set_maturity_status(self.classifiers)
572 self.set_license(self.classifiers)
573 self.set_other_classifier(self.classifiers)
574
575 def set_other_classifier(self, classifiers):
Éric Araujo45593832011-06-04 22:37:57 +0200576 if ask_yn('Do you want to set other trove identifiers?', 'n',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200577 _helptext['trove_generic']) != 'y':
578 return
579 self.walk_classifiers(classifiers, [CLASSIFIERS], '')
580
581 def walk_classifiers(self, classifiers, trovepath, desc):
582 trove = trovepath[-1]
583
584 if not trove:
585 return
586
587 for key in sorted(trove):
588 if len(trove[key]) == 0:
589 if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
590 classifiers.add(desc[4:] + ' :: ' + key)
591 continue
592
Éric Araujo45593832011-06-04 22:37:57 +0200593 if ask_yn('Do you want to set items under\n "%s" (%d sub-items)?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200594 % (key, len(trove[key])), 'n',
595 _helptext['trove_generic']) == 'y':
596 self.walk_classifiers(classifiers, trovepath + [trove[key]],
597 desc + ' :: ' + key)
598
599 def set_license(self, classifiers):
600 while True:
Éric Araujo45593832011-06-04 22:37:57 +0200601 license = ask('What license do you use?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200602 helptext=_helptext['trove_license'], required=False)
603 if not license:
604 return
605
606 license_words = license.lower().split(' ')
607 found_list = []
608
609 for index, licence in LICENCES:
610 for word in license_words:
611 if word in licence:
612 found_list.append(index)
613 break
614
615 if len(found_list) == 0:
616 print('ERROR: Could not find a matching license for "%s"' %
617 license)
618 continue
619
620 question = 'Matching licenses:\n\n'
621
622 for index, list_index in enumerate(found_list):
623 question += ' %s) %s\n' % (index + 1,
624 _CLASSIFIERS_LIST[list_index])
625
626 question += ('\nType the number of the license you wish to use or '
627 '? to try again:')
628 choice = ask(question, required=False)
629
630 if choice == '?':
631 continue
632 if choice == '':
633 return
634
635 try:
636 index = found_list[int(choice) - 1]
637 except ValueError:
638 print("ERROR: Invalid selection, type a number from the list "
639 "above.")
640
641 classifiers.add(_CLASSIFIERS_LIST[index])
642
643 def set_maturity_status(self, classifiers):
644 maturity_name = lambda mat: mat.split('- ')[-1]
645 maturity_question = '''\
646 Please select the project status:
647
648 %s
649
650 Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
651 for i, n in enumerate(PROJECT_MATURITY))
652 while True:
653 choice = ask(dedent(maturity_question), required=False)
654
655 if choice:
656 try:
657 choice = int(choice) - 1
658 key = PROJECT_MATURITY[choice]
659 classifiers.add(key)
660 return
661 except (IndexError, ValueError):
662 print("ERROR: Invalid selection, type a single digit "
663 "number.")
664
665
666def main():
667 """Main entry point."""
668 program = MainProgram()
669 # # uncomment when implemented
670 # if not program.load_existing_setup_script():
671 # program.inspect_directory()
672 # program.query_user()
673 # program.update_config_file()
674 # program.write_setup_script()
675 # packaging.util.cfg_to_args()
676 program()
677
678
679if __name__ == '__main__':
680 main()