[Python-checkins] distutils2: Merging the resource branch !

tarek.ziade python-checkins at python.org
Wed Feb 16 22:23:59 CET 2011


tarek.ziade pushed d4dad8855be3 to distutils2:

http://hg.python.org/distutils2/rev/d4dad8855be3
changeset:   1092:d4dad8855be3
parent:      1024:c3706e13ec0b
parent:      1091:db16e7c6b2ed
user:        Alexis Metaireau <alexis at notmyidea.org>
date:        Sun Feb 13 23:07:59 2011 +0000
summary:
  Merging the resource branch !

files:
  distutils2/command/install_data.py
  distutils2/command/install_dist.py
  distutils2/config.py
  distutils2/dist.py
  distutils2/index/simple.py
  distutils2/tests/test_command_sdist.py
  distutils2/tests/test_mkcfg.py
  distutils2/util.py

diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -15,3 +15,4 @@
 include
 bin
 nosetests.xml
+Distutils2.egg-info
diff --git a/distutils2/_backport/pkgutil.py b/distutils2/_backport/pkgutil.py
--- a/distutils2/_backport/pkgutil.py
+++ b/distutils2/_backport/pkgutil.py
@@ -1,13 +1,14 @@
 """Utilities to support packages."""
 
+import imp
+import sys
+
+from csv import reader as csv_reader
 import os
-import sys
-import imp
 import re
+from stat import ST_SIZE
+from types import ModuleType
 import warnings
-from csv import reader as csv_reader
-from types import ModuleType
-from stat import ST_SIZE
 
 try:
     from hashlib import md5
@@ -55,7 +56,7 @@
     """Make a trivial single-dispatch generic function"""
     registry = {}
 
-    def wrapper(*args, **kw):
+    def wrapper(*args, ** kw):
         ob = args[0]
         try:
             cls = ob.__class__
@@ -70,12 +71,12 @@
                     pass
                 mro = cls.__mro__[1:]
             except TypeError:
-                mro = object,   # must be an ExtensionClass or some such  :(
+                mro = object, # must be an ExtensionClass or some such  :(
         for t in mro:
             if t in registry:
-                return registry[t](*args, **kw)
+                return registry[t](*args, ** kw)
         else:
-            return func(*args, **kw)
+            return func(*args, ** kw)
     try:
         wrapper.__name__ = func.__name__
     except (TypeError, AttributeError):
@@ -620,7 +621,7 @@
 # PEP 376 Implementation #
 ##########################
 
-DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED',)
+DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED', 'RESOURCES')
 
 # Cache
 _cache_name = {}  # maps names to Distribution instances
@@ -659,7 +660,7 @@
 def clear_cache():
     """ Clears the internal cache. """
     global _cache_name, _cache_name_egg, _cache_path, _cache_path_egg, \
-           _cache_generated, _cache_generated_egg
+        _cache_generated, _cache_generated_egg
 
     _cache_name = {}
     _cache_name_egg = {}
@@ -749,8 +750,8 @@
         return '%s-%s at %s' % (self.name, self.metadata.version, self.path)
 
     def _get_records(self, local=False):
-        RECORD = os.path.join(self.path, 'RECORD')
-        record_reader = csv_reader(open(RECORD, 'rb'), delimiter=',')
+        RECORD = self.get_distinfo_file('RECORD')
+        record_reader = csv_reader(RECORD, delimiter=',')
         for row in record_reader:
             path, md5, size = row[:] + [None for i in xrange(len(row), 3)]
             if local:
@@ -758,6 +759,15 @@
                 path = os.path.join(sys.prefix, path)
             yield path, md5, size
 
+    def get_resource_path(self, relative_path):
+        resources_file = self.get_distinfo_file('RESOURCES')
+        resources_reader = csv_reader(resources_file, delimiter=',')
+        for relative, destination in resources_reader:
+            if relative == relative_path:
+                return destination
+        raise KeyError('No resource file with relative path %s were installed' %
+                       relative_path)
+
     def get_installed_files(self, local=False):
         """
         Iterates over the ``RECORD`` entries and returns a tuple
@@ -815,13 +825,13 @@
             distinfo_dirname, path = path.split(os.sep)[-2:]
             if distinfo_dirname != self.path.split(os.sep)[-1]:
                 raise DistutilsError("Requested dist-info file does not "
-                    "belong to the %s distribution. '%s' was requested." \
-                    % (self.name, os.sep.join([distinfo_dirname, path])))
+                                     "belong to the %s distribution. '%s' was requested." \
+                                     % (self.name, os.sep.join([distinfo_dirname, path])))
 
         # The file must be relative
         if path not in DIST_FILES:
             raise DistutilsError("Requested an invalid dist-info file: "
-                "%s" % path)
+                                 "%s" % path)
 
         # Convert the relative path back to absolute
         path = os.path.join(self.path, path)
@@ -860,11 +870,11 @@
     metadata = None
     """A :class:`distutils2.metadata.Metadata` instance loaded with
     the distribution's ``METADATA`` file."""
-    _REQUIREMENT = re.compile( \
-        r'(?P<name>[-A-Za-z0-9_.]+)\s*' \
-        r'(?P<first>(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)?\s*' \
-        r'(?P<rest>(?:\s*,\s*(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)*)\s*' \
-        r'(?P<extras>\[.*\])?')
+    _REQUIREMENT = re.compile(\
+                              r'(?P<name>[-A-Za-z0-9_.]+)\s*' \
+                              r'(?P<first>(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)?\s*' \
+                              r'(?P<rest>(?:\s*,\s*(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)*)\s*' \
+                              r'(?P<extras>\[.*\])?')
 
     def __init__(self, path, display_warnings=False):
         self.path = path
@@ -950,8 +960,8 @@
                     else:
                         if match.group('extras'):
                             s = (('Distribution %s uses extra requirements '
-                                  'which are not supported in distutils') \
-                                         % (self.name))
+                                 'which are not supported in distutils') \
+                                 % (self.name))
                             warnings.warn(s)
                         name = match.group('name')
                         version = None
@@ -1010,7 +1020,7 @@
 
     def __eq__(self, other):
         return isinstance(other, EggInfoDistribution) and \
-               self.path == other.path
+            self.path == other.path
 
     # See http://docs.python.org/reference/datamodel#object.__hash__
     __hash__ = object.__hash__
@@ -1069,7 +1079,7 @@
                 yield dist
 
 
-def get_distribution(name, use_egg_info=False, paths=sys.path):
+def get_distribution(name, use_egg_info=False, paths=None):
     """
     Scans all elements in ``sys.path`` and looks for all directories
     ending with ``.dist-info``. Returns a :class:`Distribution`
@@ -1086,6 +1096,9 @@
 
     :rtype: :class:`Distribution` or :class:`EggInfoDistribution` or None
     """
+    if paths == None:
+        paths = sys.path
+
     if not _cache_enabled:
         for dist in _yield_distributions(True, use_egg_info, paths):
             if dist.name == name:
@@ -1128,7 +1141,7 @@
                     predicate = VersionPredicate(obs)
                 except ValueError:
                     raise DistutilsError(('Distribution %s has ill formed' +
-                                          ' obsoletes field') % (dist.name,))
+                                         ' obsoletes field') % (dist.name,))
                 if name == o_components[0] and predicate.match(version):
                     yield dist
                     break
@@ -1174,8 +1187,8 @@
                 p_name, p_ver = p_components
                 if len(p_ver) < 2 or p_ver[0] != '(' or p_ver[-1] != ')':
                     raise DistutilsError(('Distribution %s has invalid ' +
-                                          'provides field: %s') \
-                                           % (dist.name, p))
+                                         'provides field: %s') \
+                                         % (dist.name, p))
                 p_ver = p_ver[1:-1]  # trim off the parenthesis
                 if p_name == name and predicate.match(p_ver):
                     yield dist
@@ -1195,3 +1208,15 @@
     for dist in get_distributions():
         if dist.uses(path):
             yield dist
