blob: ecabca041a358cea70588d4c097908d0109c2cea [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'
Éric Araujo95fc53f2011-09-01 05:11:29 +020039_DEFAULT_CFG = '.pypkgcreate' # FIXME use a section in user .pydistutils.cfg
Tarek Ziade1231a4e2011-05-19 13:07:25 +020040
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
Éric Araujo95fc53f2011-09-01 05:11:29 +0200130# XXX use util.ask
131# FIXME: if prompt ends with '?', don't add ':'
132
133
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200134def ask(question, default=None, helptext=None, required=True,
135 lengthy=False, multiline=False):
136 prompt = '%s: ' % (question,)
137 if default:
138 prompt = '%s [%s]: ' % (question, default)
139 if default and len(question) + len(default) > 70:
140 prompt = '%s\n [%s]: ' % (question, default)
141 if lengthy or multiline:
142 prompt += '\n > '
143
144 if not helptext:
145 helptext = 'No additional help available.'
146
147 helptext = helptext.strip("\n")
148
149 while True:
150 sys.stdout.write(prompt)
151 sys.stdout.flush()
152
153 line = sys.stdin.readline().strip()
154 if line == '?':
155 print('=' * 70)
156 print(helptext)
157 print('=' * 70)
158 continue
159 if default and not line:
160 return default
161 if not line and required:
162 print('*' * 70)
163 print('This value cannot be empty.')
164 print('===========================')
165 if helptext:
166 print(helptext)
167 print('*' * 70)
168 continue
169 return line
170
171
172def convert_yn_to_bool(yn, yes=True, no=False):
173 """Convert a y/yes or n/no to a boolean value."""
174 if yn.lower().startswith('y'):
175 return yes
176 else:
177 return no
178
179
180def _build_classifiers_dict(classifiers):
181 d = {}
182 for key in classifiers:
Éric Araujodf8ef022011-06-08 04:47:13 +0200183 subdict = d
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200184 for subkey in key.split(' :: '):
Éric Araujodf8ef022011-06-08 04:47:13 +0200185 if subkey not in subdict:
186 subdict[subkey] = {}
187 subdict = subdict[subkey]
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200188 return d
189
190CLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST)
191
192
193def _build_licences(classifiers):
194 res = []
195 for index, item in enumerate(classifiers):
196 if not item.startswith('License :: '):
197 continue
198 res.append((index, item.split(' :: ')[-1].lower()))
199 return res
200
201LICENCES = _build_licences(_CLASSIFIERS_LIST)
202
203
204class MainProgram:
205 """Make a project setup configuration file (setup.cfg)."""
206
207 def __init__(self):
208 self.configparser = None
209 self.classifiers = set()
210 self.data = {'name': '',
211 'version': '1.0.0',
212 'classifier': self.classifiers,
213 'packages': [],
214 'modules': [],
215 'platform': [],
216 'resources': [],
217 'extra_files': [],
218 'scripts': [],
219 }
220 self._load_defaults()
221
222 def __call__(self):
223 setupcfg_defined = False
224 if self.has_setup_py() and self._prompt_user_for_conversion():
225 setupcfg_defined = self.convert_py_to_cfg()
226 if not setupcfg_defined:
227 self.define_cfg_values()
228 self._write_cfg()
229
230 def has_setup_py(self):
Éric Araujo35a4d012011-06-04 22:24:59 +0200231 """Test for the existence of a setup.py file."""
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200232 return os.path.exists('setup.py')
233
234 def define_cfg_values(self):
235 self.inspect()
236 self.query_user()
237
238 def _lookup_option(self, key):
239 if not self.configparser.has_option('DEFAULT', key):
240 return None
241 return self.configparser.get('DEFAULT', key)
242
243 def _load_defaults(self):
244 # Load default values from a user configuration file
245 self.configparser = RawConfigParser()
246 # TODO replace with section in distutils config file
247 default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG))
248 self.configparser.read(default_cfg)
249 self.data['author'] = self._lookup_option('author')
250 self.data['author_email'] = self._lookup_option('author_email')
251
252 def _prompt_user_for_conversion(self):
253 # Prompt the user about whether they would like to use the setup.py
254 # conversion utility to generate a setup.cfg or generate the setup.cfg
255 # from scratch
256 answer = ask_yn(('A legacy setup.py has been found.\n'
257 'Would you like to convert it to a setup.cfg?'),
258 default="y",
259 helptext=_helptext['setup.py found'])
260 return convert_yn_to_bool(answer)
261
262 def _dotted_packages(self, data):
263 packages = sorted(data)
264 modified_pkgs = []
265 for pkg in packages:
266 pkg = pkg.lstrip('./')
267 pkg = pkg.replace('/', '.')
268 modified_pkgs.append(pkg)
269 return modified_pkgs
270
271 def _write_cfg(self):
272 if os.path.exists(_FILENAME):
273 if os.path.exists('%s.old' % _FILENAME):
274 print("ERROR: %(name)s.old backup exists, please check that "
275 "current %(name)s is correct and remove %(name)s.old" %
276 {'name': _FILENAME})
277 return
278 shutil.move(_FILENAME, '%s.old' % _FILENAME)
279
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200280 with open(_FILENAME, 'w', encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200281 fp.write('[metadata]\n')
Éric Araujo8f66f612011-06-04 22:36:40 +0200282 # TODO use metadata module instead of hard-coding field-specific
283 # behavior here
284
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200285 # simple string entries
286 for name in ('name', 'version', 'summary', 'download_url'):
287 fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN')))
Éric Araujo8f66f612011-06-04 22:36:40 +0200288
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200289 # optional string entries
290 if 'keywords' in self.data and self.data['keywords']:
291 fp.write('keywords = %s\n' % ' '.join(self.data['keywords']))
292 for name in ('home_page', 'author', 'author_email',
293 'maintainer', 'maintainer_email', 'description-file'):
294 if name in self.data and self.data[name]:
295 fp.write('%s = %s\n' % (name, self.data[name]))
296 if 'description' in self.data:
297 fp.write(
298 'description = %s\n'
299 % '\n |'.join(self.data['description'].split('\n')))
Éric Araujo8f66f612011-06-04 22:36:40 +0200300
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200301 # multiple use string entries
302 for name in ('platform', 'supported-platform', 'classifier',
303 'requires-dist', 'provides-dist', 'obsoletes-dist',
304 'requires-external'):
305 if not(name in self.data and self.data[name]):
306 continue
307 fp.write('%s = ' % name)
308 fp.write(''.join(' %s\n' % val
309 for val in self.data[name]).lstrip())
310 fp.write('\n[files]\n')
311 for name in ('packages', 'modules', 'scripts',
312 'package_data', 'extra_files'):
313 if not(name in self.data and self.data[name]):
314 continue
315 fp.write('%s = %s\n'
316 % (name, '\n '.join(self.data[name]).strip()))
317 fp.write('\nresources =\n')
318 for src, dest in self.data['resources']:
319 fp.write(' %s = %s\n' % (src, dest))
320 fp.write('\n')
321
322 os.chmod(_FILENAME, 0o644)
323 print('Wrote "%s".' % _FILENAME)
324
325 def convert_py_to_cfg(self):
326 """Generate a setup.cfg from an existing setup.py.
327
328 It only exports the distutils metadata (setuptools specific metadata
329 is not currently supported).
330 """
331 data = self.data
332
333 def setup_mock(**attrs):
334 """Mock the setup(**attrs) in order to retrieve metadata."""
Éric Araujo8f66f612011-06-04 22:36:40 +0200335
336 # TODO use config and metadata instead of Distribution
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200337 from distutils.dist import Distribution
338 dist = Distribution(attrs)
339 dist.parse_config_files()
340
341 # 1. retrieve metadata fields that are quite similar in
342 # PEP 314 and PEP 345
343 labels = (('name',) * 2,
344 ('version',) * 2,
345 ('author',) * 2,
346 ('author_email',) * 2,
347 ('maintainer',) * 2,
348 ('maintainer_email',) * 2,
349 ('description', 'summary'),
350 ('long_description', 'description'),
351 ('url', 'home_page'),
352 ('platforms', 'platform'),
353 # backport only for 2.5+
354 ('provides', 'provides-dist'),
355 ('obsoletes', 'obsoletes-dist'),
356 ('requires', 'requires-dist'))
357
358 get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
359 data.update((new, get(old)) for old, new in labels if get(old))
360
361 # 2. retrieve data that requires special processing
362 data['classifier'].update(dist.get_classifiers() or [])
363 data['scripts'].extend(dist.scripts or [])
364 data['packages'].extend(dist.packages or [])
365 data['modules'].extend(dist.py_modules or [])
366 # 2.1 data_files -> resources
367 if dist.data_files:
Éric Araujo8f66f612011-06-04 22:36:40 +0200368 if (len(dist.data_files) < 2 or
369 isinstance(dist.data_files[1], str)):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200370 dist.data_files = [('', dist.data_files)]
371 # add tokens in the destination paths
372 vars = {'distribution.name': data['name']}
373 path_tokens = list(sysconfig.get_paths(vars=vars).items())
374
Éric Araujo8f66f612011-06-04 22:36:40 +0200375 # TODO replace this with a key function
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200376 def length_comparison(x, y):
377 len_x = len(x[1])
378 len_y = len(y[1])
379 if len_x == len_y:
380 return 0
381 elif len_x < len_y:
382 return -1
383 else:
384 return 1
385
386 # sort tokens to use the longest one first
387 path_tokens.sort(key=cmp_to_key(length_comparison))
388 for dest, srcs in (dist.data_files or []):
389 dest = os.path.join(sys.prefix, dest)
Tarek Ziade2db56742011-05-21 14:24:14 +0200390 dest = dest.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200391 for tok, path in path_tokens:
Tarek Ziade2db56742011-05-21 14:24:14 +0200392 path = path.replace(os.path.sep, '/')
393 if not dest.startswith(path):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200394 continue
Tarek Ziade2db56742011-05-21 14:24:14 +0200395
396 dest = ('{%s}' % tok) + dest[len(path):]
397 files = [('/ '.join(src.rsplit('/', 1)), dest)
Éric Araujo8f66f612011-06-04 22:36:40 +0200398 for src in srcs]
Tarek Ziade2db56742011-05-21 14:24:14 +0200399 data['resources'].extend(files)
400
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200401 # 2.2 package_data -> extra_files
402 package_dirs = dist.package_dir or {}
Éric Araujo8f66f612011-06-04 22:36:40 +0200403 for package, extras in dist.package_data.items() or []:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200404 package_dir = package_dirs.get(package, package)
Tarek Ziade2db56742011-05-21 14:24:14 +0200405 for file_ in extras:
406 if package_dir:
407 file_ = package_dir + '/' + file_
408 data['extra_files'].append(file_)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200409
410 # Use README file if its content is the desciption
411 if "description" in data:
412 ref = md5(re.sub('\s', '',
413 self.data['description']).lower().encode())
414 ref = ref.digest()
415 for readme in glob.glob('README*'):
Victor Stinner35de5ac2011-05-19 15:09:57 +0200416 with open(readme, encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200417 contents = fp.read()
Victor Stinner35de5ac2011-05-19 15:09:57 +0200418 contents = re.sub('\s', '', contents.lower()).encode()
419 val = md5(contents).digest()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200420 if val == ref:
421 del data['description']
422 data['description-file'] = readme
423 break
424
425 # apply monkey patch to distutils (v1) and setuptools (if needed)
426 # (abort the feature if distutils v1 has been killed)
427 try:
428 from distutils import core
429 core.setup # make sure it's not d2 maskerading as d1
430 except (ImportError, AttributeError):
431 return
432 saved_setups = [(core, core.setup)]
433 core.setup = setup_mock
434 try:
435 import setuptools
436 except ImportError:
437 pass
438 else:
439 saved_setups.append((setuptools, setuptools.setup))
440 setuptools.setup = setup_mock
441 # get metadata by executing the setup.py with the patched setup(...)
442 success = False # for python < 2.4
443 try:
444 load_setup()
445 success = True
446 finally: # revert monkey patches
447 for patched_module, original_setup in saved_setups:
448 patched_module.setup = original_setup
449 if not self.data:
450 raise ValueError('Unable to load metadata from setup.py')
451 return success
452
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200453 def inspect(self):
454 """Inspect the current working diretory for a name and version.
455
456 This information is harvested in where the directory is named
457 like [name]-[version].
458 """
459 dir_name = os.path.basename(os.getcwd())
460 self.data['name'] = dir_name
461 match = re.match(r'(.*)-(\d.+)', dir_name)
462 if match:
463 self.data['name'] = match.group(1)
464 self.data['version'] = match.group(2)
Éric Araujo8f66f612011-06-04 22:36:40 +0200465 # TODO needs testing!
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200466 if not is_valid_version(self.data['version']):
467 msg = "Invalid version discovered: %s" % self.data['version']
Éric Araujo8f66f612011-06-04 22:36:40 +0200468 raise ValueError(msg)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200469
470 def query_user(self):
471 self.data['name'] = ask('Project name', self.data['name'],
472 _helptext['name'])
473
474 self.data['version'] = ask('Current version number',
475 self.data.get('version'), _helptext['version'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200476 self.data['summary'] = ask('Project description summary',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200477 self.data.get('summary'), _helptext['summary'],
478 lengthy=True)
479 self.data['author'] = ask('Author name',
480 self.data.get('author'), _helptext['author'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200481 self.data['author_email'] = ask('Author email address',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200482 self.data.get('author_email'), _helptext['author_email'])
Éric Araujo45593832011-06-04 22:37:57 +0200483 self.data['home_page'] = ask('Project home page',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200484 self.data.get('home_page'), _helptext['home_page'],
485 required=False)
486
487 if ask_yn('Do you want me to automatically build the file list '
Éric Araujo45593832011-06-04 22:37:57 +0200488 'with everything I can find in the current directory? '
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200489 'If you say no, you will have to define them manually.') == 'y':
490 self._find_files()
491 else:
Éric Araujo45593832011-06-04 22:37:57 +0200492 while ask_yn('Do you want to add a single module?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200493 ' (you will be able to add full packages next)',
494 helptext=_helptext['modules']) == 'y':
495 self._set_multi('Module name', 'modules')
496
Éric Araujo45593832011-06-04 22:37:57 +0200497 while ask_yn('Do you want to add a package?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200498 helptext=_helptext['packages']) == 'y':
499 self._set_multi('Package name', 'packages')
500
Éric Araujo45593832011-06-04 22:37:57 +0200501 while ask_yn('Do you want to add an extra file?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200502 helptext=_helptext['extra_files']) == 'y':
503 self._set_multi('Extra file/dir name', 'extra_files')
504
505 if ask_yn('Do you want to set Trove classifiers?',
506 helptext=_helptext['do_classifier']) == 'y':
507 self.set_classifier()
508
509 def _find_files(self):
510 # we are looking for python modules and packages,
511 # other stuff are added as regular files
512 pkgs = self.data['packages']
513 modules = self.data['modules']
514 extra_files = self.data['extra_files']
515
516 def is_package(path):
517 return os.path.exists(os.path.join(path, '__init__.py'))
518
519 curdir = os.getcwd()
520 scanned = []
521 _pref = ['lib', 'include', 'dist', 'build', '.', '~']
522 _suf = ['.pyc']
523
524 def to_skip(path):
525 path = relative(path)
526
527 for pref in _pref:
528 if path.startswith(pref):
529 return True
530
531 for suf in _suf:
532 if path.endswith(suf):
533 return True
534
535 return False
536
537 def relative(path):
538 return path[len(curdir) + 1:]
539
540 def dotted(path):
541 res = relative(path).replace(os.path.sep, '.')
542 if res.endswith('.py'):
543 res = res[:-len('.py')]
544 return res
545
546 # first pass: packages
547 for root, dirs, files in os.walk(curdir):
548 if to_skip(root):
549 continue
550 for dir_ in sorted(dirs):
551 if to_skip(dir_):
552 continue
553 fullpath = os.path.join(root, dir_)
554 dotted_name = dotted(fullpath)
555 if is_package(fullpath) and dotted_name not in pkgs:
556 pkgs.append(dotted_name)
557 scanned.append(fullpath)
558
559 # modules and extra files
560 for root, dirs, files in os.walk(curdir):
561 if to_skip(root):
562 continue
563
564 if any(root.startswith(path) for path in scanned):
565 continue
566
567 for file in sorted(files):
568 fullpath = os.path.join(root, file)
569 if to_skip(fullpath):
570 continue
571 # single module?
572 if os.path.splitext(file)[-1] == '.py':
573 modules.append(dotted(fullpath))
574 else:
575 extra_files.append(relative(fullpath))
576
577 def _set_multi(self, question, name):
578 existing_values = self.data[name]
579 value = ask(question, helptext=_helptext[name]).strip()
580 if value not in existing_values:
581 existing_values.append(value)
582
583 def set_classifier(self):
584 self.set_maturity_status(self.classifiers)
585 self.set_license(self.classifiers)
586 self.set_other_classifier(self.classifiers)
587
588 def set_other_classifier(self, classifiers):
Éric Araujo45593832011-06-04 22:37:57 +0200589 if ask_yn('Do you want to set other trove identifiers?', 'n',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200590 _helptext['trove_generic']) != 'y':
591 return
592 self.walk_classifiers(classifiers, [CLASSIFIERS], '')
593
594 def walk_classifiers(self, classifiers, trovepath, desc):
595 trove = trovepath[-1]
596
597 if not trove:
598 return
599
600 for key in sorted(trove):
601 if len(trove[key]) == 0:
602 if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
603 classifiers.add(desc[4:] + ' :: ' + key)
604 continue
605
Éric Araujo45593832011-06-04 22:37:57 +0200606 if ask_yn('Do you want to set items under\n "%s" (%d sub-items)?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200607 % (key, len(trove[key])), 'n',
608 _helptext['trove_generic']) == 'y':
609 self.walk_classifiers(classifiers, trovepath + [trove[key]],
610 desc + ' :: ' + key)
611
612 def set_license(self, classifiers):
613 while True:
Éric Araujo45593832011-06-04 22:37:57 +0200614 license = ask('What license do you use?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200615 helptext=_helptext['trove_license'], required=False)
616 if not license:
617 return
618
619 license_words = license.lower().split(' ')
620 found_list = []
621
622 for index, licence in LICENCES:
623 for word in license_words:
624 if word in licence:
625 found_list.append(index)
626 break
627
628 if len(found_list) == 0:
629 print('ERROR: Could not find a matching license for "%s"' %
630 license)
631 continue
632
633 question = 'Matching licenses:\n\n'
634
635 for index, list_index in enumerate(found_list):
636 question += ' %s) %s\n' % (index + 1,
637 _CLASSIFIERS_LIST[list_index])
638
639 question += ('\nType the number of the license you wish to use or '
640 '? to try again:')
641 choice = ask(question, required=False)
642
643 if choice == '?':
644 continue
645 if choice == '':
646 return
647
648 try:
649 index = found_list[int(choice) - 1]
650 except ValueError:
651 print("ERROR: Invalid selection, type a number from the list "
652 "above.")
653
654 classifiers.add(_CLASSIFIERS_LIST[index])
655
656 def set_maturity_status(self, classifiers):
657 maturity_name = lambda mat: mat.split('- ')[-1]
658 maturity_question = '''\
659 Please select the project status:
660
661 %s
662
663 Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
664 for i, n in enumerate(PROJECT_MATURITY))
665 while True:
666 choice = ask(dedent(maturity_question), required=False)
667
668 if choice:
669 try:
670 choice = int(choice) - 1
671 key = PROJECT_MATURITY[choice]
672 classifiers.add(key)
673 return
674 except (IndexError, ValueError):
675 print("ERROR: Invalid selection, type a single digit "
676 "number.")
677
678
679def main():
680 """Main entry point."""
681 program = MainProgram()
682 # # uncomment when implemented
683 # if not program.load_existing_setup_script():
684 # program.inspect_directory()
685 # program.query_user()
686 # program.update_config_file()
687 # program.write_setup_script()
688 # packaging.util.cfg_to_args()
689 program()
690
691
692if __name__ == '__main__':
693 main()