blob: 34a8c823b375e9a578d4144d1312c1e48d0f461e [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
Tarek Ziade1231a4e2011-05-19 13:07:25 +020028from hashlib import md5
Éric Araujo35a4d012011-06-04 22:24:59 +020029from textwrap import dedent
Éric Araujoc1b7e7f2011-09-18 23:12:30 +020030from tokenize import detect_encoding
Éric Araujo35a4d012011-06-04 22:24:59 +020031from configparser import RawConfigParser
Éric Araujoc1b7e7f2011-09-18 23:12:30 +020032
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:
Éric Araujoc1b7e7f2011-09-18 23:12:30 +0200115 encoding, lines = detect_encoding(f.readline)
Victor Stinner9cf6d132011-05-19 21:42:47 +0200116 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']}
Éric Araujof30b5ae2011-09-18 21:03:24 +0200373 path_tokens = sysconfig.get_paths(vars=vars).items()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200374 # sort tokens to use the longest one first
Éric Araujof30b5ae2011-09-18 21:03:24 +0200375 path_tokens = sorted(path_tokens, key=lambda x: len(x[1]))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200376 for dest, srcs in (dist.data_files or []):
377 dest = os.path.join(sys.prefix, dest)
Tarek Ziade2db56742011-05-21 14:24:14 +0200378 dest = dest.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200379 for tok, path in path_tokens:
Tarek Ziade2db56742011-05-21 14:24:14 +0200380 path = path.replace(os.path.sep, '/')
381 if not dest.startswith(path):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200382 continue
Tarek Ziade2db56742011-05-21 14:24:14 +0200383
384 dest = ('{%s}' % tok) + dest[len(path):]
385 files = [('/ '.join(src.rsplit('/', 1)), dest)
Éric Araujo8f66f612011-06-04 22:36:40 +0200386 for src in srcs]
Tarek Ziade2db56742011-05-21 14:24:14 +0200387 data['resources'].extend(files)
388
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200389 # 2.2 package_data -> extra_files
390 package_dirs = dist.package_dir or {}
Éric Araujo8f66f612011-06-04 22:36:40 +0200391 for package, extras in dist.package_data.items() or []:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200392 package_dir = package_dirs.get(package, package)
Tarek Ziade2db56742011-05-21 14:24:14 +0200393 for file_ in extras:
394 if package_dir:
395 file_ = package_dir + '/' + file_
396 data['extra_files'].append(file_)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200397
398 # Use README file if its content is the desciption
399 if "description" in data:
400 ref = md5(re.sub('\s', '',
401 self.data['description']).lower().encode())
402 ref = ref.digest()
403 for readme in glob.glob('README*'):
Victor Stinner35de5ac2011-05-19 15:09:57 +0200404 with open(readme, encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200405 contents = fp.read()
Victor Stinner35de5ac2011-05-19 15:09:57 +0200406 contents = re.sub('\s', '', contents.lower()).encode()
407 val = md5(contents).digest()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200408 if val == ref:
409 del data['description']
410 data['description-file'] = readme
411 break
412
413 # apply monkey patch to distutils (v1) and setuptools (if needed)
414 # (abort the feature if distutils v1 has been killed)
415 try:
416 from distutils import core
417 core.setup # make sure it's not d2 maskerading as d1
418 except (ImportError, AttributeError):
419 return
420 saved_setups = [(core, core.setup)]
421 core.setup = setup_mock
422 try:
423 import setuptools
424 except ImportError:
425 pass
426 else:
427 saved_setups.append((setuptools, setuptools.setup))
428 setuptools.setup = setup_mock
429 # get metadata by executing the setup.py with the patched setup(...)
430 success = False # for python < 2.4
431 try:
432 load_setup()
433 success = True
434 finally: # revert monkey patches
435 for patched_module, original_setup in saved_setups:
436 patched_module.setup = original_setup
437 if not self.data:
438 raise ValueError('Unable to load metadata from setup.py')
439 return success
440
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200441 def inspect(self):
442 """Inspect the current working diretory for a name and version.
443
444 This information is harvested in where the directory is named
445 like [name]-[version].
446 """
447 dir_name = os.path.basename(os.getcwd())
448 self.data['name'] = dir_name
449 match = re.match(r'(.*)-(\d.+)', dir_name)
450 if match:
451 self.data['name'] = match.group(1)
452 self.data['version'] = match.group(2)
Éric Araujo8f66f612011-06-04 22:36:40 +0200453 # TODO needs testing!
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200454 if not is_valid_version(self.data['version']):
455 msg = "Invalid version discovered: %s" % self.data['version']
Éric Araujo8f66f612011-06-04 22:36:40 +0200456 raise ValueError(msg)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200457
458 def query_user(self):
459 self.data['name'] = ask('Project name', self.data['name'],
460 _helptext['name'])
461
462 self.data['version'] = ask('Current version number',
463 self.data.get('version'), _helptext['version'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200464 self.data['summary'] = ask('Project description summary',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200465 self.data.get('summary'), _helptext['summary'],
466 lengthy=True)
467 self.data['author'] = ask('Author name',
468 self.data.get('author'), _helptext['author'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200469 self.data['author_email'] = ask('Author email address',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200470 self.data.get('author_email'), _helptext['author_email'])
Éric Araujo45593832011-06-04 22:37:57 +0200471 self.data['home_page'] = ask('Project home page',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200472 self.data.get('home_page'), _helptext['home_page'],
473 required=False)
474
475 if ask_yn('Do you want me to automatically build the file list '
Éric Araujo45593832011-06-04 22:37:57 +0200476 'with everything I can find in the current directory? '
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200477 'If you say no, you will have to define them manually.') == 'y':
478 self._find_files()
479 else:
Éric Araujo45593832011-06-04 22:37:57 +0200480 while ask_yn('Do you want to add a single module?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200481 ' (you will be able to add full packages next)',
482 helptext=_helptext['modules']) == 'y':
483 self._set_multi('Module name', 'modules')
484
Éric Araujo45593832011-06-04 22:37:57 +0200485 while ask_yn('Do you want to add a package?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200486 helptext=_helptext['packages']) == 'y':
487 self._set_multi('Package name', 'packages')
488
Éric Araujo45593832011-06-04 22:37:57 +0200489 while ask_yn('Do you want to add an extra file?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200490 helptext=_helptext['extra_files']) == 'y':
491 self._set_multi('Extra file/dir name', 'extra_files')
492
493 if ask_yn('Do you want to set Trove classifiers?',
494 helptext=_helptext['do_classifier']) == 'y':
495 self.set_classifier()
496
497 def _find_files(self):
498 # we are looking for python modules and packages,
499 # other stuff are added as regular files
500 pkgs = self.data['packages']
501 modules = self.data['modules']
502 extra_files = self.data['extra_files']
503
504 def is_package(path):
505 return os.path.exists(os.path.join(path, '__init__.py'))
506
507 curdir = os.getcwd()
508 scanned = []
509 _pref = ['lib', 'include', 'dist', 'build', '.', '~']
510 _suf = ['.pyc']
511
512 def to_skip(path):
513 path = relative(path)
514
515 for pref in _pref:
516 if path.startswith(pref):
517 return True
518
519 for suf in _suf:
520 if path.endswith(suf):
521 return True
522
523 return False
524
525 def relative(path):
526 return path[len(curdir) + 1:]
527
528 def dotted(path):
529 res = relative(path).replace(os.path.sep, '.')
530 if res.endswith('.py'):
531 res = res[:-len('.py')]
532 return res
533
534 # first pass: packages
535 for root, dirs, files in os.walk(curdir):
536 if to_skip(root):
537 continue
538 for dir_ in sorted(dirs):
539 if to_skip(dir_):
540 continue
541 fullpath = os.path.join(root, dir_)
542 dotted_name = dotted(fullpath)
543 if is_package(fullpath) and dotted_name not in pkgs:
544 pkgs.append(dotted_name)
545 scanned.append(fullpath)
546
547 # modules and extra files
548 for root, dirs, files in os.walk(curdir):
549 if to_skip(root):
550 continue
551
552 if any(root.startswith(path) for path in scanned):
553 continue
554
555 for file in sorted(files):
556 fullpath = os.path.join(root, file)
557 if to_skip(fullpath):
558 continue
559 # single module?
560 if os.path.splitext(file)[-1] == '.py':
561 modules.append(dotted(fullpath))
562 else:
563 extra_files.append(relative(fullpath))
564
565 def _set_multi(self, question, name):
566 existing_values = self.data[name]
567 value = ask(question, helptext=_helptext[name]).strip()
568 if value not in existing_values:
569 existing_values.append(value)
570
571 def set_classifier(self):
572 self.set_maturity_status(self.classifiers)
573 self.set_license(self.classifiers)
574 self.set_other_classifier(self.classifiers)
575
576 def set_other_classifier(self, classifiers):
Éric Araujo45593832011-06-04 22:37:57 +0200577 if ask_yn('Do you want to set other trove identifiers?', 'n',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200578 _helptext['trove_generic']) != 'y':
579 return
580 self.walk_classifiers(classifiers, [CLASSIFIERS], '')
581
582 def walk_classifiers(self, classifiers, trovepath, desc):
583 trove = trovepath[-1]
584
585 if not trove:
586 return
587
588 for key in sorted(trove):
589 if len(trove[key]) == 0:
590 if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
591 classifiers.add(desc[4:] + ' :: ' + key)
592 continue
593
Éric Araujo45593832011-06-04 22:37:57 +0200594 if ask_yn('Do you want to set items under\n "%s" (%d sub-items)?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200595 % (key, len(trove[key])), 'n',
596 _helptext['trove_generic']) == 'y':
597 self.walk_classifiers(classifiers, trovepath + [trove[key]],
598 desc + ' :: ' + key)
599
600 def set_license(self, classifiers):
601 while True:
Éric Araujo45593832011-06-04 22:37:57 +0200602 license = ask('What license do you use?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200603 helptext=_helptext['trove_license'], required=False)
604 if not license:
605 return
606
607 license_words = license.lower().split(' ')
608 found_list = []
609
610 for index, licence in LICENCES:
611 for word in license_words:
612 if word in licence:
613 found_list.append(index)
614 break
615
616 if len(found_list) == 0:
617 print('ERROR: Could not find a matching license for "%s"' %
618 license)
619 continue
620
621 question = 'Matching licenses:\n\n'
622
623 for index, list_index in enumerate(found_list):
624 question += ' %s) %s\n' % (index + 1,
625 _CLASSIFIERS_LIST[list_index])
626
627 question += ('\nType the number of the license you wish to use or '
628 '? to try again:')
629 choice = ask(question, required=False)
630
631 if choice == '?':
632 continue
633 if choice == '':
634 return
635
636 try:
637 index = found_list[int(choice) - 1]
638 except ValueError:
639 print("ERROR: Invalid selection, type a number from the list "
640 "above.")
641
642 classifiers.add(_CLASSIFIERS_LIST[index])
643
644 def set_maturity_status(self, classifiers):
645 maturity_name = lambda mat: mat.split('- ')[-1]
646 maturity_question = '''\
647 Please select the project status:
648
649 %s
650
651 Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
652 for i, n in enumerate(PROJECT_MATURITY))
653 while True:
654 choice = ask(dedent(maturity_question), required=False)
655
656 if choice:
657 try:
658 choice = int(choice) - 1
659 key = PROJECT_MATURITY[choice]
660 classifiers.add(key)
661 return
662 except (IndexError, ValueError):
663 print("ERROR: Invalid selection, type a single digit "
664 "number.")
665
666
667def main():
668 """Main entry point."""
669 program = MainProgram()
670 # # uncomment when implemented
671 # if not program.load_existing_setup_script():
672 # program.inspect_directory()
673 # program.query_user()
674 # program.update_config_file()
675 # program.write_setup_script()
676 # packaging.util.cfg_to_args()
677 program()
678
679
680if __name__ == '__main__':
681 main()