+
+def resource_path(distribution_name, relative_path):
+    dist = get_distribution(distribution_name)
+    if dist != None:
+        return dist.get_resource_path(relative_path)
+    raise LookupError('No distribution named %s is installed.' %
+                      distribution_name)
+
+def resource_open(distribution_name, relative_path, * args, ** kwargs):
+    file = open(resource_path(distribution_name, relative_path), * args,
+                ** kwargs)
+    return file
\ No newline at end of file
diff --git a/distutils2/_backport/sysconfig.py b/distutils2/_backport/sysconfig.py
--- a/distutils2/_backport/sysconfig.py
+++ b/distutils2/_backport/sysconfig.py
@@ -120,6 +120,14 @@
         res[key] = os.path.normpath(_subst_vars(value, vars))
     return res
 
+def format_value(value, vars):
+    def _replacer(matchobj):
+         name = matchobj.group(1)
+         if name in vars:
+             return vars[name]
+         return matchobj.group(0)
+    return _VAR_REPL.sub(_replacer, value)
+ 
 
 def _get_default_scheme():
     if os.name == 'posix':
diff --git a/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/INSTALLER b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/INSTALLER
new file mode 100644
diff --git a/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/METADATA b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/METADATA
new file mode 100644
--- /dev/null
+++ b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/METADATA
@@ -0,0 +1,4 @@
+Metadata-version: 1.2
+Name: babar
+Version: 0.1
+Author: FELD Boris
\ No newline at end of file
diff --git a/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/RECORD b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/RECORD
new file mode 100644
diff --git a/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/REQUESTED b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/REQUESTED
new file mode 100644
diff --git a/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/RESOURCES b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/RESOURCES
new file mode 100644
--- /dev/null
+++ b/distutils2/_backport/tests/fake_dists/babar-0.1.dist-info/RESOURCES
@@ -0,0 +1,2 @@
+babar.png,babar.png
+babar.cfg,babar.cfg
\ No newline at end of file
diff --git a/distutils2/_backport/tests/fake_dists/babar.cfg b/distutils2/_backport/tests/fake_dists/babar.cfg
new file mode 100644
--- /dev/null
+++ b/distutils2/_backport/tests/fake_dists/babar.cfg
@@ -0,0 +1,1 @@
+Config
\ No newline at end of file
diff --git a/distutils2/_backport/tests/fake_dists/babar.png b/distutils2/_backport/tests/fake_dists/babar.png
new file mode 100644
diff --git a/distutils2/_backport/tests/test_pkgutil.py b/distutils2/_backport/tests/test_pkgutil.py
--- a/distutils2/_backport/tests/test_pkgutil.py
+++ b/distutils2/_backport/tests/test_pkgutil.py
@@ -1,11 +1,12 @@
 # -*- coding: utf-8 -*-
 """Tests for PEP 376 pkgutil functionality"""
+import imp
 import sys
+
+import csv
 import os
-import csv
-import imp
+import shutil
 import tempfile
-import shutil
 import zipfile
 try:
     from hashlib import md5
@@ -18,9 +19,9 @@
 
 from distutils2._backport import pkgutil
 from distutils2._backport.pkgutil import (
-    Distribution, EggInfoDistribution, get_distribution, get_distributions,
-    provides_distribution, obsoletes_distribution, get_file_users,
-    distinfo_dirname, _yield_distributions)
+                                          Distribution, EggInfoDistribution, get_distribution, get_distributions,
+                                          provides_distribution, obsoletes_distribution, get_file_users,
+                                          distinfo_dirname, _yield_distributions)
 
 try:
     from os.path import relpath
@@ -123,7 +124,6 @@
 
         del sys.modules[pkg]
 
-
 # Adapted from Python 2.7's trunk
 
 
@@ -182,7 +182,7 @@
     def setUp(self):
         super(TestPkgUtilDistribution, self).setUp()
         self.fake_dists_path = os.path.abspath(
-            os.path.join(os.path.dirname(__file__), 'fake_dists'))
+                                               os.path.join(os.path.dirname(__file__), 'fake_dists'))
         pkgutil.disable_cache()
 
         self.distinfo_dirs = [os.path.join(self.fake_dists_path, dir)
@@ -205,7 +205,7 @@
             # Setup the RECORD file for this dist
             record_file = os.path.join(distinfo_dir, 'RECORD')
             record_writer = csv.writer(open(record_file, 'w'), delimiter=',',
-                quoting=csv.QUOTE_NONE)
+                                       quoting=csv.QUOTE_NONE)
             dist_location = distinfo_dir.replace('.dist-info', '')
 
             for path, dirs, files in os.walk(dist_location):
@@ -214,15 +214,15 @@
                                            os.path.join(path, f)))
             for file in ['INSTALLER', 'METADATA', 'REQUESTED']:
                 record_writer.writerow(record_pieces(
-                    os.path.join(distinfo_dir, file)))
+                                       os.path.join(distinfo_dir, file)))
             record_writer.writerow([relpath(record_file, sys.prefix)])
             del record_writer  # causes the RECORD file to close
             record_reader = csv.reader(open(record_file, 'rb'))
             record_data = []
             for row in record_reader:
                 path, md5_, size = row[:] + \
-                                   [None for i in xrange(len(row), 3)]
-                record_data.append([path, (md5_, size,)])
+                    [None for i in xrange(len(row), 3)]
+                record_data.append([path, (md5_, size, )])
             self.records[distinfo_dir] = dict(record_data)
 
     def tearDown(self):
@@ -240,7 +240,7 @@
         name = 'choxie'
         version = '2.0.0.9'
         dist_path = os.path.join(here, 'fake_dists',
-            distinfo_dirname(name, version))
+                                 distinfo_dirname(name, version))
         dist = Distribution(dist_path)
 
         self.assertEqual(dist.name, name)
@@ -264,9 +264,9 @@
         # Criteria to test against
         distinfo_name = 'grammar-1.0a4'
         distinfo_dir = os.path.join(self.fake_dists_path,
-            distinfo_name + '.dist-info')
+                                    distinfo_name + '.dist-info')
         true_path = [self.fake_dists_path, distinfo_name, \
-                     'grammar', 'utils.py']
+            'grammar', 'utils.py']
         true_path = relpath(os.path.join(*true_path), sys.prefix)
         false_path = [self.fake_dists_path, 'towel_stuff-0.1', 'towel_stuff',
             '__init__.py']
@@ -282,7 +282,7 @@
         distinfo_name = 'choxie-2.0.0.9'
         other_distinfo_name = 'grammar-1.0a4'
         distinfo_dir = os.path.join(self.fake_dists_path,
-            distinfo_name + '.dist-info')
+                                    distinfo_name + '.dist-info')
         dist = Distribution(distinfo_dir)
         # Test for known good file matches
         distinfo_files = [
@@ -301,9 +301,9 @@
 
         # Test an absolute path that is part of another distributions dist-info
         other_distinfo_file = os.path.join(self.fake_dists_path,
-            other_distinfo_name + '.dist-info', 'REQUESTED')
+                                           other_distinfo_name + '.dist-info', 'REQUESTED')
         self.assertRaises(DistutilsError, dist.get_distinfo_file,
-            other_distinfo_file)
+                          other_distinfo_file)
         # Test for a file that does not exist and should not exist
         self.assertRaises(DistutilsError, dist.get_distinfo_file, \
                           'ENTRYPOINTS')
@@ -312,7 +312,7 @@
         # Test for the iteration of RECORD path entries.
         distinfo_name = 'towel_stuff-0.1'
         distinfo_dir = os.path.join(self.fake_dists_path,
-            distinfo_name + '.dist-info')
+                                    distinfo_name + '.dist-info')
         dist = Distribution(distinfo_dir)
         # Test for the iteration of the raw path
         distinfo_record_paths = self.records[distinfo_dir].keys()
@@ -324,6 +324,16 @@
         found = [path for path in dist.get_distinfo_files(local=True)]
         self.assertEqual(sorted(found), sorted(distinfo_record_paths))
 
+    def test_get_resources_path(self):
+        distinfo_name = 'babar-0.1'
+        distinfo_dir = os.path.join(self.fake_dists_path,
+                                    distinfo_name + '.dist-info')
+        dist = Distribution(distinfo_dir)
+        resource_path = dist.get_resource_path('babar.png')
+        self.assertEqual(resource_path, 'babar.png')
+        self.assertRaises(KeyError, dist.get_resource_path, 'notexist')
+
+
 
 class TestPkgUtilPEP376(support.LoggingCatcher, support.WarningsCatcher,
                         unittest.TestCase):
