[Python-checkins] distutils2: merge with Alain
tarek.ziade
python-checkins at python.org
Sun Jan 30 10:43:58 CET 2011
tarek.ziade pushed 20b7b525914c to distutils2:
http://hg.python.org/distutils2/rev/20b7b525914c
changeset: 951:20b7b525914c
parent: 943:3bb80edf7d81
parent: 950:8f043681485c
user: Alexis Metaireau <alexis at notmyidea.org>
date: Sat Jan 29 18:07:02 2011 +0100
summary:
merge with Alain
files:
diff --git a/distutils2/mkcfg.py b/distutils2/mkcfg.py
--- a/distutils2/mkcfg.py
+++ b/distutils2/mkcfg.py
@@ -20,17 +20,27 @@
# Ask for the dependencies.
# Ask for the Requires-Dist
# Ask for the Provides-Dist
+# Ask for a description
# Detect scripts (not sure how. #! outside of package?)
import os
import sys
import re
import shutil
+import glob
+import re
from ConfigParser import RawConfigParser
from textwrap import dedent
+if sys.version_info[:2] < (2, 6):
+ from sets import Set as set
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
# importing this with an underscore as it should be replaced by the
# dict form or another structures for all purposes
from distutils2._trove import all_classifiers as _CLASSIFIERS_LIST
+from distutils2._backport import sysconfig
_FILENAME = 'setup.cfg'
@@ -82,6 +92,10 @@
Optionally, you can set other trove identifiers for things such as the
human language, programming language, user interface, etc...
''',
+ 'setup.py found':'''
+The setup.py script will be executed to retrieve the metadata.
+A wizard will be run if you answer "n",
+'''
}
# XXX everything needs docstrings and tests (both low-level tests of various
@@ -158,16 +172,18 @@
LICENCES = _build_licences(_CLASSIFIERS_LIST)
-
class MainProgram(object):
def __init__(self):
self.configparser = None
- self.classifiers = {}
+ self.classifiers = set([])
self.data = {}
self.data['classifier'] = self.classifiers
self.data['packages'] = []
self.data['modules'] = []
+ self.data['platform'] = []
+ self.data['resources'] = []
self.data['extra_files'] = []
+ self.data['scripts'] = []
self.load_config_file()
def lookup_option(self, key):
@@ -178,6 +194,7 @@
def load_config_file(self):
self.configparser = RawConfigParser()
# TODO replace with section in distutils config file
+ #XXX freedesktop
self.configparser.read(os.path.expanduser('~/.mkcfg'))
self.data['author'] = self.lookup_option('author')
self.data['author_email'] = self.lookup_option('author_email')
@@ -194,6 +211,7 @@
if not valuesDifferent:
return
+ #XXX freedesktop
fp = open(os.path.expanduser('~/.mkcfgpy'), 'w')
try:
self.configparser.write(fp)
@@ -201,19 +219,118 @@
fp.close()
def load_existing_setup_script(self):
- raise NotImplementedError
- # Ideas:
- # - define a mock module to assign to sys.modules['distutils'] before
- # importing the setup script as a module (or executing it); it would
- # provide setup (a function that just returns its args as a dict),
- # Extension (ditto), find_packages (the real function)
- # - we could even mock Distribution and commands to handle more setup
- # scripts
- # - we could use a sandbox (http://bugs.python.org/issue8680)
- # - the cleanest way is to parse the file, not import it, but there is
- # no way to do that across versions (the compiler package is
- # deprecated or removed in recent Pythons, the ast module is not
- # present before 2.6)
+ """ Generate a setup.cfg from an existing setup.py.
+
+ It only exports the distutils metadata (setuptools specific metadata
+ is not actually supported).
+ """
+ setuppath = 'setup.py'
+ if not os.path.exists(setuppath):
+ return
+ else:
+ ans = ask_yn(('A legacy setup.py has been found.\n'
+ 'Would you like to convert it to a setup.cfg ?'),
+ 'y',
+ _helptext['setup.py found'])
+ if ans != 'y':
+ return
+
+ #_______mock setup start
+ data = self.data
+ def setup(**attrs):
+ """Mock the setup(**attrs) in order to retrive metadata."""
+ # use the distutils v1 processings to correctly parse metadata.
+ #XXX we could also use the setuptools distibution ???
+ from distutils.dist import Distribution
+ dist = Distribution(attrs)
+ dist.parse_config_files()
+ # 1. retrieves metadata that are quite similar PEP314<->PEP345
+ labels = (('name',) * 2,
+ ('version',) * 2,
+ ('author',) * 2,
+ ('author_email',) * 2,
+ ('maintainer',) * 2,
+ ('maintainer_email',) * 2,
+ ('description', 'summary'),
+ ('long_description', 'description'),
+ ('url', 'home_page'),
+ ('platforms', 'platform'),
+ ('provides', 'provides-dist'),
+ ('obsoletes', 'obsoletes-dist'),
+ ('requires', 'requires-dist'),)
+ get = lambda lab: getattr(dist.metadata, lab.replace('-', '_'))
+ data.update((new, get(old)) for (old, new) in labels if get(old))
+ # 2. retrieves data that requires special processings.
+ data['classifier'].update(dist.get_classifiers() or [])
+ data['scripts'].extend(dist.scripts or [])
+ data['packages'].extend(dist.packages or [])
+ data['modules'].extend(dist.py_modules or [])
+ # 2.1 data_files -> resources.
+ if len(dist.data_files) < 2 or isinstance(dist.data_files[1], str):
+ dist.data_files = [('', dist.data_files)]
+ #add tokens in the destination paths
+ vars = {'distribution.name':data['name']}
+ path_tokens = sysconfig.get_paths(vars=vars).items()
+ #sort tokens to use the longest one first
+ path_tokens.sort(cmp=lambda x,y: cmp(len(y), len(x)),
+ key=lambda x: x[1])
+ for dest, srcs in (dist.data_files or []):
+ dest = os.path.join(sys.prefix, dest)
+ for tok, path in path_tokens:
+ if dest.startswith(path):
+ dest = ('{%s}' % tok) + dest[len(path):]
+ files = [('/ '.join(src.rsplit('/', 1)), dest)
+ for src in srcs]
+ data['resources'].extend(files)
+ continue
+ # 2.2 package_data -> extra_files
+ package_dirs = dist.package_dir or {}
+ for package, extras in dist.package_data.iteritems() or []:
+ package_dir = package_dirs.get(package, package)
+ fils = [os.path.join(package_dir, fil) for fil in extras]
+ data['extra_files'].extend(fils)
+
+ # Use README file if its content is the desciption
+ if "description" in data:
+ ref = md5(re.sub('\s', '', self.data['description']).lower())
+ ref = ref.digest()
+ for readme in glob.glob('README*'):
+ fob = open(readme)
+ val = md5(re.sub('\s', '', fob.read()).lower()).digest()
+ fob.close()
+ if val == ref:
+ del data['description']
+ data['description-file'] = readme
+ break
+ #_________ mock setup end
+
+ # apply monkey patch to distutils (v1) and setuptools (if needed)
+ # (abord the feature if distutils v1 has been killed)
+ try:
+ import distutils.core as DC
+ getattr(DC, 'setup') # ensure distutils v1
+ except ImportError, AttributeError:
+ return
+ saved_setups = [(DC, DC.setup)]
+ DC.setup = setup
+ try:
+ import setuptools
+ saved_setups.append((setuptools, setuptools.setup))
+ setuptools.setup = setup
+ except ImportError, AttributeError:
+ pass
+ # get metadata by executing the setup.py with the patched setup(...)
+ success = False # for python < 2.4
+ try:
+ pyenv = globals().copy()
+ execfile(setuppath, pyenv)
+ success = True
+ finally: #revert monkey patches
+ for patched_module, original_setup in saved_setups:
+ patched_module.setup = original_setup
+ if not self.data:
+ raise ValueError('Unable to load metadata from setup.py')
+ return success
def inspect_file(self, path):
fp = open(path, 'r')
@@ -222,9 +339,11 @@
m = re.match(r'^#!.*python((?P<major>\d)(\.\d+)?)?$', line)
if m:
if m.group('major') == '3':
- self.classifiers['Programming Language :: Python :: 3'] = 1
+ self.classifiers.add(
+ 'Programming Language :: Python :: 3')
else:
- self.classifiers['Programming Language :: Python :: 2'] = 1
+ self.classifiers.add(
+ 'Programming Language :: Python :: 2')
finally:
fp.close()
@@ -370,7 +489,7 @@
for key in sorted(trove):
if len(trove[key]) == 0:
if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y':
- classifiers[desc[4:] + ' :: ' + key] = 1
+ classifiers.add(desc[4:] + ' :: ' + key)
continue
if ask_yn('Do you want to set items under\n "%s" (%d sub-items)'
@@ -421,7 +540,7 @@
print ("ERROR: Invalid selection, type a number from the list "
"above.")
- classifiers[_CLASSIFIERS_LIST[index]] = 1
+ classifiers.add(_CLASSIFIERS_LIST[index])
return
def set_devel_status(self, classifiers):
@@ -448,7 +567,7 @@
'Development Status :: 5 - Production/Stable',
'Development Status :: 6 - Mature',
'Development Status :: 7 - Inactive'][choice]
- classifiers[key] = 1
+ classifiers.add(key)
return
except (IndexError, ValueError):
print ("ERROR: Invalid selection, type a single digit "
@@ -475,28 +594,39 @@
fp = open(_FILENAME, 'w')
try:
fp.write('[metadata]\n')
- fp.write('name = %s\n' % self.data['name'])
- fp.write('version = %s\n' % self.data['version'])
- fp.write('author = %s\n' % self.data['author'])
- fp.write('author_email = %s\n' % self.data['author_email'])
- fp.write('summary = %s\n' % self.data['summary'])
- fp.write('home_page = %s\n' % self.data['home_page'])
- fp.write('\n')
- if len(self.data['classifier']) > 0:
- classifiers = '\n'.join([' %s' % clas for clas in
- self.data['classifier']])
- fp.write('classifier = %s\n' % classifiers.strip())
- fp.write('\n')
-
- fp.write('[files]\n')
- for element in ('packages', 'modules', 'extra_files'):
- if len(self.data[element]) == 0:
+ # simple string entries
+ for name in ('name', 'version', 'summary', 'download_url'):
+ fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN')))
+ # optional string entries
+ if 'keywords' in self.data and self.data['keywords']:
+ fp.write('keywords = %s\n' % ' '.join(self.data['keywords']))
+ for name in ('home_page', 'author', 'author_email',
+ 'maintainer', 'maintainer_email', 'description-file'):
+ if name in self.data and self.data[name]:
+ fp.write('%s = %s\n' % (name, self.data[name]))
+ if 'description' in self.data:
+ fp.write(
+ 'description = %s\n'
+ % '\n |'.join(self.data['description'].split('\n')))
+ # multiple use string entries
+ for name in ('platform', 'supported-platform', 'classifier',
+ 'requires-dist', 'provides-dist', 'obsoletes-dist',
+ 'requires-external'):
+ if not(name in self.data and self.data[name]):
continue
- items = '\n'.join([' %s' % item for item in
- self.data[element]])
- fp.write('%s = %s\n' % (element, items.strip()))
-
- fp.write('\n')
+ fp.write('%s = ' % name)
+ fp.write(''.join(' %s\n' % val
+ for val in self.data[name]).lstrip())
+ fp.write('\n[files]\n')
+ for name in ('packages', 'modules', 'scripts',
+ 'package_data', 'extra_files'):
+ if not(name in self.data and self.data[name]):
+ continue
+ fp.write('%s = %s\n'
+ % (name, '\n '.join(self.data[name]).strip()))
+ fp.write('\n[resources]\n')
+ for src, dest in self.data['resources']:
+ fp.write('%s = %s\n' % (src, dest))
finally:
fp.close()
@@ -508,11 +638,12 @@
"""Main entry point."""
program = MainProgram()
# uncomment when implemented
- #program.load_existing_setup_script()
- program.inspect_directory()
- program.query_user()
- program.update_config_file()
+ if not program.load_existing_setup_script():
+ program.inspect_directory()
+ program.query_user()
+ program.update_config_file()
program.write_setup_script()
+ # istutils2.util.generate_distutils_setup_py()
if __name__ == '__main__':
diff --git a/distutils2/tests/test_mkcfg.py b/distutils2/tests/test_mkcfg.py
--- a/distutils2/tests/test_mkcfg.py
+++ b/distutils2/tests/test_mkcfg.py
@@ -1,10 +1,17 @@
+# -*- coding: utf-8 -*-
"""Tests for distutils.mkcfg."""
import os
+import os.path as osp
import sys
import StringIO
+if sys.version_info[:2] < (2, 6):
+ from sets import Set as set
+from textwrap import dedent
+
from distutils2.tests import run_unittest, support, unittest
from distutils2.mkcfg import MainProgram
-from distutils2.mkcfg import ask_yn, ask
+from distutils2.mkcfg import ask_yn, ask, main
+
class MkcfgTestCase(support.TempdirManager,
unittest.TestCase):
@@ -12,16 +19,20 @@
def setUp(self):
super(MkcfgTestCase, self).setUp()
self._stdin = sys.stdin
- self._stdout = sys.stdout
+ self._stdout = sys.stdout
sys.stdin = StringIO.StringIO()
sys.stdout = StringIO.StringIO()
-
+ self._cwd = os.getcwd()
+ self.wdir = self.mkdtemp()
+ os.chdir(self.wdir)
+
def tearDown(self):
super(MkcfgTestCase, self).tearDown()
sys.stdin = self._stdin
sys.stdout = self._stdout
-
- def test_ask_yn(self):
+ os.chdir(self._cwd)
+
+ def test_ask_yn(self):
sys.stdin.write('y\n')
sys.stdin.seek(0)
self.assertEqual('y', ask_yn('is this a test'))
@@ -40,13 +51,13 @@
main.data['author'] = []
main._set_multi('_set_multi test', 'author')
self.assertEqual(['aaaaa'], main.data['author'])
-
+
def test_find_files(self):
# making sure we scan a project dir correctly
main = MainProgram()
# building the structure
- tempdir = self.mkdtemp()
+ tempdir = self.wdir
dirs = ['pkg1', 'data', 'pkg2', 'pkg2/sub']
files = ['README', 'setup.cfg', 'foo.py',
'pkg1/__init__.py', 'pkg1/bar.py',
@@ -60,12 +71,7 @@
path = os.path.join(tempdir, file_)
self.write_file(path, 'xxx')
- old_dir = os.getcwd()
- os.chdir(tempdir)
- try:
- main._find_files()
- finally:
- os.chdir(old_dir)
+ main._find_files()
# do we have what we want ?
self.assertEqual(main.data['packages'], ['pkg1', 'pkg2', 'pkg2.sub'])
@@ -73,6 +79,131 @@
self.assertEqual(set(main.data['extra_files']),
set(['setup.cfg', 'README', 'data/data1']))
+ def test_convert_setup_py_to_cfg(self):
+ self.write_file((self.wdir, 'setup.py'),
+ dedent("""
+ # -*- coding: utf-8 -*-
+ from distutils.core import setup
+ lg_dsc = '''My super Death-scription
+ barbar is now on the public domain,
+ ho, baby !'''
+ setup(name='pyxfoil',
+ version='0.2',
+ description='Python bindings for the Xfoil engine',
+ long_description = lg_dsc,
+ maintainer='André Espaze',
+ maintainer_email='andre.espaze at logilab.fr',
+ url='http://www.python-science.org/project/pyxfoil',
+ license='GPLv2',
+ packages=['pyxfoil', 'babar', 'me'],
+ data_files=[('share/doc/pyxfoil', ['README.rst']),
+ ('share/man', ['pyxfoil.1']),
+ ],
+ py_modules = ['my_lib', 'mymodule'],
+ package_dir = {'babar' : '',
+ 'me' : 'Martinique/Lamentin',
+ },
+ package_data = {'babar': ['Pom', 'Flora', 'Alexander'],
+ 'me': ['dady', 'mumy', 'sys', 'bro'],
+ '': ['setup.py', 'README'],
+ 'pyxfoil' : ['fengine.so'],
+ },
+ scripts = ['my_script', 'bin/run'],
+ )
+ """))
+ sys.stdin.write('y\n')
+ sys.stdin.seek(0)
+ main()
+ fid = open(osp.join(self.wdir, 'setup.cfg'))
+ lines = set([line.rstrip() for line in fid])
+ fid.close()
+ self.assertEqual(lines, set(['',
+ '[metadata]',
+ 'version = 0.2',
+ 'name = pyxfoil',
+ 'maintainer = André Espaze',
+ 'description = My super Death-scription',
+ ' |barbar is now on the public domain,',
+ ' |ho, baby !',
+ 'maintainer_email = andre.espaze at logilab.fr',
+ 'home_page = http://www.python-science.org/project/pyxfoil',
+ 'download_url = UNKNOWN',
+ 'summary = Python bindings for the Xfoil engine',
+ '[files]',
+ 'modules = my_lib',
+ ' mymodule',
+ 'packages = pyxfoil',
+ ' babar',
+ ' me',
+ 'extra_files = Martinique/Lamentin/dady',
+ ' Martinique/Lamentin/mumy',
+ ' Martinique/Lamentin/sys',
+ ' Martinique/Lamentin/bro',
+ ' Pom',
+ ' Flora',
+ ' Alexander',
+ ' setup.py',
+ ' README',
+ ' pyxfoil/fengine.so',
+ 'scripts = my_script',
+ ' bin/run',
+ '[resources]',
+ 'README.rst = {doc}',
+ 'pyxfoil.1 = {man}',
+ ]))
+
+ def test_convert_setup_py_to_cfg_with_description_in_readme(self):
+ self.write_file((self.wdir, 'setup.py'),
+ dedent("""
+ # -*- coding: utf-8 -*-
+ from distutils.core import setup
+ lg_dsc = open('README.txt').read()
+ setup(name='pyxfoil',
+ version='0.2',
+ description='Python bindings for the Xfoil engine',
+ long_description=lg_dsc,
+ maintainer='André Espaze',
+ maintainer_email='andre.espaze at logilab.fr',
+ url='http://www.python-science.org/project/pyxfoil',
+ license='GPLv2',
+ packages=['pyxfoil'],
+ package_data={'pyxfoil' : ['fengine.so']},
+ data_files=[
+ ('share/doc/pyxfoil', ['README.rst']),
+ ('share/man', ['pyxfoil.1']),
+ ],
+ )
+ """))
+ self.write_file((self.wdir, 'README.txt'),
+ dedent('''
+My super Death-scription
+barbar is now on the public domain,
+ho, baby !
+ '''))
+ sys.stdin.write('y\n')
+ sys.stdin.seek(0)
+ main()
+ fid = open(osp.join(self.wdir, 'setup.cfg'))
+ lines = set([line.strip() for line in fid])
+ fid.close()
+ self.assertEqual(lines, set(['',
+ '[metadata]',
+ 'version = 0.2',
+ 'name = pyxfoil',
+ 'maintainer = André Espaze',
+ 'maintainer_email = andre.espaze at logilab.fr',
+ 'home_page = http://www.python-science.org/project/pyxfoil',
+ 'download_url = UNKNOWN',
+ 'summary = Python bindings for the Xfoil engine',
+ 'description-file = README.txt',
+ '[files]',
+ 'packages = pyxfoil',
+ 'extra_files = pyxfoil/fengine.so',
+ '[resources]',
+ 'README.rst = {doc}',
+ 'pyxfoil.1 = {man}',
+ ]))
+
def test_suite():
return unittest.makeSuite(MkcfgTestCase)
--
Repository URL: http://hg.python.org/distutils2
More information about the Python-checkins
mailing list