[Python-checkins] distutils2: Improve mkcfg script to recycle deprecated setup.py

tarek.ziade python-checkins at python.org
Sun Jan 30 10:43:58 CET 2011


tarek.ziade pushed 8f043681485c to distutils2:

http://hg.python.org/distutils2/rev/8f043681485c
changeset:   950:8f043681485c
parent:      896:272155a17d56
user:        Alain Leufroy <alain.leufroy at logilab.fr
date:        Sat Jan 29 17:07:03 2011 +0100
summary:
  Improve mkcfg script to recycle deprecated setup.py

- recycle setup.py (if found) to generate a setup.cfg

  Implements the distutils2.mkcfg.MainProgram.load_existing_setup_script(...) method
  that recycle the setup.py to generate a setup.cfg.
  Convert PEP314 -> PEP345

- complete PEP345 support when writing the new setup.cfg

Note:
  The wizard has not been improved.

files:
  distutils2/mkcfg.py
  distutils2/tests/test_mkcfg.py

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