@@ -347,7 +357,7 @@
         # Given a name and a version, we expect the distinfo_dirname function
         # to return a standard distribution information directory name.
 
-        items = [  # (name, version, standard_dirname)
+        items = [# (name, version, standard_dirname)
             # Test for a very simple single word name and decimal
             # version number
             ('docutils', '0.5', 'docutils-0.5.dist-info'),
@@ -367,7 +377,7 @@
         # Lookup all distributions found in the ``sys.path``.
         # This test could potentially pick up other installed distributions
         fake_dists = [('grammar', '1.0a4'), ('choxie', '2.0.0.9'),
-            ('towel-stuff', '0.1')]
+                      ('towel-stuff', '0.1'), ('babar', '0.1')]
         found_dists = []
 
         # Verify the fake dists have been found.
@@ -375,10 +385,10 @@
         for dist in dists:
             if not isinstance(dist, Distribution):
                 self.fail("item received was not a Distribution instance: "
-                    "%s" % type(dist))
+                          "%s" % type(dist))
             if dist.name in dict(fake_dists) and \
-               dist.path.startswith(self.fake_dists_path):
-                found_dists.append((dist.name, dist.metadata['version'],))
+                dist.path.startswith(self.fake_dists_path):
+                    found_dists.append((dist.name, dist.metadata['version'], ))
             else:
                 # check that it doesn't find anything more than this
                 self.assertFalse(dist.path.startswith(self.fake_dists_path))
@@ -401,8 +411,8 @@
                 self.fail("item received was not a Distribution or "
                           "EggInfoDistribution instance: %s" % type(dist))
             if dist.name in dict(fake_dists) and \
-               dist.path.startswith(self.fake_dists_path):
-                found_dists.append((dist.name, dist.metadata['version']))
+                dist.path.startswith(self.fake_dists_path):
+                    found_dists.append((dist.name, dist.metadata['version']))
             else:
                 self.assertFalse(dist.path.startswith(self.fake_dists_path))
 
@@ -453,7 +463,7 @@
         # Test the iteration of distributions that use a file.
         name = 'towel_stuff-0.1'
         path = os.path.join(self.fake_dists_path, name,
-            'towel_stuff', '__init__.py')
+                            'towel_stuff', '__init__.py')
         for dist in get_file_users(path):
             self.assertTrue(isinstance(dist, Distribution))
             self.assertEqual(dist.name, name)
@@ -560,7 +570,7 @@
                 ('truffles', '5.0'), ('cheese', '2.0.2'),
                 ('coconuts-aster', '10.3'), ('nut', 'funkyversion')]
         dists = [('choxie', '2.0.0.9'), ('grammar', '1.0a4'),
-                 ('towel-stuff', '0.1')]
+                 ('towel-stuff', '0.1'), ('babar', '0.1')]
 
         checkLists([], _yield_distributions(False, False))
 
diff --git a/distutils2/command/install_data.py b/distutils2/command/install_data.py
--- a/distutils2/command/install_data.py
+++ b/distutils2/command/install_data.py
@@ -9,6 +9,8 @@
 import os
 from distutils2.command.cmd import Command
 from distutils2.util import change_root, convert_path
+from distutils2._backport.sysconfig import get_paths, format_value
+from distutils2._backport.shutil import Error
 
 class install_data(Command):
 
@@ -28,6 +30,7 @@
     def initialize_options(self):
         self.install_dir = None
         self.outfiles = []
+        self.data_files_out = []
         self.root = None
         self.force = 0
         self.data_files = self.distribution.data_files
@@ -40,54 +43,38 @@
 
     def run(self):
         self.mkpath(self.install_dir)
-        for f in self.data_files:
-            if isinstance(f, str):
-                # it's a simple file, so copy it
-                f = convert_path(f)
-                if self.warn_dir:
-                    self.warn("setup script did not provide a directory for "
-                              "'%s' -- installing right in '%s'" %
-                              (f, self.install_dir))
-                (out, _) = self.copy_file(f, self.install_dir)
-                self.outfiles.append(out)
-            else:
-                # it's a tuple with path to install to and a list of files
-                dir = convert_path(f[0])
-                if not os.path.isabs(dir):
-                    dir = os.path.join(self.install_dir, dir)
-                elif self.root:
-                    dir = change_root(self.root, dir)
-                self.mkpath(dir)
+        for file in self.data_files.items():
+            destination = convert_path(self.expand_categories(file[1]))
+            dir_dest = os.path.abspath(os.path.dirname(destination))
+            
+            self.mkpath(dir_dest)
+            try:
+                (out, _) = self.copy_file(file[0], dir_dest)
+            except Error, e:
+                self.warn(e.message)
+                out = destination
 
-                if f[1] == []:
-                    # If there are no files listed, the user must be
-                    # trying to create an empty directory, so add the
-                    # directory to the list of output files.
-                    self.outfiles.append(dir)
-                else:
-                    # Copy files, adding them to the list of output files.
-                    for data in f[1]:
-                        data = convert_path(data)
-                        (out, _) = self.copy_file(data, dir)
-                        self.outfiles.append(out)
+            self.outfiles.append(out)
+            self.data_files_out.append((file[0], destination))
+
+    def expand_categories(self, path_with_categories):
+        local_vars = get_paths()
+        local_vars['distribution.name'] = self.distribution.metadata['Name']
+        expanded_path = format_value(path_with_categories, local_vars)
+        expanded_path = format_value(expanded_path, local_vars)
+        if '{' in expanded_path and '}' in expanded_path:
+            self.warn("Unable to expand %s, some categories may missing." %
+                path_with_categories)
+        return expanded_path
 
     def get_source_files(self):
-        sources = []
-        for item in self.data_files:
-            if isinstance(item, str): # plain file
-                item = convert_path(item)
-                if os.path.isfile(item):
-                    sources.append(item)
-            else:    # a (dirname, filenames) tuple
-                dirname, filenames = item
-                for f in filenames:
-                    f = convert_path(f)
-                    if os.path.isfile(f):
-                        sources.append(f)
-        return sources
+        return self.data_files.keys()
 
     def get_inputs(self):
-        return self.data_files or []
+        return self.data_files.keys()
 
     def get_outputs(self):
         return self.outfiles
+
+    def get_resources_out(self):
+        return self.data_files_out
diff --git a/distutils2/command/install_dist.py b/distutils2/command/install_dist.py
--- a/distutils2/command/install_dist.py
+++ b/distutils2/command/install_dist.py
@@ -87,6 +87,8 @@
         ('record=', None,
          "filename in which to record a list of installed files "
          "(not PEP 376-compliant)"),
+        ('resources=', None,
+         "data files mapping"),
 
         # .dist-info related arguments, read by install_dist_info
         ('no-distinfo', None,
@@ -184,12 +186,14 @@
         #self.install_info = None
 
         self.record = None
+        self.resources = None
 
         # .dist-info related options
         self.no_distinfo = None
         self.installer = None
         self.requested = None
         self.no_record = None
+        self.no_resources = None
 
     # -- Option finalizing methods -------------------------------------
     # (This is rather more involved than for most commands,
diff --git a/distutils2/command/install_distinfo.py b/distutils2/command/install_distinfo.py
--- a/distutils2/command/install_distinfo.py
+++ b/distutils2/command/install_distinfo.py
@@ -12,12 +12,12 @@
 
 # This file was created from the code for the former command install_egg_info
 
-import os
 import csv
-import re
-from distutils2.command.cmd import Command
 from distutils2 import logger
 from distutils2._backport.shutil import rmtree
+from distutils2.command.cmd import Command
+import os
+import re
 try:
     import hashlib
 except ImportError:
@@ -39,9 +39,11 @@
          "do not generate a REQUESTED file"),
         ('no-record', None,
          "do not generate a RECORD file"),
+        ('no-resources', None,
+         "do not generate a RESSOURCES list installed file")
     ]
 
-    boolean_options = ['requested', 'no-record']
+    boolean_options = ['requested', 'no-record', 'no-resources']
 
     negative_opt = {'no-requested': 'requested'}
 
@@ -50,6 +52,7 @@
         self.installer = None
         self.requested = None
         self.no_record = None
