blob: 3d45ca9f90e0644217a096ab0d931deb1f3fd20a [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
Éric Araujofad46e12011-11-06 11:32:47 +010033from packaging import logger
Tarek Ziade1231a4e2011-05-19 13:07:25 +020034# importing this with an underscore as it should be replaced by the
35# dict form or another structures for all purposes
36from packaging._trove import all_classifiers as _CLASSIFIERS_LIST
37from packaging.version import is_valid_version
38
39_FILENAME = 'setup.cfg'
Éric Araujo95fc53f2011-09-01 05:11:29 +020040_DEFAULT_CFG = '.pypkgcreate' # FIXME use a section in user .pydistutils.cfg
Tarek Ziade1231a4e2011-05-19 13:07:25 +020041
42_helptext = {
43 'name': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020044The name of the project to be packaged, usually a single word composed
45of lower-case characters such as "zope.interface", "sqlalchemy" or
46"CherryPy".
Tarek Ziade1231a4e2011-05-19 13:07:25 +020047''',
48 'version': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020049Version number of the software, typically 2 or 3 numbers separated by
50dots such as "1.0", "0.6b3", or "3.2.1". "0.1.0" is recommended for
51initial development.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020052''',
53 'summary': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020054A one-line summary of what this project is or does, typically a sentence
5580 characters or less in length.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020056''',
57 'author': '''
58The full name of the author (typically you).
59''',
60 'author_email': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020061Email address of the project author.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020062''',
63 'do_classifier': '''
64Trove classifiers are optional identifiers that allow you to specify the
65intended audience by saying things like "Beta software with a text UI
Éric Araujo50e516a2011-08-19 00:56:57 +020066for Linux under the PSF license". However, this can be a somewhat
67involved process.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020068''',
69 'packages': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020070Python packages included in the project.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020071''',
72 'modules': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020073Pure Python modules included in the project.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020074''',
75 'extra_files': '''
76You can provide extra files/dirs contained in your project.
77It has to follow the template syntax. XXX add help here.
78''',
79
80 'home_page': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020081The home page for the project, typically a public Web page.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020082''',
83 'trove_license': '''
Éric Araujo50e516a2011-08-19 00:56:57 +020084Optionally you can specify a license. Type a string that identifies a
85common license, and then you can select a list of license specifiers.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020086''',
87 'trove_generic': '''
88Optionally, you can set other trove identifiers for things such as the
Éric Araujo50e516a2011-08-19 00:56:57 +020089human language, programming language, user interface, etc.
Tarek Ziade1231a4e2011-05-19 13:07:25 +020090''',
91 'setup.py found': '''
92The setup.py script will be executed to retrieve the metadata.
Éric Araujo45593832011-06-04 22:37:57 +020093An interactive helper will be run if you answer "n",
Tarek Ziade1231a4e2011-05-19 13:07:25 +020094''',
95}
96
97PROJECT_MATURITY = ['Development Status :: 1 - Planning',
98 'Development Status :: 2 - Pre-Alpha',
99 'Development Status :: 3 - Alpha',
100 'Development Status :: 4 - Beta',
101 'Development Status :: 5 - Production/Stable',
102 'Development Status :: 6 - Mature',
103 'Development Status :: 7 - Inactive']
104
105# XXX everything needs docstrings and tests (both low-level tests of various
106# methods and functional tests of running the script)
107
108
109def load_setup():
110 """run the setup script (i.e the setup.py file)
111
112 This function load the setup file in all cases (even if it have already
113 been loaded before, because we are monkey patching its setup function with
114 a particular one"""
Victor Stinner9cf6d132011-05-19 21:42:47 +0200115 with open("setup.py", "rb") as f:
Éric Araujoc1b7e7f2011-09-18 23:12:30 +0200116 encoding, lines = detect_encoding(f.readline)
Victor Stinner9cf6d132011-05-19 21:42:47 +0200117 with open("setup.py", encoding=encoding) as f:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200118 imp.load_module("setup", f, "setup.py", (".py", "r", imp.PY_SOURCE))
119
120
121def ask_yn(question, default=None, helptext=None):
122 question += ' (y/n)'
123 while True:
124 answer = ask(question, default, helptext, required=True)
Éric Araujo1cbd2ab2011-09-19 16:21:37 +0200125 if answer and answer[0].lower() in ('y', 'n'):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200126 return answer[0].lower()
127
Éric Araujofad46e12011-11-06 11:32:47 +0100128 logger.error('You must select "Y" or "N".')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200129
130
Éric Araujo95fc53f2011-09-01 05:11:29 +0200131# XXX use util.ask
132# FIXME: if prompt ends with '?', don't add ':'
133
134
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200135def ask(question, default=None, helptext=None, required=True,
136 lengthy=False, multiline=False):
137 prompt = '%s: ' % (question,)
138 if default:
139 prompt = '%s [%s]: ' % (question, default)
140 if default and len(question) + len(default) > 70:
141 prompt = '%s\n [%s]: ' % (question, default)
142 if lengthy or multiline:
143 prompt += '\n > '
144
145 if not helptext:
146 helptext = 'No additional help available.'
147
148 helptext = helptext.strip("\n")
149
150 while True:
Éric Araujofad46e12011-11-06 11:32:47 +0100151 line = input(prompt).strip()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200152 if line == '?':
153 print('=' * 70)
154 print(helptext)
155 print('=' * 70)
156 continue
157 if default and not line:
158 return default
159 if not line and required:
160 print('*' * 70)
161 print('This value cannot be empty.')
162 print('===========================')
163 if helptext:
164 print(helptext)
165 print('*' * 70)
166 continue
167 return line
168
169
170def convert_yn_to_bool(yn, yes=True, no=False):
171 """Convert a y/yes or n/no to a boolean value."""
172 if yn.lower().startswith('y'):
173 return yes
174 else:
175 return no
176
177
178def _build_classifiers_dict(classifiers):
179 d = {}
180 for key in classifiers:
Éric Araujodf8ef022011-06-08 04:47:13 +0200181 subdict = d
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200182 for subkey in key.split(' :: '):
Éric Araujodf8ef022011-06-08 04:47:13 +0200183 if subkey not in subdict:
184 subdict[subkey] = {}
185 subdict = subdict[subkey]
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200186 return d
187
188CLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST)
189
190
191def _build_licences(classifiers):
192 res = []
193 for index, item in enumerate(classifiers):
194 if not item.startswith('License :: '):
195 continue
196 res.append((index, item.split(' :: ')[-1].lower()))
197 return res
198
199LICENCES = _build_licences(_CLASSIFIERS_LIST)
200
201
202class MainProgram:
203 """Make a project setup configuration file (setup.cfg)."""
204
205 def __init__(self):
206 self.configparser = None
207 self.classifiers = set()
208 self.data = {'name': '',
209 'version': '1.0.0',
210 'classifier': self.classifiers,
211 'packages': [],
212 'modules': [],
213 'platform': [],
214 'resources': [],
215 'extra_files': [],
216 'scripts': [],
217 }
218 self._load_defaults()
219
220 def __call__(self):
221 setupcfg_defined = False
222 if self.has_setup_py() and self._prompt_user_for_conversion():
223 setupcfg_defined = self.convert_py_to_cfg()
224 if not setupcfg_defined:
225 self.define_cfg_values()
226 self._write_cfg()
227
228 def has_setup_py(self):
Éric Araujo35a4d012011-06-04 22:24:59 +0200229 """Test for the existence of a setup.py file."""
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200230 return os.path.exists('setup.py')
231
232 def define_cfg_values(self):
233 self.inspect()
234 self.query_user()
235
236 def _lookup_option(self, key):
237 if not self.configparser.has_option('DEFAULT', key):
238 return None
239 return self.configparser.get('DEFAULT', key)
240
241 def _load_defaults(self):
242 # Load default values from a user configuration file
243 self.configparser = RawConfigParser()
244 # TODO replace with section in distutils config file
245 default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG))
246 self.configparser.read(default_cfg)
247 self.data['author'] = self._lookup_option('author')
248 self.data['author_email'] = self._lookup_option('author_email')
249
250 def _prompt_user_for_conversion(self):
251 # Prompt the user about whether they would like to use the setup.py
252 # conversion utility to generate a setup.cfg or generate the setup.cfg
253 # from scratch
254 answer = ask_yn(('A legacy setup.py has been found.\n'
255 'Would you like to convert it to a setup.cfg?'),
256 default="y",
257 helptext=_helptext['setup.py found'])
258 return convert_yn_to_bool(answer)
259
260 def _dotted_packages(self, data):
261 packages = sorted(data)
262 modified_pkgs = []
263 for pkg in packages:
264 pkg = pkg.lstrip('./')
265 pkg = pkg.replace('/', '.')
266 modified_pkgs.append(pkg)
267 return modified_pkgs
268
269 def _write_cfg(self):
270 if os.path.exists(_FILENAME):
271 if os.path.exists('%s.old' % _FILENAME):
Éric Araujofad46e12011-11-06 11:32:47 +0100272 message = ("ERROR: %(name)s.old backup exists, please check "
273 "that current %(name)s is correct and remove "
274 "%(name)s.old" % {'name': _FILENAME})
275 logger.error(message)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200276 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']:
Éric Araujodcfcb642012-02-05 10:26:16 +0100290 # XXX shoud use comma to separate, not space
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200291 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())
Éric Araujodcfcb642012-02-05 10:26:16 +0100310
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200311 fp.write('\n[files]\n')
Éric Araujodcfcb642012-02-05 10:26:16 +0100312
313 for name in ('packages', 'modules', 'scripts', 'extra_files'):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200314 if not(name in self.data and self.data[name]):
315 continue
316 fp.write('%s = %s\n'
317 % (name, '\n '.join(self.data[name]).strip()))
Éric Araujodcfcb642012-02-05 10:26:16 +0100318
319 if self.data.get('package_data'):
320 fp.write('package_data =\n')
321 for pkg, spec in sorted(self.data['package_data'].items()):
322 # put one spec per line, indented under the package name
323 indent = ' ' * (len(pkg) + 7)
324 spec = ('\n' + indent).join(spec)
325 fp.write(' %s = %s\n' % (pkg, spec))
326 fp.write('\n')
327
328 if self.data.get('resources'):
329 fp.write('resources =\n')
330 for src, dest in self.data['resources']:
331 fp.write(' %s = %s\n' % (src, dest))
332 fp.write('\n')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200333
334 os.chmod(_FILENAME, 0o644)
Éric Araujofad46e12011-11-06 11:32:47 +0100335 logger.info('Wrote "%s".' % _FILENAME)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200336
337 def convert_py_to_cfg(self):
338 """Generate a setup.cfg from an existing setup.py.
339
340 It only exports the distutils metadata (setuptools specific metadata
341 is not currently supported).
342 """
343 data = self.data
344
345 def setup_mock(**attrs):
346 """Mock the setup(**attrs) in order to retrieve metadata."""
Éric Araujo8f66f612011-06-04 22:36:40 +0200347
348 # TODO use config and metadata instead of Distribution
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200349 from distutils.dist import Distribution
350 dist = Distribution(attrs)
351 dist.parse_config_files()
352
353 # 1. retrieve metadata fields that are quite similar in
354 # PEP 314 and PEP 345
355 labels = (('name',) * 2,
356 ('version',) * 2,
357 ('author',) * 2,
358 ('author_email',) * 2,
359 ('maintainer',) * 2,
360 ('maintainer_email',) * 2,
361 ('description', 'summary'),
362 ('long_description', 'description'),
363 ('url', 'home_page'),
364 ('platforms', 'platform'),
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200365 ('provides', 'provides-dist'),
366 ('obsoletes', 'obsoletes-dist'),
367 ('requires', 'requires-dist'))
368
369 get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
370 data.update((new, get(old)) for old, new in labels if get(old))
371
372 # 2. retrieve data that requires special processing
373 data['classifier'].update(dist.get_classifiers() or [])
374 data['scripts'].extend(dist.scripts or [])
375 data['packages'].extend(dist.packages or [])
376 data['modules'].extend(dist.py_modules or [])
377 # 2.1 data_files -> resources
378 if dist.data_files:
Éric Araujo8f66f612011-06-04 22:36:40 +0200379 if (len(dist.data_files) < 2 or
380 isinstance(dist.data_files[1], str)):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200381 dist.data_files = [('', dist.data_files)]
382 # add tokens in the destination paths
383 vars = {'distribution.name': data['name']}
Éric Araujof30b5ae2011-09-18 21:03:24 +0200384 path_tokens = sysconfig.get_paths(vars=vars).items()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200385 # sort tokens to use the longest one first
Éric Araujof30b5ae2011-09-18 21:03:24 +0200386 path_tokens = sorted(path_tokens, key=lambda x: len(x[1]))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200387 for dest, srcs in (dist.data_files or []):
388 dest = os.path.join(sys.prefix, dest)
Tarek Ziade2db56742011-05-21 14:24:14 +0200389 dest = dest.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200390 for tok, path in path_tokens:
Tarek Ziade2db56742011-05-21 14:24:14 +0200391 path = path.replace(os.path.sep, '/')
392 if not dest.startswith(path):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200393 continue
Tarek Ziade2db56742011-05-21 14:24:14 +0200394
395 dest = ('{%s}' % tok) + dest[len(path):]
396 files = [('/ '.join(src.rsplit('/', 1)), dest)
Éric Araujo8f66f612011-06-04 22:36:40 +0200397 for src in srcs]
Tarek Ziade2db56742011-05-21 14:24:14 +0200398 data['resources'].extend(files)
399
Éric Araujodcfcb642012-02-05 10:26:16 +0100400 # 2.2 package_data
401 data['package_data'] = dist.package_data.copy()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200402
403 # Use README file if its content is the desciption
404 if "description" in data:
405 ref = md5(re.sub('\s', '',
406 self.data['description']).lower().encode())
407 ref = ref.digest()
408 for readme in glob.glob('README*'):
Victor Stinner35de5ac2011-05-19 15:09:57 +0200409 with open(readme, encoding='utf-8') as fp:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200410 contents = fp.read()
Victor Stinner35de5ac2011-05-19 15:09:57 +0200411 contents = re.sub('\s', '', contents.lower()).encode()
412 val = md5(contents).digest()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200413 if val == ref:
414 del data['description']
415 data['description-file'] = readme
416 break
417
418 # apply monkey patch to distutils (v1) and setuptools (if needed)
419 # (abort the feature if distutils v1 has been killed)
420 try:
421 from distutils import core
422 core.setup # make sure it's not d2 maskerading as d1
423 except (ImportError, AttributeError):
424 return
425 saved_setups = [(core, core.setup)]
426 core.setup = setup_mock
427 try:
428 import setuptools
429 except ImportError:
430 pass
431 else:
432 saved_setups.append((setuptools, setuptools.setup))
433 setuptools.setup = setup_mock
434 # get metadata by executing the setup.py with the patched setup(...)
435 success = False # for python < 2.4
436 try:
437 load_setup()
438 success = True
439 finally: # revert monkey patches
440 for patched_module, original_setup in saved_setups:
441 patched_module.setup = original_setup
442 if not self.data:
443 raise ValueError('Unable to load metadata from setup.py')
444 return success
445
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200446 def inspect(self):
447 """Inspect the current working diretory for a name and version.
448
449 This information is harvested in where the directory is named
450 like [name]-[version].
451 """
452 dir_name = os.path.basename(os.getcwd())
453 self.data['name'] = dir_name
454 match = re.match(r'(.*)-(\d.+)', dir_name)
455 if match:
456 self.data['name'] = match.group(1)
457 self.data['version'] = match.group(2)
Éric Araujo8f66f612011-06-04 22:36:40 +0200458 # TODO needs testing!
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200459 if not is_valid_version(self.data['version']):
460 msg = "Invalid version discovered: %s" % self.data['version']
Éric Araujo8f66f612011-06-04 22:36:40 +0200461 raise ValueError(msg)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200462
463 def query_user(self):
464 self.data['name'] = ask('Project name', self.data['name'],
465 _helptext['name'])
466
467 self.data['version'] = ask('Current version number',
468 self.data.get('version'), _helptext['version'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200469 self.data['summary'] = ask('Project description summary',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200470 self.data.get('summary'), _helptext['summary'],
471 lengthy=True)
472 self.data['author'] = ask('Author name',
473 self.data.get('author'), _helptext['author'])
Éric Araujo50e516a2011-08-19 00:56:57 +0200474 self.data['author_email'] = ask('Author email address',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200475 self.data.get('author_email'), _helptext['author_email'])
Éric Araujo45593832011-06-04 22:37:57 +0200476 self.data['home_page'] = ask('Project home page',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200477 self.data.get('home_page'), _helptext['home_page'],
478 required=False)
479
480 if ask_yn('Do you want me to automatically build the file list '
Éric Araujo45593832011-06-04 22:37:57 +0200481 'with everything I can find in the current directory? '
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200482 'If you say no, you will have to define them manually.') == 'y':
483 self._find_files()
484 else:
Éric Araujo45593832011-06-04 22:37:57 +0200485 while ask_yn('Do you want to add a single module?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200486 ' (you will be able to add full packages next)',
487 helptext=_helptext['modules']) == 'y':
488 self._set_multi('Module name', 'modules')
489
Éric Araujo45593832011-06-04 22:37:57 +0200490 while ask_yn('Do you want to add a package?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200491 helptext=_helptext['packages']) == 'y':
492 self._set_multi('Package name', 'packages')
493
Éric Araujo45593832011-06-04 22:37:57 +0200494 while ask_yn('Do you want to add an extra file?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200495 helptext=_helptext['extra_files']) == 'y':
496 self._set_multi('Extra file/dir name', 'extra_files')
497
498 if ask_yn('Do you want to set Trove classifiers?',
499 helptext=_helptext['do_classifier']) == 'y':
500 self.set_classifier()
501
502 def _find_files(self):
503 # we are looking for python modules and packages,
504 # other stuff are added as regular files
505 pkgs = self.data['packages']
506 modules = self.data['modules']
507 extra_files = self.data['extra_files']
508
509 def is_package(path):
510 return os.path.exists(os.path.join(path, '__init__.py'))
511
512 curdir = os.getcwd()
513 scanned = []
514 _pref = ['lib', 'include', 'dist', 'build', '.', '~']
515 _suf = ['.pyc']
516
517 def to_skip(path):
518 path = relative(path)
519
520 for pref in _pref:
521 if path.startswith(pref):
522 return True
523
524 for suf in _suf:
525 if path.endswith(suf):
526 return True
527
528 return False
529
530 def relative(path):
531 return path[len(curdir) + 1:]
532
533 def dotted(path):
534 res = relative(path).replace(os.path.sep, '.')
535 if res.endswith('.py'):
536 res = res[:-len('.py')]
537 return res
538
539 # first pass: packages
540 for root, dirs, files in os.walk(curdir):
541 if to_skip(root):
542 continue
543 for dir_ in sorted(dirs):
544 if to_skip(dir_):
545 continue
546 fullpath = os.path.join(root, dir_)
547 dotted_name = dotted(fullpath)
548 if is_package(fullpath) and dotted_name not in pkgs:
549 pkgs.append(dotted_name)
550 scanned.append(fullpath)
551
552 # modules and extra files
553 for root, dirs, files in os.walk(curdir):
554 if to_skip(root):
555 continue
556
557 if any(root.startswith(path) for path in scanned):
558 continue
559
560 for file in sorted(files):
561 fullpath = os.path.join(root, file)
562 if to_skip(fullpath):
563 continue
564 # single module?
565 if os.path.splitext(file)[-1] == '.py':
566 modules.append(dotted(fullpath))
567 else:
568 extra_files.append(relative(fullpath))
569
570 def _set_multi(self, question, name):
571 existing_values = self.data[name]
572 value = ask(question, helptext=_helptext[name]).strip()
573 if value not in existing_values:
574 existing_values.append(value)
575
576 def set_classifier(self):
577 self.set_maturity_status(self.classifiers)
578 self.set_license(self.classifiers)
579 self.set_other_classifier(self.classifiers)
580
581 def set_other_classifier(self, classifiers):
Éric Araujo45593832011-06-04 22:37:57 +0200582 if ask_yn('Do you want to set other trove identifiers?', 'n',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200583 _helptext['trove_generic']) != 'y':
584 return
585 self.walk_classifiers(classifiers, [CLASSIFIERS], '')
586
587 def walk_classifiers(self, classifiers, trovepath, desc):
588 trove = trovepath[-1]
589
590 if not trove:
591 return
592
593 for key in sorted(trove):
594 if len(trove[key]) == 0:
595 if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
596 classifiers.add(desc[4:] + ' :: ' + key)
597 continue
598
Éric Araujo45593832011-06-04 22:37:57 +0200599 if ask_yn('Do you want to set items under\n "%s" (%d sub-items)?'
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200600 % (key, len(trove[key])), 'n',
601 _helptext['trove_generic']) == 'y':
602 self.walk_classifiers(classifiers, trovepath + [trove[key]],
603 desc + ' :: ' + key)
604
605 def set_license(self, classifiers):
606 while True:
Éric Araujo45593832011-06-04 22:37:57 +0200607 license = ask('What license do you use?',
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200608 helptext=_helptext['trove_license'], required=False)
609 if not license:
610 return
611
612 license_words = license.lower().split(' ')
613 found_list = []
614
615 for index, licence in LICENCES:
616 for word in license_words:
617 if word in licence:
618 found_list.append(index)
619 break
620
621 if len(found_list) == 0:
Éric Araujofad46e12011-11-06 11:32:47 +0100622 logger.error('Could not find a matching license for "%s"' %
623 license)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200624 continue
625
626 question = 'Matching licenses:\n\n'
627
628 for index, list_index in enumerate(found_list):
629 question += ' %s) %s\n' % (index + 1,
630 _CLASSIFIERS_LIST[list_index])
631
632 question += ('\nType the number of the license you wish to use or '
633 '? to try again:')
634 choice = ask(question, required=False)
635
636 if choice == '?':
637 continue
638 if choice == '':
639 return
640
641 try:
642 index = found_list[int(choice) - 1]
643 except ValueError:
Éric Araujofad46e12011-11-06 11:32:47 +0100644 logger.error(
645 "Invalid selection, type a number from the list above.")
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200646
647 classifiers.add(_CLASSIFIERS_LIST[index])
648
649 def set_maturity_status(self, classifiers):
650 maturity_name = lambda mat: mat.split('- ')[-1]
651 maturity_question = '''\
652 Please select the project status:
653
654 %s
655
656 Status''' % '\n'.join('%s - %s' % (i, maturity_name(n))
657 for i, n in enumerate(PROJECT_MATURITY))
658 while True:
659 choice = ask(dedent(maturity_question), required=False)
660
661 if choice:
662 try:
663 choice = int(choice) - 1
664 key = PROJECT_MATURITY[choice]
665 classifiers.add(key)
666 return
667 except (IndexError, ValueError):
Éric Araujofad46e12011-11-06 11:32:47 +0100668 logger.error(
669 "Invalid selection, type a single digit number.")
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200670
671
672def main():
673 """Main entry point."""
674 program = MainProgram()
675 # # uncomment when implemented
676 # if not program.load_existing_setup_script():
677 # program.inspect_directory()
678 # program.query_user()
679 # program.update_config_file()
680 # program.write_setup_script()
681 # packaging.util.cfg_to_args()
682 program()