+        self.no_resources = None
 
     def finalize_options(self):
         self.set_undefined_options('install_dist',
@@ -66,13 +69,16 @@
             self.requested = True
         if self.no_record is None:
             self.no_record = False
+        if self.no_resources is None:
+            self.no_resources = False
+
 
         metadata = self.distribution.metadata
 
         basename = "%s-%s.dist-info" % (
-            to_filename(safe_name(metadata['Name'])),
-            to_filename(safe_version(metadata['Version'])),
-        )
+                                        to_filename(safe_name(metadata['Name'])),
+                                        to_filename(safe_version(metadata['Version'])),
+                                        )
 
         self.distinfo_dir = os.path.join(self.distinfo_dir, basename)
         self.outputs = []
@@ -113,6 +119,25 @@
                 f.close()
                 self.outputs.append(requested_path)
 
+
+            if not self.no_resources:
+                install_data = self.get_finalized_command('install_data')
+                if install_data.get_resources_out() != []:
+                    resources_path = os.path.join(self.distinfo_dir,
+                                                  'RESOURCES')
+                    logger.info('creating %s', resources_path)
+                    f = open(resources_path, 'wb')
+                    try:
+                        writer = csv.writer(f, delimiter=',',
+                                            lineterminator=os.linesep,
+                                            quotechar='"')
+                        for tuple in install_data.get_resources_out():
+                            writer.writerow(tuple)
+
+                        self.outputs.append(resources_path)
+                    finally:
+                        f.close()
+
             if not self.no_record:
                 record_path = os.path.join(self.distinfo_dir, 'RECORD')
                 logger.info('creating %s', record_path)
@@ -142,6 +167,7 @@
                 finally:
                     f.close()
 
+
     def get_outputs(self):
         return self.outputs
 
diff --git a/distutils2/config.py b/distutils2/config.py
--- a/distutils2/config.py
+++ b/distutils2/config.py
@@ -2,6 +2,7 @@
 
     Know how to read all config files Distutils2 uses.
 """
+import os.path
 import os
 import sys
 import logging
@@ -14,6 +15,7 @@
 from distutils2.util import check_environ, resolve_name, strtobool
 from distutils2.compiler import set_compiler
 from distutils2.command import set_command
+from distutils2.resources import resources_dests
 from distutils2.markers import interpret
 
 
@@ -105,7 +107,8 @@
                 if v != '']
         return value
 
-    def _read_setup_cfg(self, parser):
+    def _read_setup_cfg(self, parser, cfg_filename):
+        cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
         content = {}
         for section in parser.sections():
             content[section] = dict(parser.items(section))
@@ -145,11 +148,12 @@
                     # concatenate each files
                     value = ''
                     for filename in filenames:
-                        f = open(filename)    # will raise if file not found
+                        # will raise if file not found
+                        description_file = open(filename)
                         try:
-                            value += f.read().strip() + '\n'
+                            value += description_file.read().strip() + '\n'
                         finally:
-                            f.close()
+                            description_file.close()
                         # add filename as a required file
                         if filename not in metadata.requires_files:
                             metadata.requires_files.append(filename)
@@ -189,22 +193,28 @@
             for data in files.get('package_data', []):
                 data = data.split('=')
                 if len(data) != 2:
-                    continue
+                    continue # XXX error should never pass silently
                 key, value = data
                 self.dist.package_data[key.strip()] = value.strip()
 
-            self.dist.data_files = []
-            for data in files.get('data_files', []):
-                data = data.split('=')
-                if len(data) != 2:
-                    continue
-                key, value = data
-                values = [v.strip() for v in value.split(',')]
-                self.dist.data_files.append((key, values))
-
             # manifest template
             self.dist.extra_files = files.get('extra_files', [])
 
+            resources = []
+            for rule in files.get('resources', []):
+                glob , destination  = rule.split('=', 1)
+                rich_glob = glob.strip().split(' ', 1)
+                if len(rich_glob) == 2:
+                    prefix, suffix = rich_glob
+                else:
+                    assert len(rich_glob) == 1
+                    prefix = ''
+                    suffix = glob
+                if destination == '<exclude>':
+                    destination = None
+                resources.append((prefix.strip(), suffix.strip(), destination.strip()))
+                self.dist.data_files = resources_dests(cfg_directory, resources)
+
         ext_modules = self.dist.ext_modules
         for section_key in content:
             labels = section_key.split('=')
@@ -232,7 +242,6 @@
                     **values_dct
                 ))
 
-
     def parse_config_files(self, filenames=None):
         if filenames is None:
             filenames = self.find_config_files()
@@ -246,7 +255,7 @@
             parser.read(filename)
 
             if os.path.split(filename)[-1] == 'setup.cfg':
-                self._read_setup_cfg(parser)
+                self._read_setup_cfg(parser, filename)
 
             for section in parser.sections():
                 if section == 'global':
diff --git a/distutils2/dist.py b/distutils2/dist.py
--- a/distutils2/dist.py
+++ b/distutils2/dist.py
@@ -191,7 +191,7 @@
         self.include_dirs = []
         self.extra_path = None
         self.scripts = []
-        self.data_files = []
+        self.data_files = {}
         self.password = ''
         self.use_2to3 = False
         self.convert_2to3_doctests = []
diff --git a/distutils2/mkcfg.py b/distutils2/mkcfg.py
--- a/distutils2/mkcfg.py
+++ b/distutils2/mkcfg.py
@@ -629,9 +629,11 @@
                     continue
                 fp.write('%s = %s\n'
                          % (name, '\n    '.join(self.data[name]).strip()))
-            fp.write('\n[resources]\n')
+            fp.write('\nresources =\n')
             for src, dest in self.data['resources']:
-                fp.write('%s = %s\n' % (src, dest))
+                fp.write('    %s = %s\n' % (src, dest))
+            fp.write('\n')
+
         finally:
             fp.close()
 
diff --git a/distutils2/resources.py b/distutils2/resources.py
new file mode 100644
--- /dev/null
+++ b/distutils2/resources.py
@@ -0,0 +1,22 @@
+import os
+from distutils2.util import iglob
+
+def _rel_path(base, path):
+    assert path.startswith(base)
+    return path[len(base):].lstrip('/')
+
+def resources_dests(resources_root, rules):
+    """find destination of ressources files"""
+    destinations = {}
+    for (base, suffix, dest) in rules:
+        prefix = os.path.join(resources_root, base)
+        for abs_base in iglob(prefix):
+            abs_glob = os.path.join(abs_base, suffix)
+            for abs_path in iglob(abs_glob):
+                resource_file = _rel_path(resources_root, abs_path)
+                if dest is None: #remove the entry if it was here
+                    destinations.pop(resource_file, None)
+                else:
+                    rel_path = _rel_path(abs_base, abs_path)
+                    destinations[resource_file] = os.path.join(dest, rel_path)
+    return destinations
diff --git a/distutils2/tests/test_command_install_data.py b/distutils2/tests/test_command_install_data.py
--- a/distutils2/tests/test_command_install_data.py
+++ b/distutils2/tests/test_command_install_data.py
@@ -1,4 +1,5 @@
 """Tests for distutils.command.install_data."""
+import cmd
 import os
 
 from distutils2.command.install_data import install_data
@@ -10,21 +11,29 @@
                           unittest.TestCase):
 
     def test_simple_run(self):
+        from distutils2._backport.sysconfig import _SCHEMES as sysconfig_SCHEMES
+        from distutils2._backport.sysconfig import _get_default_scheme
+            #dirty but hit marmoute
+
+        old_scheme = sysconfig_SCHEMES
+
         pkg_dir, dist = self.create_dist()
         cmd = install_data(dist)
         cmd.install_dir = inst = os.path.join(pkg_dir, 'inst')
 
-        # data_files can contain
-        #  - simple files
-        #  - a tuple with a path, and a list of file
+        sysconfig_SCHEMES.set(_get_default_scheme(), 'inst',
+            os.path.join(pkg_dir, 'inst'))
+        sysconfig_SCHEMES.set(_get_default_scheme(), 'inst2',
+            os.path.join(pkg_dir, 'inst2'))
+
         one = os.path.join(pkg_dir, 'one')
         self.write_file(one, 'xxx')
         inst2 = os.path.join(pkg_dir, 'inst2')
         two = os.path.join(pkg_dir, 'two')
         self.write_file(two, 'xxx')
 
-        cmd.data_files = [one, (inst2, [two])]
-        self.assertEqual(cmd.get_inputs(), [one, (inst2, [two])])
+        cmd.data_files = {one : '{inst}/one', two : '{inst2}/two'}
+        self.assertItemsEqual(cmd.get_inputs(), [one, two])
 
         # let's run the command
         cmd.ensure_finalized()
@@ -54,17 +63,22 @@
         inst4 = os.path.join(pkg_dir, 'inst4')
         three = os.path.join(cmd.install_dir, 'three')
         self.write_file(three, 'xx')
-        cmd.data_files = [one, (inst2, [two]),
-                          ('inst3', [three]),
-                          (inst4, [])]
+
+        sysconfig_SCHEMES.set(_get_default_scheme(), 'inst3', cmd.install_dir)
+
+        cmd.data_files = {one : '{inst}/one',
+                          two : '{inst2}/two',
+                          three : '{inst3}/three'}
         cmd.ensure_finalized()
         cmd.run()
 
         # let's check the result
-        self.assertEqual(len(cmd.get_outputs()), 4)
+        self.assertEqual(len(cmd.get_outputs()), 3)
         self.assertTrue(os.path.exists(os.path.join(inst2, rtwo)))
         self.assertTrue(os.path.exists(os.path.join(inst, rone)))
 
+        sysconfig_SCHEMES = old_scheme
+
 def test_suite():
     return unittest.makeSuite(InstallDataTestCase)
 
diff --git a/distutils2/tests/test_command_sdist.py b/distutils2/tests/test_command_sdist.py
--- a/distutils2/tests/test_command_sdist.py
+++ b/distutils2/tests/test_command_sdist.py
@@ -202,11 +202,10 @@
         self.write_file((some_dir, 'file.txt'), '#')
         self.write_file((some_dir, 'other_file.txt'), '#')
 
-        dist.data_files = [('data', ['data/data.dt',
-                                     'inroot.txt',
-                                     'notexisting']),
-                           'some/file.txt',
-                           'some/other_file.txt']
+        dist.data_files = {'data/data.dt' : '{appdata}/data.dt',
+                           'inroot.txt' : '{appdata}/inroot.txt',
+                           'some/file.txt' : '{appdata}/file.txt',
+                           'some/other_file.txt' : '{appdata}/other_file.txt'}
 
         # adding a script
         script_dir = join(self.tmp_dir, 'scripts')
diff --git a/distutils2/tests/test_config.py b/distutils2/tests/test_config.py
--- a/distutils2/tests/test_config.py
+++ b/distutils2/tests/test_config.py
@@ -65,11 +65,6 @@
 package_data =
   cheese = data/templates/*
 
-data_files =
-  bitmaps = bm/b1.gif, bm/b2.gif
-  config = cfg/data.cfg
-  /etc/init.d = init-script
-
 extra_files = %(extra-files)s
 
 # Replaces MANIFEST.in
@@ -78,6 +73,11 @@
   recursive-include examples *.txt *.py
   prune examples/sample?/build
 
+resources=
+  bm/ {b1,b2}.gif = {icon}
+  Cf*/ *.CFG = {config}/baBar/
+  init_script = {script}/JunGle/
+
 [global]
 commands =
     distutils2.tests.test_config.FooBarBazTest
@@ -193,6 +193,12 @@
     def test_config(self):
         self.write_setup()
         self.write_file('README', 'yeah')
+        os.mkdir('bm')
+        self.write_file(os.path.join('bm', 'b1.gif'), '')
+        self.write_file(os.path.join('bm', 'b2.gif'), '')
+        os.mkdir('Cfg')
+        self.write_file(os.path.join('Cfg', 'data.CFG'), '')
+        self.write_file('init_script', '')
 
         # try to load the metadata now
         dist = self.run_setup('--version')
@@ -237,10 +243,12 @@
         self.assertEqual(dist.packages, ['one', 'two', 'three'])
         self.assertEqual(dist.py_modules, ['haven'])
         self.assertEqual(dist.package_data, {'cheese': 'data/templates/*'})
-        self.assertEqual(dist.data_files,
-            [('bitmaps ', ['bm/b1.gif', 'bm/b2.gif']),
-             ('config ', ['cfg/data.cfg']),
-             ('/etc/init.d ', ['init-script'])])
+        self.assertEqual(
+            {'bm/b1.gif' : '{icon}/b1.gif',
+             'bm/b2.gif' : '{icon}/b2.gif',
+             'Cfg/data.CFG' : '{config}/baBar/data.CFG',
+             'init_script' : '{script}/JunGle/init_script'},
+             dist.data_files)
 
         self.assertEqual(dist.package_dir, 'src')
 
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
@@ -153,9 +153,9 @@
             '    pyxfoil/fengine.so',
             'scripts = my_script',
             '    bin/run',
-            '[resources]',
-            'README.rst = {doc}',
-            'pyxfoil.1 = {man}',
+            'resources =',
+            '    README.rst = {doc}',
+            '    pyxfoil.1 = {man}',
         ]))
 
     def test_convert_setup_py_to_cfg_with_description_in_readme(self):
@@ -173,7 +173,7 @@
               url='http://www.python-science.org/project/pyxfoil',
               license='GPLv2',
               packages=['pyxfoil'],
-              package_data={'pyxfoil' : ['fengine.so']},
+              package_data={'pyxfoil' : ['fengine.so', 'babar.so']},
               data_files=[
                 ('share/doc/pyxfoil', ['README.rst']),
                 ('share/man', ['pyxfoil.1']),
@@ -191,7 +191,7 @@
         main()
         fp = open(os.path.join(self.wdir, 'setup.cfg'))
         try:
-            lines = set([line.strip() for line in fp])
+            lines = set([line.rstrip() for line in fp])
         finally:
             fp.close()
         self.assertEqual(lines, set(['',
@@ -207,9 +207,10 @@
             '[files]',
             'packages = pyxfoil',
             'extra_files = pyxfoil/fengine.so',
-            '[resources]',
-            'README.rst = {doc}',
-            'pyxfoil.1 = {man}',
+            '    pyxfoil/babar.so',
+            'resources =',
+            '    README.rst = {doc}',
+            '    pyxfoil.1 = {man}',
         ]))
 
 
diff --git a/distutils2/tests/test_resources.py b/distutils2/tests/test_resources.py
new file mode 100644
--- /dev/null
+++ b/distutils2/tests/test_resources.py
@@ -0,0 +1,174 @@
+# -*- encoding: utf-8 -*-
+"""Tests for distutils.data."""
+import pkgutil
+import sys
+
+from distutils2._backport.pkgutil import resource_open
+from distutils2._backport.pkgutil import resource_path
+from distutils2._backport.pkgutil import disable_cache
+from distutils2._backport.pkgutil import enable_cache
+from distutils2.command.install_dist import install_dist
+from distutils2.resources import resources_dests
+from distutils2.tests import run_unittest
+from distutils2.tests import unittest
+from distutils2.tests.test_util import GlobTestCaseBase
+import os
+import tempfile
+
+
+class DataFilesTestCase(GlobTestCaseBase):
+
+    def assertRulesMatch(self, rules, spec):
+        tempdir = self.build_files_tree(spec)
+        expected = self.clean_tree(spec)
+        result = resources_dests(tempdir, rules)
+        self.assertEquals(expected, result)
+
+    def clean_tree(self, spec):
+        files = {}
+        for path, value in spec.items():
+            if value is not None:
+                path = self.os_dependant_path(path)
+                files[path] = value
+        return files
+
+    def test_simple_glob(self):
+        rules = [('', '*.tpl', '{data}')]
+        spec  = {'coucou.tpl': '{data}/coucou.tpl',
+            'Donotwant': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_multiple_match(self):
+        rules = [('scripts', '*.bin', '{appdata}'),
+            ('scripts', '*', '{appscript}')]
+        spec  = {'scripts/script.bin': '{appscript}/script.bin',
+            'Babarlikestrawberry': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_set_match(self):
+        rules = [('scripts', '*.{bin,sh}', '{appscript}')]
+        spec  = {'scripts/script.bin': '{appscript}/script.bin',
+            'scripts/babar.sh':  '{appscript}/babar.sh',
+            'Babarlikestrawberry': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_set_match_multiple(self):
+        rules = [('scripts', 'script{s,}.{bin,sh}', '{appscript}')]
+        spec  = {'scripts/scripts.bin': '{appscript}/scripts.bin',
+            'scripts/script.sh':  '{appscript}/script.sh',
+            'Babarlikestrawberry': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_set_match_exclude(self):
+        rules = [('scripts', '*', '{appscript}'),
+            ('', '**/*.sh', None)]
+        spec  = {'scripts/scripts.bin': '{appscript}/scripts.bin',
+            'scripts/script.sh':  None,
+            'Babarlikestrawberry': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_glob_in_base(self):
+        rules = [('scrip*', '*.bin', '{appscript}')]
+        spec  = {'scripts/scripts.bin': '{appscript}/scripts.bin',
+                 'scripouille/babar.bin': '{appscript}/babar.bin',
+                 'scriptortu/lotus.bin': '{appscript}/lotus.bin',
+                 'Babarlikestrawberry': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_recursive_glob(self):
+        rules = [('', '**/*.bin', '{binary}')]
+        spec  = {'binary0.bin': '{binary}/binary0.bin',
+            'scripts/binary1.bin': '{binary}/scripts/binary1.bin',
+            'scripts/bin/binary2.bin': '{binary}/scripts/bin/binary2.bin',
+            'you/kill/pandabear.guy': None}
+        self.assertRulesMatch(rules, spec)
+
+    def test_final_exemple_glob(self):
+        rules = [
+            ('mailman/database/schemas/', '*', '{appdata}/schemas'),
+            ('', '**/*.tpl', '{appdata}/templates'),
+            ('', 'developer-docs/**/*.txt', '{doc}'),
+            ('', 'README', '{doc}'),
+            ('mailman/etc/', '*', '{config}'),
+            ('mailman/foo/', '**/bar/*.cfg', '{config}/baz'),
+            ('mailman/foo/', '**/*.cfg', '{config}/hmm'),
+            ('', 'some-new-semantic.sns', '{funky-crazy-category}')
+        ]
+        spec = {
+            'README': '{doc}/README',
+            'some.tpl': '{appdata}/templates/some.tpl',
+            'some-new-semantic.sns': '{funky-crazy-category}/some-new-semantic.sns',
+            'mailman/database/mailman.db': None,
+            'mailman/database/schemas/blah.schema': '{appdata}/schemas/blah.schema',
+            'mailman/etc/my.cnf': '{config}/my.cnf',
+            'mailman/foo/some/path/bar/my.cfg': '{config}/hmm/some/path/bar/my.cfg',
+            'mailman/foo/some/path/other.cfg': '{config}/hmm/some/path/other.cfg',
+            'developer-docs/index.txt': '{doc}/developer-docs/index.txt',
+            'developer-docs/api/toc.txt': '{doc}/developer-docs/api/toc.txt',
+        }
+        self.maxDiff = None
+        self.assertRulesMatch(rules, spec)
+
+    def test_resource_open(self):
+
+
+        #Create a fake-dist
+        temp_site_packages = tempfile.mkdtemp()
+
+        dist_name = 'test'
+        dist_info = os.path.join(temp_site_packages, 'test-0.1.dist-info')
+        os.mkdir(dist_info)
+
+        metadata_path = os.path.join(dist_info, 'METADATA')
+        resources_path = os.path.join(dist_info, 'RESOURCES')
+
+        metadata_file = open(metadata_path, 'w')
+
+        metadata_file.write(
+"""Metadata-Version: 1.2
+Name: test
+Version: 0.1
+Summary: test
+Author: me
+        """)
+
+        metadata_file.close()
+
+        test_path = 'test.cfg'
+
+        _, test_resource_path = tempfile.mkstemp()
+
+        test_resource_file = open(test_resource_path, 'w')
+
+        content = 'Config'
+        test_resource_file.write(content)
+        test_resource_file.close()
+
+        resources_file = open(resources_path, 'w')
+
+        resources_file.write("""%s,%s""" % (test_path, test_resource_path))
+        resources_file.close()
+
+        #Add fake site-packages to sys.path to retrieve fake dist
+        old_sys_path = sys.path
+        sys.path.insert(0, temp_site_packages)
+
+        #Force pkgutil to rescan the sys.path
+        disable_cache()
+
+        #Try to retrieve resources paths and files
+        self.assertEqual(resource_path(dist_name, test_path), test_resource_path)
+        self.assertRaises(KeyError, resource_path, dist_name, 'notexis')
+
+        self.assertEqual(resource_open(dist_name, test_path).read(), content)
+        self.assertRaises(KeyError, resource_open, dist_name, 'notexis')
+
+        sys.path = old_sys_path
+
+        enable_cache()
+
+def test_suite():
+    return unittest.makeSuite(DataFilesTestCase)
+
+if __name__ == '__main__':
+    run_unittest(test_suite())
diff --git a/distutils2/tests/test_util.py b/distutils2/tests/test_util.py
--- a/distutils2/tests/test_util.py
+++ b/distutils2/tests/test_util.py
@@ -18,7 +18,7 @@
                              _find_exe_version, _MAC_OS_X_LD_VERSION,
                              byte_compile, find_packages, spawn, find_executable,
                              _nt_quote_args, get_pypirc_path, generate_pypirc,
-                             read_pypirc, resolve_name)
+                             read_pypirc, resolve_name, iglob, RICH_GLOB)
 
 from distutils2 import util
 from distutils2.tests import unittest, support
@@ -479,9 +479,186 @@
         content = open(rc).read()
         self.assertEqual(content, WANTED)
 
+class GlobTestCaseBase(support.TempdirManager,
+                       support.LoggingCatcher,
+                       unittest.TestCase):
+
+    def build_files_tree(self, files):
+        tempdir = self.mkdtemp()
+        for filepath in files:
+            is_dir = filepath.endswith('/')
+            filepath = os.path.join(tempdir, *filepath.split('/'))
+            if is_dir:
+                dirname = filepath
+            else:
+                dirname = os.path.dirname(filepath)
+            if dirname and not os.path.exists(dirname):
+                os.makedirs(dirname)
+            if not is_dir:
+                self.write_file(filepath, 'babar')
+        return tempdir
+
+    @staticmethod
+    def os_dependant_path(path):
+        path = path.rstrip('/').split('/')
+        return os.path.join(*path)
+
+    def clean_tree(self, spec):
+        files = []
+        for path, includes in list(spec.items()):
+            if includes:
+                files.append(self.os_dependant_path(path))
+        return files
+
+class GlobTestCase(GlobTestCaseBase):
+
+
+    def assertGlobMatch(self, glob, spec):
+        """"""
+        tempdir  = self.build_files_tree(spec)
+        expected = self.clean_tree(spec)
+        self.addCleanup(os.chdir, os.getcwd())
+        os.chdir(tempdir)
+        result = list(iglob(glob))
+        self.assertItemsEqual(expected, result)
+
+    def test_regex_rich_glob(self):
+        matches = RICH_GLOB.findall(r"babar aime les {fraises} est les {huitres}")
+        self.assertEquals(["fraises","huitres"], matches)
+
+    def test_simple_glob(self):
+        glob = '*.tp?'
+        spec  = {'coucou.tpl': True,
+                 'coucou.tpj': True,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_simple_glob_in_dir(self):
+        glob = 'babar/*.tp?'
+        spec  = {'babar/coucou.tpl': True,
+                 'babar/coucou.tpj': True,
+                 'babar/toto.bin': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_recursive_glob_head(self):
+        glob = '**/tip/*.t?l'
+        spec  = {'babar/zaza/zuzu/tip/coucou.tpl': True,
+                 'babar/z/tip/coucou.tpl': True,
+                 'babar/tip/coucou.tpl': True,
+                 'babar/zeop/tip/babar/babar.tpl': False,
+                 'babar/z/tip/coucou.bin': False,
+                 'babar/toto.bin': False,
+                 'zozo/zuzu/tip/babar.tpl': True,
+                 'zozo/tip/babar.tpl': True,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_recursive_glob_tail(self):
+        glob = 'babar/**'
+        spec = {'babar/zaza/': True,
+                'babar/zaza/zuzu/': True,
+                'babar/zaza/zuzu/babar.xml': True,
+                'babar/zaza/zuzu/toto.xml': True,
+                'babar/zaza/zuzu/toto.csv': True,
+                'babar/zaza/coucou.tpl': True,
+                'babar/bubu.tpl': True,
+                'zozo/zuzu/tip/babar.tpl': False,
+                'zozo/tip/babar.tpl': False,
+                'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_recursive_glob_middle(self):
+        glob = 'babar/**/tip/*.t?l'
+        spec  = {'babar/zaza/zuzu/tip/coucou.tpl': True,
+                 'babar/z/tip/coucou.tpl': True,
+                 'babar/tip/coucou.tpl': True,
+                 'babar/zeop/tip/babar/babar.tpl': False,
+                 'babar/z/tip/coucou.bin': False,
+                 'babar/toto.bin': False,
+                 'zozo/zuzu/tip/babar.tpl': False,
+                 'zozo/tip/babar.tpl': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_glob_set_tail(self):
+        glob = 'bin/*.{bin,sh,exe}'
+        spec  = {'bin/babar.bin': True,
+                 'bin/zephir.sh': True,
+                 'bin/celestine.exe': True,
+                 'bin/cornelius.bat': False,
+                 'bin/cornelius.xml': False,
+                 'toto/yurg': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_glob_set_middle(self):
+        glob = 'xml/{babar,toto}.xml'
+        spec  = {'xml/babar.xml': True,
+                 'xml/toto.xml': True,
+                 'xml/babar.xslt': False,
+                 'xml/cornelius.sgml': False,
+                 'xml/zephir.xml': False,
+                 'toto/yurg.xml': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_glob_set_head(self):
+        glob = '{xml,xslt}/babar.*'
+        spec  = {'xml/babar.xml': True,
+                 'xml/toto.xml': False,
+                 'xslt/babar.xslt': True,
+                 'xslt/toto.xslt': False,
+                 'toto/yurg.xml': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_glob_all(self):
+        glob = '{xml/*,xslt/**}/babar.xml'
+        spec  = {'xml/a/babar.xml': True,
+                 'xml/b/babar.xml': True,
+                 'xml/a/c/babar.xml': False,
+                 'xslt/a/babar.xml': True,
+                 'xslt/b/babar.xml': True,
+                 'xslt/a/c/babar.xml': True,
+                 'toto/yurg.xml': False,
+                 'Donotwant': False}
+        self.assertGlobMatch(glob, spec)
+
+    def test_invalid_glob_pattern(self):
+        invalids = [
+            'ppooa**',
+            'azzaeaz4**/',
+            '/**ddsfs',
+            '**##1e"&e',
+            'DSFb**c009',
+            '{'
+            '{aaQSDFa'
+            '}'
+            'aQSDFSaa}'
+            '{**a,'
+            ',**a}'
+            '{a**,'
+            ',b**}'
+            '{a**a,babar}'
+            '{bob,b**z}'
+            ]
+        msg = "%r is not supposed to be a valid pattern"
+        for pattern in invalids:
+            try:
+                iglob(pattern)
+            except ValueError:
+                continue
+            else:
+                self.fail("%r is not a valid iglob pattern" % pattern)
+
+
 
 def test_suite():
-    return unittest.makeSuite(UtilTestCase)
+    suite = unittest.makeSuite(UtilTestCase)
+    suite.addTest(unittest.makeSuite(GlobTestCase))
+    return suite
+
 
 if __name__ == "__main__":
     unittest.main(defaultTest="test_suite")
diff --git a/distutils2/util.py b/distutils2/util.py
--- a/distutils2/util.py
+++ b/distutils2/util.py
@@ -15,6 +15,10 @@
 from subprocess import call as sub_call
 from copy import copy
 from fnmatch import fnmatchcase
+try:
+    from glob import iglob as std_iglob
+except ImportError:
+    from glob import glob as std_iglob # for python < 2.5
 from ConfigParser import RawConfigParser
 from inspect import getsource
 
@@ -946,6 +950,47 @@
         return run_2to3(files, doctests_only, self.fixer_names,
                         self.options, self.explicit)
 
+RICH_GLOB = re.compile(r'\{([^}]*)\}')
+_CHECK_RECURSIVE_GLOB = re.compile(r'[^/,{]\*\*|\*\*[^/,}]')
+_CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$')
+
+def iglob(path_glob):
+    """Richer glob than the std glob module support ** and {opt1,opt2,opt3}"""
+    if _CHECK_RECURSIVE_GLOB.search(path_glob):
+        msg = """Invalid glob %r: Recursive glob "**" must be used alone"""
+        raise ValueError(msg % path_glob)
+    if _CHECK_MISMATCH_SET.search(path_glob):
+        msg = """Invalid glob %r: Mismatching set marker '{' or '}'"""
+        raise ValueError(msg % path_glob)
+    return _iglob(path_glob)
+
+
+def _iglob(path_glob):
+    """Actual logic of the iglob function"""
+    rich_path_glob = RICH_GLOB.split(path_glob, 1)
+    if len(rich_path_glob) > 1:
+        assert len(rich_path_glob) == 3, rich_path_glob
+        prefix, set, suffix = rich_path_glob
+        for item in set.split(','):
+            for path in _iglob( ''.join((prefix, item, suffix))):
+                yield path
+    else:
+        if '**' not in path_glob:
+            for item in std_iglob(path_glob):
+                yield item
+        else:
+            prefix, radical = path_glob.split('**', 1)
+            if prefix == '':
+                prefix = '.'
+            if radical == '':
+                radical = '*'
+            else:
+                radical = radical.lstrip('/')
+            for (path, dir, files) in os.walk(prefix):
+                path = os.path.normpath(path)
+                for file in _iglob(os.path.join(path, radical)):
+                   yield file
+
 
 def generate_distutils_kwargs_from_setup_cfg(file='setup.cfg'):
     """ Distutils2 to distutils1 compatibility util.
diff --git a/docs/design/wiki.rst b/docs/design/wiki.rst
--- a/docs/design/wiki.rst
+++ b/docs/design/wiki.rst
@@ -250,8 +250,8 @@
 ==  ====================================  ===================================================================================================
 1   mailman/database/schemas/blah.schema  /var/mailman/schemas/blah.schema
 2   some.tpl                              /var/mailman/templates/some.tpl
-3   path/to/some.tpl                      /var/mailman/templates/path/to/some.tpl
-4   mailman/database/mailman.db           /var/mailman/database/mailman.db
+3   path/to/some.tpl                      /var/mailman/templates/path/to/some.tpl !
+4   mailman/database/mailman.db           /var/mailman/database/mailman.db !
 5   developer-docs/index.txt              /usr/share/doc/mailman/developer-docs/index.txt
 6   developer-docs/api/toc.txt            /usr/share/doc/mailman/developer-docs/api/toc.txt
 7   README                                /usr/share/doc/mailman/README
@@ -259,7 +259,7 @@
 9   mailman/foo/some/path/bar/my.cfg      /etc/mailman/baz/some/path/bar/my.cfg AND
                                           /etc/mailman/hmm/some/path/bar/my.cfg + 
                                           emit a warning
-10  mailman/foo/some/path/other.cfg       /etc/mailman/some/path/other.cfg
+10  mailman/foo/some/path/other.cfg       /etc/mailman/some/path/other.cfg !
 11  some-new-semantic.sns                 /var/funky/mailman/some-new-semantic.sns
 ==  ====================================  ===================================================================================================
 
diff --git a/docs/source/setupcfg.rst b/docs/source/setupcfg.rst
--- a/docs/source/setupcfg.rst
+++ b/docs/source/setupcfg.rst
@@ -176,11 +176,360 @@
 .. Note::
     In Distutils2, setup.cfg will be implicitly included.
 
+Resources
+=========
+
+This section describes the files used by the project which must not be installed in the same place that python modules or libraries, they are called **resources**. They are for example documentation files, script files, databases, etc...
+
+For declaring resources, you must use this notation ::
+
+    source = destination
+
+Data-files are declared in the **resources** field in the **file** section, for example::
+
+    [files]
+    resources =
+        source1 = destination1
+        source2 = destination2
+
+The **source** part of the declaration are relative paths of resources files (using unix path separator **/**). For example, if you've this source tree::
+
+    foo/
+        doc/
+            doc.man
+        scripts/
+            foo.sh
+            
+Your setup.cfg will look like::
+
+    [files]
+    resources =
+        doc/doc.man = destination_doc
+        scripts/foo.sh = destination_scripts
+        
+The final paths where files will be placed are composed by : **source** + **destination**. In the previous example, **doc/doc.man** will be placed in **destination_doc/doc/doc.man** and **scripts/foo.sh** will be placed in **destination_scripts/scripts/foo.sh**. (If you want more control on the final path, take a look at base_prefix_).
+
+The **destination** part of resources declaration are paths with categories. Indeed, it's generally a bad idea to give absolute path as it will be cross incompatible. So, you must use resources categories in your **destination** declaration. Categories will be replaced by their real path at the installation time. Using categories is all benefit, your declaration will be simpler, cross platform and it will allow packager to place resources files where they want without breaking your code.
+
+Categories can be specified by using this syntax::
+
+    {category}
+    
+Default categories are::
+
+* config
+* appdata
+* appdata.arch
+* appdata.persistent
+* appdata.disposable
+* help
+* icon
+* scripts
+* doc
+* info
+* man
+
+A special category also exists **{distribution.name}** that will be replaced by the name of the distribution, but as most of the defaults categories use them, so it's not necessary to add **{distribution.name}** into your destination.
+
+If you use categories in your declarations, and you are encouraged to do, final path will be::
+
+    source + destination_expanded
+
+.. _example_final_path:
+
+For example, if you have this setup.cfg::
+
+    [metadata]
+    name = foo
+
+    [files]
+    resources =
+        doc/doc.man = {doc}
+
+And if **{doc}** is replaced by **{datadir}/doc/{distribution.name}**, final path will be::
+
+    {datadir}/doc/foo/doc/doc.man
+    
+Where {datafir} category will be platform-dependent.
+
+    
+More control on source part
+---------------------------
+
+Glob syntax
+___________
+
+When you declare source file, you can use a glob-like syntax to match multiples file, for example::
+
+    scripts/* = {script}
+    
+Will match all the files in the scripts directory and placed them in the script category.
+
+Glob tokens are:
+
+ * * : match all files.
+ * ? : match any character.
+ * ** : match any level of tree recursion (even 0).
+ * {} : will match any part separated by comma (example : {sh,bat}).
+ 
+TODO ::
+
+    Add an example
+    
+Order of declaration
+____________________
+
+The order of declaration is important if one file match multiple rules. The last rules matched by file is used, this is useful if you have this source tree::
+
+    foo/
+        doc/
+            index.rst
+            setup.rst
+            documentation.txt
+            doc.tex
+            README
+            
+And you want all the files in the doc directory to be placed in {doc} category, but README must be placed in {help} category, instead of listing all the files one by one, you can declare them in this way::
+
+    [files]
+    resources =
+        doc/* = {doc}
+        doc/README = {help}
+        
+Exclude
+_______
+
+You can exclude some files of resources declaration by giving no destination, it can be useful if you have a non-resources file in the same directory of resources files::
+
+    foo/
+        doc/
+           RELEASES
+           doc.tex
+           documentation.txt
+           docu.rst
+           
+Your **file** section will be::
+
+    [files]
+    resources =
+        doc/* = {doc}
+        doc/RELEASES =
+        
+More control on destination part
+--------------------------------  
+
+.. _base_prefix:
+
+Define a base-prefix
+____________________
+
+When you define your resources, you can have more control of how the final path is compute.
+
+By default, the final path is::
+
+    destination + source
+    
+This can generate long paths, for example (example_final_path_)::
+
+    {datadir}/doc/foo/doc/doc.man
+    
+When you declare your source, you can use a separator to split the source in **prefix** **suffix**. The supported separator are :
+
+ * Whitespace
+ 
+So, for example, if you have this source::
+
+    docs/ doc.man
+    
+The **prefix** is "docs/" and the **suffix** is "doc.html".
+
+.. note::
+
+    Separator can be placed after a path separator or replace it. So theses two sources are equivalent::
+    
+        docs/ doc.man
+        docs doc.man
+
+.. note::
+
+    Glob syntax is working the same way with standard source and splitted source. So theses rules::
+    
+        docs/*
+        docs/ *
+        docs *
+        
+    Will match all the files in the docs directory.
+    
+When you use splitted source, the final path is compute in this way::
+
+    destination + prefix
+    
+So for example, if you have this setup.cfg::
+
+    [metadata]
+    name = foo
+
+    [files]
+    resources =
+        doc/ doc.man = {doc}
+
+And if **{doc}** is replaced by **{datadir}/doc/{distribution.name}**, final path will be::
+
+    {datadir}/doc/foo/doc.man
+    
+    
+Overwrite paths for categories
+------------------------------
+
+.. warning::
+
+    This part is intended for system administrator or packager.
+    
+The real paths of categories are registered in the *sysconfig.cfg* file installed in your python installation. The format of this file is INI-like. The content of the file is  organized into several sections :
+
+ * globals : Standard categories's paths.
+ * posix_prefix : Standard paths for categories and installation paths for posix system.
+ * other one...
+ 
+Standard categories's paths are platform independent, they generally refers to other categories, which are platform dependent. Sysconfig module will choose these category from sections matching os.name. For example::
+
+    doc = {datadir}/doc/{distribution.name}
+
+It refers to datadir category, which can be different between platforms. In posix system, it may be::
+
+    datadir = /usr/share
+    
+So the final path will be::
+
+    doc = /usr/share/doc/{distribution.name}
+    
+The platform dependent categories are :
+ 
+ * confdir
+ * datadir
+ * libdir
+ * base
+
+Define extra-categories
+-----------------------
+
+Examples
+--------
+
+.. note::
+
+    These examples are incremental but works unitarily.
+
+Resources in root dir
+_____________________
+
+Source tree::
+
+  babar-1.0/
+    README
+    babar.sh
+    launch.sh
+    babar.py
+    
+Setup.cfg::
+
+    [files]
+    resources =
+        README = {doc}
+        *.sh = {scripts}
+  
+So babar.sh and launch.sh will be placed in {scripts} directory.
+
+Now let's move all the scripts into a scripts directory.
+
+Resources in sub-directory
+__________________________
+
+Source tree::
+
+  babar-1.1/
+    README
+    scripts/
+      babar.sh
+      launch.sh
+      LAUNCH
+    babar.py
+    
+Setup.cfg::
+
+    [files]
+    resources =
+        README = {doc}
+        scripts/ LAUNCH = {doc}
+        scripts/ *.sh = {scripts}
+  
+It's important to use the separator after scripts/ to install all the bash scripts into {scripts} instead of {scripts}/scripts.
+
+Now let's add some docs.
+
+Resources in multiple sub-directories
+_____________________________________
+
+Source tree::
+
+  babar-1.2/
+    README
+    scripts/
+      babar.sh
+      launch.sh
+      LAUNCH
+    docs/
+      api
+      man
+    babar.py
+
+Setup.cfg::
+
+   [files]
+   resources =
+        README = {doc}
+        scripts/ LAUNCH = {doc}
+        scripts/ *.sh = {scripts}
+        doc/ * = {doc}
+        doc/ man = {man}
+  
+You want to place all the file in the docs script into {doc} category, instead of man, which must be placed into {man} category, we will use the order of declaration of globs to choose the destination, the last glob that match the file is used.
+
+Now let's add some scripts for windows users.
+  
+Complete example
+________________
+
+Source tree::
+
+  babar-1.3/
+    README
+    doc/
+      api
+      man
+    scripts/  
+      babar.sh
+      launch.sh
+      babar.bat
+      launch.bat
+      LAUNCH
+
+Setup.cfg::
+
+    [files]
+    resources = 
+        README = {doc}
+        scripts/ LAUNCH = {doc}
+        scripts/ *.{sh,bat} = {scripts}
+        doc/ * = {doc}
+        doc/ man = {man}
+
+We use brace expansion syntax to place all the bash and batch scripts into {scripts} category.
+
 .. Warning::
     In Distutils2, setup.py and README (or README.txt) files are not more
     included in source distribution by default
 
-
 `command` sections
 ==================
 

--
Repository URL: http://hg.python.org/distutils2


More information about the Python-checkins mailing list