Commits

Tarek Ziadé committed 113ea40

refactored DistributionMetadata -- cleaner implementation

  • Participants
  • Parent commits 17f69a0

Comments (0)

Files changed (14)

src/distutils2/command/check.py

 from distutils2.core import Command
 from distutils2.errors import DistutilsSetupError
 
-try:
-    # docutils is installed
-    from docutils.utils import Reporter
-    from docutils.parsers.rst import Parser
-    from docutils import frontend
-    from docutils import nodes
-    from StringIO import StringIO
-
-    class SilentReporter(Reporter):
-
-        def __init__(self, source, report_level, halt_level, stream=None,
-                     debug=0, encoding='ascii', error_handler='replace'):
-            self.messages = []
-            Reporter.__init__(self, source, report_level, halt_level, stream,
-                              debug, encoding, error_handler)
-
-        def system_message(self, level, message, *children, **kwargs):
-            self.messages.append((level, message, children, kwargs))
-
-    HAS_DOCUTILS = True
-except ImportError:
-    # docutils is not installed
-    HAS_DOCUTILS = False
-
 class check(Command):
     """This command checks the meta-data of the package.
     """
         if self.metadata:
             self.check_metadata()
         if self.restructuredtext:
-            if HAS_DOCUTILS:
+            if self.distribution.metadata.docutils_support:
                 self.check_restructuredtext()
             elif self.strict:
                 raise DistutilsSetupError('The docutils package is needed.')
 
         Warns if any are missing.
         """
-        metadata = self.distribution.metadata
-
-        missing = []
-        for attr in ('name', 'version', 'url'):
-            if not (hasattr(metadata, attr) and getattr(metadata, attr)):
-                missing.append(attr)
-
-        if missing:
+        missing, __ = self.distribution.metadata.check()
+        if missing != []:
             self.warn("missing required meta-data: %s"  % ', '.join(missing))
-        if metadata.author:
-            if not metadata.author_email:
-                self.warn("missing meta-data: if 'author' supplied, " +
-                          "'author_email' must be supplied too")
-        elif metadata.maintainer:
-            if not metadata.maintainer_email:
-                self.warn("missing meta-data: if 'maintainer' supplied, " +
-                          "'maintainer_email' must be supplied too")
-        else:
-            self.warn("missing meta-data: either (author and author_email) " +
-                      "or (maintainer and maintainer_email) " +
-                      "must be supplied")
 
     def check_restructuredtext(self):
         """Checks if the long string fields are reST-compliant."""
-        data = self.distribution.get_long_description()
-        for warning in self._check_rst_data(data):
+        missing, warnings = self.distribution.metadata.check()
+        for warning in warnings:
             line = warning[-1].get('line')
             if line is None:
                 warning = warning[1]
                 warning = '%s (line %s)' % (warning[1], line)
             self.warn(warning)
 
-    def _check_rst_data(self, data):
-        """Returns warnings when the provided data doesn't compile."""
-        source_path = StringIO()
-        parser = Parser()
-        settings = frontend.OptionParser().get_default_values()
-        settings.tab_width = 4
-        settings.pep_references = None
-        settings.rfc_references = None
-        reporter = SilentReporter(source_path,
-                          settings.report_level,
-                          settings.halt_level,
-                          stream=settings.warning_stream,
-                          debug=settings.debug,
-                          encoding=settings.error_encoding,
-                          error_handler=settings.error_encoding_error_handler)
-
-        document = nodes.document(settings, reporter, source=source_path)
-        document.note_source(source_path, -1)
-        try:
-            parser.parse(data, document)
-        except AttributeError:
-            reporter.messages.append((-1, 'Could not finish the parsing.',
-                                      '', {}))
-
-        return reporter.messages

src/distutils2/command/install.py

         prefix, exec_prefix, srcdir = get_config_vars('prefix', 'exec_prefix',
                                                       'srcdir')
 
-        self.config_vars = {'dist_name': self.distribution.get_name(),
-                            'dist_version': self.distribution.get_version(),
-                            'dist_fullname': self.distribution.get_fullname(),
+        metadata = self.distribution.metadata
+        self.config_vars = {'dist_name': metadata['Name'],
+                            'dist_version': metadata['Version'],
+                            'dist_fullname': metadata.get_fullname(),
                             'py_version': py_version,
                             'py_version_short': py_version[0:3],
                             'py_version_nodot': py_version[0] + py_version[2],
         for key, value in scheme.items():
             if key == 'platinclude':
                 key = 'headers'
-                value = os.path.join(value, self.distribution.get_name())
+                value = os.path.join(value, self.distribution.metadata['Name'])
             attrname = 'install_' + key
             if hasattr(self, attrname):
                 if getattr(self, attrname) is None:

src/distutils2/command/install_egg_info.py

         self.install_dir = None
 
     def finalize_options(self):
+        metadata = self.distribution.metadata
         self.set_undefined_options('install_lib',('install_dir','install_dir'))
         basename = "%s-%s-py%s.egg-info" % (
-            to_filename(safe_name(self.distribution.get_name())),
-            to_filename(safe_version(self.distribution.get_version())),
+            to_filename(safe_name(metadata['Name'])),
+            to_filename(safe_version(metadata['Version'])),
             sys.version[:3]
         )
         self.target = os.path.join(self.install_dir, basename)
         log.info("Writing %s", target)
         if not self.dry_run:
             f = open(target, 'w')
-            self.distribution.metadata.write_pkg_file(f)
+            self.distribution.metadata.write_file(f)
             f.close()
 
     def get_outputs(self):

src/distutils2/command/register.py

         meta = self.distribution.metadata
         data = {
             ':action': action,
+            # XXX implement 1.1
             'metadata_version' : '1.0',
-            'name': meta.get_name(),
-            'version': meta.get_version(),
-            'summary': meta.get_description(),
-            'home_page': meta.get_url(),
-            'author': meta.get_contact(),
-            'author_email': meta.get_contact_email(),
-            'license': meta.get_licence(),
-            'description': meta.get_long_description(),
-            'keywords': meta.get_keywords(),
-            'platform': meta.get_platforms(),
-            'classifiers': meta.get_classifiers(),
-            'download_url': meta.get_download_url(),
-            # PEP 314
-            'provides': meta.get_provides(),
-            'requires': meta.get_requires(),
-            'obsoletes': meta.get_obsoletes(),
+            'name': meta['Name'],
+            'version': meta['Version'],
+            'summary': meta['Summary'],
+            'home_page': meta['Home-page'],
+            'author': meta['Author'],
+            'author_email': meta['Author-email'],
+            'license': meta['License'],
+            'description': meta['Description'],
+            'keywords': meta['Keywords'],
+            'platform': meta['Platform'],
+            'classifiers': meta['Classifier'],
+            'download_url': meta['Download-URL'],
+            #'provides': meta['Provides'],
+            #'requires': meta['Requires'],
+            #'obsoletes': meta['Obsoletes'],
         }
-        if data['provides'] or data['requires'] or data['obsoletes']:
-            data['metadata_version'] = '1.1'
+        #if data['provides'] or data['requires'] or data['obsoletes']:
+        #    data['metadata_version'] = '1.1'
         return data
 
     def post_to_server(self, data, auth=None):

src/distutils2/command/sdist.py

                 dest = os.path.join(base_dir, file)
                 self.copy_file(file, dest, link=link)
 
-        self.distribution.metadata.write_pkg_info(base_dir)
+        self.distribution.metadata.write(os.path.join(base_dir, 'PKG-INFO'))
 
     def make_distribution(self):
         """Create the source distribution(s).  First, we create the release

src/distutils2/command/upload.py

         # register a new release
         content = open(filename,'rb').read()
         meta = self.distribution.metadata
+
         data = {
             # action
             ':action': 'file_upload',
             'protcol_version': '1',
 
             # identify release
-            'name': meta.get_name(),
-            'version': meta.get_version(),
+            'name': meta['Name'],
+            'version': meta['Version'],
 
             # file content
             'content': (os.path.basename(filename),content),
             'md5_digest': md5(content).hexdigest(),
 
             # additional meta-data
+            # XXX Implement 1.1
             'metadata_version' : '1.0',
-            'summary': meta.get_description(),
-            'home_page': meta.get_url(),
-            'author': meta.get_contact(),
-            'author_email': meta.get_contact_email(),
-            'license': meta.get_licence(),
-            'description': meta.get_long_description(),
-            'keywords': meta.get_keywords(),
-            'platform': meta.get_platforms(),
-            'classifiers': meta.get_classifiers(),
-            'download_url': meta.get_download_url(),
-            # PEP 314
-            'provides': meta.get_provides(),
-            'requires': meta.get_requires(),
-            'obsoletes': meta.get_obsoletes(),
+            'name': meta['Name'],
+            'version': meta['Version'],
+            'summary': meta['Summary'],
+            'home_page': meta['Home-page'],
+            'author': meta['Author'],
+            'author_email': meta['Author-email'],
+            'license': meta['License'],
+            'description': meta['Description'],
+            'keywords': meta['Keywords'],
+            'platform': meta['Platform'],
+            'classifiers': meta['Classifier'],
+            'download_url': meta['Download-URL'],
+            #'provides': meta['Provides'],
+            #'requires': meta['Requires'],
+            #'obsoletes': meta['Obsoletes'],
             }
         comment = ''
         if command == 'bdist_rpm':

src/distutils2/dist.py

 command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
 
 
-class Distribution:
+class Distribution(object):
     """The core of the Distutils.  Most of the work hiding behind 'setup'
     is really done within a Distribution instance, which farms the work out
     to the Distutils commands specified on the command line.
         # information here (and enough command-line options) that it's
         # worth it.  Also delegate 'get_XXX()' methods to the 'metadata'
         # object in a sneaky and underhanded (but efficient!) way.
+
         self.metadata = DistributionMetadata()
-        for basename in self.metadata._METHOD_BASENAMES:
-            method_name = "get_" + basename
-            setattr(self, method_name, getattr(self.metadata, method_name))
+        #for basename in self.metadata._METHOD_BASENAMES:
+        #    method_name = "get_" + basename
+        #    setattr(self, method_name, getattr(self.metadata, method_name))
 
         # 'cmdclass' maps command names to class objects, so we
         # can 1) quickly figure out which class to instantiate when
         # the setup script) to possibly override any or all of these
         # distribution options.
 
-        if attrs:
+        if attrs is not None:
             # Pull out the set of command options and work on them
             # specifically.  Note that this order guarantees that aliased
             # command options will override any supplied redundantly
                     for (opt, val) in cmd_options.items():
                         opt_dict[opt] = ("setup script", val)
 
-            if 'licence' in attrs:
-                attrs['license'] = attrs['licence']
-                del attrs['licence']
-                msg = "'licence' distribution option is deprecated; use 'license'"
-                if warnings is not None:
-                    warnings.warn(msg)
-                else:
-                    sys.stderr.write(msg + "\n")
-
             # Now work on the rest of the attributes.  Any attribute that's
             # not already defined is invalid!
-            for (key, val) in attrs.items():
-                if hasattr(self.metadata, "set_" + key):
-                    getattr(self.metadata, "set_" + key)(val)
-                elif hasattr(self.metadata, key):
-                    setattr(self.metadata, key, val)
+            for key, val in attrs.items():
+                if self.metadata.is_metadata_field(key):
+                    self.metadata[key] = val
                 elif hasattr(self, key):
                     setattr(self, key, val)
                 else:
             dict = self.command_options[command] = {}
         return dict
 
+    def get_fullname(self):
+        return self.metadata.get_fullname()
+
     def dump_option_dicts(self, header=None, commands=None, indent=""):
         from pprint import pformat
 
         instance, analogous to the .finalize_options() method of Command
         objects.
         """
-        for attr in ('keywords', 'platforms'):
-            value = getattr(self.metadata, attr)
-            if value is None:
-                continue
-            if isinstance(value, str):
-                value = [elm.strip() for elm in value.split(',')]
-                setattr(self.metadata, attr, value)
+
+        # XXX conversion -- removed
+        #for attr in ('keywords', 'platforms'):
+        #    value = self.metadata.get_field(attr)
+        #    if value is None:
+        #        continue
+        #    if isinstance(value, str):
+        #        value = [elm.strip() for elm in value.split(',')]
+        #        setattr(self.metadata, attr, value)
 
     def _show_help(self, parser, global_options=1, display_options=1,
                    commands=[]):
         for option in self.display_options:
             is_display_option[option[0]] = 1
 
-        for (opt, val) in option_order:
+        for opt, val in option_order:
             if val and is_display_option.get(opt):
                 opt = translate_longopt(opt)
-                value = getattr(self.metadata, "get_"+opt)()
-                if opt in ['keywords', 'platforms']:
+                value = self.metadata[opt]
+                if opt in ['keywords', 'platform']:
                     print(','.join(value))
                 elif opt in ('classifiers', 'provides', 'requires',
                              'obsoletes'):

src/distutils2/metadata.py

+"""
+==================================================
+Implementation of the Metadata for Python packages
+==================================================
+
+The file format is RFC 822 and there are currently three implementations.
+We only support reading/writing Metadata v1.0 or v1.2. If 1.1 is encountered
+1.1 extra fields will be ignored.
+
+PEP 241 - Metadata v1.0
+=======================
+
+- Metadata-Version
+- Name
+- Version
+- Platform (multiple)
+- Summary
+- Description (optional)
+- Keywords (optional)
+- Home-page (optional)
+- Author  (optional)
+- Author-email (optional)
+- License (optional)
+
+PEP 345 - Metadata v1.2
+=======================
+
+# XXX adding codename ? multiple email rfc232 ?
+
+- Metadata-Version
+- Name
+- Version
+- Platform (multiple)
+- Supported-Platform (multiple)
+- Summary
+- Description (optional) -- changed format
+- Keywords (optional)
+- Home-page (optional)
+- Download-URL
+- Author  (optional)
+- Author-email (optional)
+- Maintainer (optional)
+- Maintainer-email (optional)
+- License (optional)
+- Classifier (multiple) -- see PEP 241
+- Requires-Python
+- Requires-External (multiple)
+- Requires-Dist (multiple)
+- Provides-Dist (multiple)
+- Obsoletes-Dist (multiple)
+
+"""
+
 import os
 import sys
 import platform
 
 from distutils2.util import rfc822_escape
 
+try:
+    # docutils is installed
+    from docutils.utils import Reporter
+    from docutils.parsers.rst import Parser
+    from docutils import frontend
+    from docutils import nodes
+    from StringIO import StringIO
+
+    class SilentReporter(Reporter):
+
+        def __init__(self, source, report_level, halt_level, stream=None,
+                     debug=0, encoding='ascii', error_handler='replace'):
+            self.messages = []
+            Reporter.__init__(self, source, report_level, halt_level, stream,
+                              debug, encoding, error_handler)
+
+        def system_message(self, level, message, *children, **kwargs):
+            self.messages.append((level, message, children, kwargs))
+
+    _HAS_DOCUTILS = True
+except ImportError:
+    # docutils is not installed
+    _HAS_DOCUTILS = False
+
 # Encoding used for the PKG-INFO files
 PKG_INFO_ENCODING = 'utf-8'
 
 
+_241_FIELDS = ('Metadata-Version',  'Name', 'Version', 'Platform',
+               'Summary', 'Description',
+               'Keywords', 'Home-page', 'Author', 'Author-email',
+               'License')
+
+_345_FIELDS = ('Metadata-Version',  'Name', 'Version', 'Platform',
+               'Supported-Platform', 'Summary', 'Description',
+               'Keywords', 'Home-page', 'Author', 'Author-email',
+               'Maintainer', 'Maintainer-email', 'License',
+               'Classifier', 'Download-URL', 'Obsoletes-Dist',
+               'Provides-Dist', 'Requires-Dist', 'Requires-Python',
+               'Requires-External')
+
+_ATTR2FIELD = {'metadata_version': 'Metadata-Version',
+               'name': 'Name',
+               'version': 'Version',
+               'platform': 'Platform',
+               'supported_platform': 'Supported-Platform',
+               'description': 'Summary',
+               'long_description': 'Description',
+               'keywords': 'Keywords',
+               'url': 'Home-page',
+               'author': 'Author',
+               'author_email': 'Author-email',
+               'maintainer': 'Maintainer',
+               'maintainer_email': 'Maintainer-email',
+               'licence': 'License',
+               'classifier': 'Classifier',
+               'download_url': 'Download-URL',
+               'obsoletes_dist': 'Obsoletes-Dist',
+               'provides_dist': 'Provides-Dist',
+               'requires_dist': 'Requires-Dist',
+               'requires_python': 'Requires-Python',
+               'requires_external': 'Requires-External',
+               'requires': 'Requires',
+               'provides': 'Provides',
+               'obsoletes': 'Obsoletes',
+               }
+
+_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
+               'Requires', 'Provides', 'Obsoletes-Dist',
+               'Provides-Dist', 'Requires-Dist', 'Requires-Python',
+               'Requires-External')
+
+_ELEMENTSFIELD = ('Keywords',)
+
+_UNICODEFIELDS = ('Author', 'Maintainer')
+
+
 class DistributionMetadata(object):
-    """Dummy class to hold the distribution meta-data: name, version,
-    author, and so forth.
+    """Distribution meta-data class (1.0 or 1.2).
     """
+    def __init__(self, path=None):
+        self._fields = {}
+        self.version = None
+        self.docutils_support = _HAS_DOCUTILS
+        if path is not None:
+            self.read(path)
 
-    _METHOD_BASENAMES = ("name", "version", "author", "author_email",
-                         "maintainer", "maintainer_email", "url",
-                         "license", "description", "long_description",
-                         "keywords", "platforms", "fullname", "contact",
-                         "contact_email", "license", "classifiers",
-                         "download_url",
-                         # PEP 314
-                         "provides", "requires", "obsoletes",
-                         )
-
-    def __init__(self, path=None):
-        if path is not None:
-            self.read_pkg_file(open(path))
-        else:
-            self.name = None
-            self.version = None
-            self.author = None
-            self.author_email = None
-            self.maintainer = None
-            self.maintainer_email = None
-            self.url = None
-            self.license = None
-            self.description = None
-            self.long_description = None
-            self.keywords = None
-            self.platforms = None
-            self.classifiers = None
-            self.download_url = None
-            # PEP 314
-            self.provides = None
-            self.requires = None
-            self.obsoletes = None
-
-    def read_pkg_file(self, file):
-        """Reads the metadata values from a file object."""
-        msg = message_from_file(file)
-
-        def _read_field(name):
-            value = msg[name]
-            if value == 'UNKNOWN':
-                return None
-            return value
-
-        def _read_list(name):
-            values = msg.get_all(name, None)
-            if values == []:
-                return None
-            return values
-
-        metadata_version = msg['metadata-version']
-        self.name = _read_field('name')
-        self.version = _read_field('version')
-        self.description = _read_field('summary')
-        # we are filling author only.
-        self.author = _read_field('author')
-        self.maintainer = None
-        self.author_email = _read_field('author-email')
-        self.maintainer_email = None
-        self.url = _read_field('home-page')
-        self.license = _read_field('license')
-
-        if 'download-url' in msg:
-            self.download_url = _read_field('download-url')
-        else:
-            self.download_url = None
-
-        self.long_description = _read_field('description')
-        self.description = _read_field('summary')
-
-        if 'keywords' in msg:
-            self.keywords = _read_field('keywords').split(',')
-
-        self.platforms = _read_list('platform')
-        self.classifiers = _read_list('classifier')
-
-        # PEP 314 - these fields only exist in 1.1
-        if metadata_version == '1.1':
-            self.requires = _read_list('requires')
-            self.provides = _read_list('provides')
-            self.obsoletes = _read_list('obsoletes')
-        else:
-            self.requires = None
-            self.provides = None
-            self.obsoletes = None
-
-    def write_pkg_info(self, base_dir):
-        """Write the PKG-INFO file into the release tree.
-        """
-        pkg_info = open( os.path.join(base_dir, 'PKG-INFO'), 'w')
-        self.write_pkg_file(pkg_info)
-        pkg_info.close()
-
-    def write_pkg_file(self, file):
-        """Write the PKG-INFO format data to a file object.
-        """
-        version = '1.0'
-        if self.provides or self.requires or self.obsoletes:
-            version = '1.1'
-
-        self._write_field(file, 'Metadata-Version', version)
-        self._write_field(file, 'Name', self.get_name())
-        self._write_field(file, 'Version', self.get_version())
-        self._write_field(file, 'Summary', self.get_description())
-        self._write_field(file, 'Home-page', self.get_url())
-        self._write_field(file, 'Author', self.get_contact())
-        self._write_field(file, 'Author-email', self.get_contact_email())
-        self._write_field(file, 'License', self.get_license())
-        if self.download_url:
-            self._write_field(file, 'Download-URL', self.download_url)
-
-        long_desc = rfc822_escape(self.get_long_description())
-        self._write_field(file, 'Description', long_desc)
-
-        keywords = ','.join(self.get_keywords())
-        if keywords:
-            self._write_field(file, 'Keywords', keywords)
-
-        self._write_list(file, 'Platform', self.get_platforms())
-        self._write_list(file, 'Classifier', self.get_classifiers())
-
-        # PEP 314
-        self._write_list(file, 'Requires', self.get_requires())
-        self._write_list(file, 'Provides', self.get_provides())
-        self._write_list(file, 'Obsoletes', self.get_obsoletes())
+    def _guessmetadata_version(self):
+        for field in self._fields:
+            if field in _345_FIELDS and field not in _241_FIELDS:
+                return '1.2'
+        return '1.0'
 
     def _write_field(self, file, name, value):
-        file.write('%s: %s\n' % (name, self._encode_field(value)))
+        file.write('%s: %s\n' % (name, value))
 
     def _write_list (self, file, name, values):
         for value in values:
             self._write_field(file, name, value)
 
     def _encode_field(self, value):
-        if value is None:
-            return None
         if isinstance(value, unicode):
             return value.encode(PKG_INFO_ENCODING)
         return str(value)
 
-    # -- Metadata query methods ----------------------------------------
+    def __getitem__(self, name):
+        return self.get_field(name)
 
-    def get_name(self):
-        return self.name or "UNKNOWN"
+    def __setitem__(self, name, value):
+        return self.set_field(name, value)
 
-    def get_version(self):
-        return self.version or "0.0.0"
+    def _convert_name(self, name):
+        if name in _241_FIELDS + _345_FIELDS:
+            return name
+        name = name.replace('-', '_').lower()
+        if name in _ATTR2FIELD:
+            return _ATTR2FIELD[name]
+        return name
 
+    def _default_value(self, name):
+        if name in _LISTFIELDS + _ELEMENTSFIELD:
+            return []
+        return 'UNKNOWN'
+
+    def _check_rst_data(self, data):
+        """Returns warnings when the provided data doesn't compile."""
+        source_path = StringIO()
+        parser = Parser()
+        settings = frontend.OptionParser().get_default_values()
+        settings.tab_width = 4
+        settings.pep_references = None
+        settings.rfc_references = None
+        reporter = SilentReporter(source_path,
+                          settings.report_level,
+                          settings.halt_level,
+                          stream=settings.warning_stream,
+                          debug=settings.debug,
+                          encoding=settings.error_encoding,
+                          error_handler=settings.error_encoding_error_handler)
+
+        document = nodes.document(settings, reporter, source=source_path)
+        document.note_source(source_path, -1)
+        try:
+            parser.parse(data, document)
+        except AttributeError:
+            reporter.messages.append((-1, 'Could not finish the parsing.',
+                                      '', {}))
+
+        return reporter.messages
+
+    #
+    # Public APIs
+    #
     def get_fullname(self):
-        return "%s-%s" % (self.get_name(), self.get_version())
+        return '%s-%s' % (self['Name'], self['Version'])
 
-    def get_author(self):
-        return self._encode_field(self.author) or "UNKNOWN"
+    def is_metadata_field(self, name):
+        name = self._convert_name(name)
+        return name in _241_FIELDS + _345_FIELDS
 
-    def get_author_email(self):
-        return self.author_email or "UNKNOWN"
+    def read(self, filepath):
+        self.read_file(open(filepath))
 
-    def get_maintainer(self):
-        return self._encode_field(self.maintainer) or "UNKNOWN"
+    def read_file(self, fileob):
+        """Reads the metadata values from a file object."""
+        msg = message_from_file(fileob)
+        version = msg['metadata-version']
+        if version in ('1.0', '1.1'):
+            fields = _241_FIELDS
+        else:
+            fields = _345_FIELDS
 
-    def get_maintainer_email(self):
-        return self.maintainer_email or "UNKNOWN"
+        for field in fields:
+            value = msg[field.lower()]
+            if value is None:
+                continue
+            self.set_field(field, value)
 
-    def get_contact(self):
-        return (self._encode_field(self.maintainer) or
-                self._encode_field(self.author) or "UNKNOWN")
+        self.version = self._guessmetadata_version()
+        self.set_field('Metadata-Version', self.version)
 
-    def get_contact_email(self):
-        return self.maintainer_email or self.author_email or "UNKNOWN"
+    def write(self, filepath):
+        """Write the metadata fields into path.
+        """
+        pkg_info = open(filepath, 'w')
+        try:
+            self.write_file(pkg_info)
+        finally:
+            pkg_info.close()
 
-    def get_url(self):
-        return self.url or "UNKNOWN"
+    def write_file(self, fileobject):
+        """Write the PKG-INFO format data to a file object.
+        """
+        version = self._guessmetadata_version()
+        if 'Metadata-Version' not in self._fields:
+            self['Metadata-Version'] = version
+        if version == '1.0':
+            fields = _241_FIELDS
+        else:
+            fields = _345_FIELDS
+        for field in fields:
+            values = self.get_field(field)
+            if field in _ELEMENTSFIELD:
+                self._write_field(fileobject, field, ','.join(values))
+                continue
+            if field not in _LISTFIELDS:
+                values = [values]
+            for value in values:
+                self._write_field(fileobject, field, value)
 
-    def get_license(self):
-        return self.license or "UNKNOWN"
-    get_licence = get_license
+    def set_field(self, name, value):
+        """Controls then sets a metadata field"""
+        name = self._convert_name(name)
+        if name in ('Requires', 'Obsoletes', 'Provides'):
+            # check the values
+            for version in value:
+                distutils2.versionpredicate.VersionPredicate(version)
+        if name in _LISTFIELDS + _ELEMENTSFIELD:
+            if isinstance(value, str):
+                value = value.split(',')
+        elif name in _UNICODEFIELDS:
+            value = self._encode_field(value)
+        self._fields[name] = value
 
-    def get_description(self):
-        return self._encode_field(self.description) or "UNKNOWN"
+    def get_field(self, name):
+        """Gets a metadata field."""
+        name = self._convert_name(name)
+        if name not in self._fields:
+            return self._default_value(name)
+        if name in _UNICODEFIELDS:
+            return self._encode_field(self._fields[name])
+        elif name in _LISTFIELDS:
+            value = self._fields[name]
+            if value is None:
+                return []
+            return [self._encode_field(v) for v in value]
+        elif name in _ELEMENTSFIELD:
+            value = self._fields[name]
+            if isinstance(value, str):
+                return value.split(',')
+        return self._fields[name]
 
-    def get_long_description(self):
-        return self._encode_field(self.long_description) or "UNKNOWN"
+    def check(self):
+        """Checks if the metadata are compliant."""
+        missing = []
+        for attr in ('Name', 'Version', 'Home-page'):
+            value = self[attr]
+            if value == 'UNKNOWN':
+                missing.append(attr)
 
-    def get_keywords(self):
-        return self.keywords or []
-
-    def get_platforms(self):
-        return self.platforms or ["UNKNOWN"]
-
-    def get_classifiers(self):
-        return self.classifiers or []
-
-    def get_download_url(self):
-        return self.download_url or "UNKNOWN"
-
-    # PEP 314
-    def get_requires(self):
-        return self.requires or []
-
-    def set_requires(self, value):
-        import distutils2.versionpredicate
-        for v in value:
-            distutils2.versionpredicate.VersionPredicate(v)
-        self.requires = value
-
-    def get_provides(self):
-        return self.provides or []
-
-    def set_provides(self, value):
-        value = [v.strip() for v in value]
-        for v in value:
-            import distutils2.versionpredicate
-            distutils2.versionpredicate.split_provision(v)
-        self.provides = value
-
-    def get_obsoletes(self):
-        return self.obsoletes or []
-
-    def set_obsoletes(self, value):
-        import distutils2.versionpredicate
-        for v in value:
-            distutils2.versionpredicate.VersionPredicate(v)
-        self.obsoletes = value
+        if _HAS_DOCUTILS:
+            warnings = self._check_rst_data(self['Description'])
+        else:
+            warnings = []
+        return missing, warnings
 
 
 #
 # micro-language for PEP 345 environment markers
 #
-
 _STR_LIMIT = "'\""
 
 class _Operation(object):

src/distutils2/tests/PKG-INFO

+Metadata-Version: 1.0
+Name: CLVault
+Version: 0.5
+Summary: Command-Line utility to store and retrieve passwords
+Home-page: http://bitbucket.org/tarek/clvault
+Author: Tarek Ziade
+Author-email: tarek@ziade.org
+License: PSF
+Description: CLVault
+        =======
+
+        CLVault uses Keyring to provide a command-line utility to safely store
+        and retrieve passwords.
+
+        Install it using pip or the setup.py script::
+
+        $ python setup.py install
+
+        $ pip install clvault
+
+        Once it's installed, you will have three scripts installed in your
+        Python scripts folder, you can use to list, store and retrieve passwords::
+
+        $ clvault-set blog
+        Set your password:
+        Set the associated username (can be blank): tarek
+        Set a description (can be blank): My blog password
+        Password set.
+
+        $ clvault-get blog
+        The username is "tarek"
+        The password has been copied in your clipboard
+
+        $ clvault-list
+        Registered services:
+        blog    My blog password
+
+
+        *clvault-set* takes a service name then prompt you for a password, and some
+        optional information about your service. The password is safely stored in
+        a keyring while the description is saved in a ``.clvault`` file in your
+        home directory. This file is created automatically the first time the command
+        is used.
+
+        *clvault-get* copies the password for a given service in your clipboard, and
+        displays the associated user if any.
+
+        *clvault-list* lists all registered services, with their description when
+        given.
+
+
+        Project page: http://bitbucket.org/tarek/clvault
+
+
+Keywords: keyring,password,crypt
+Platform: UNKNOWN

src/distutils2/tests/test_check.py

 """Tests for distutils.command.check."""
 import unittest2
 
-from distutils2.command.check import check, HAS_DOCUTILS
+from distutils2.command.check import check
+from distutils2.metadata import _HAS_DOCUTILS
 from distutils2.tests import support
 from distutils2.errors import DistutilsSetupError
 
         # by default, check is checking the metadata
         # should have some warnings
         cmd = self._run()
-        self.assertEquals(cmd._warnings, 2)
+        self.assert_(cmd._warnings > 0)
 
         # now let's add the required fields
         # and run it again, to make sure we don't get
         # any warning anymore
         metadata = {'url': 'xxx', 'author': 'xxx',
                     'author_email': 'xxx',
-                    'name': 'xxx', 'version': 'xxx'}
+                    'name': 'xxx', 'version': 'xxx'
+                    }
         cmd = self._run(metadata)
         self.assertEquals(cmd._warnings, 0)
 
         self.assertEquals(cmd._warnings, 0)
 
     def test_check_document(self):
-        if not HAS_DOCUTILS: # won't test without docutils
+        if not _HAS_DOCUTILS: # won't test without docutils
             return
         pkg_info, dist = self.create_dist()
         cmd = check(dist)
         self.assertEquals(len(msgs), 0)
 
     def test_check_restructuredtext(self):
-        if not HAS_DOCUTILS: # won't test without docutils
+        if not _HAS_DOCUTILS: # won't test without docutils
             return
         # let's see if it detects broken rest in long_description
         broken_rest = 'title\n===\n\ntest'

src/distutils2/tests/test_dist.py

         # let's make sure the file can be written
         # with Unicode fields. they are encoded with
         # PKG_INFO_ENCODING
-        dist.metadata.write_pkg_file(open(my_file, 'w'))
+        dist.metadata.write_file(open(my_file, 'w'))
 
         # regular ascii is of course always usable
         dist = klass(attrs={'author': 'Mister Cafe',
                             'long_description': 'Hehehe'})
 
         my_file2 = os.path.join(tmp_dir, 'f2')
-        dist.metadata.write_pkg_file(open(my_file, 'w'))
+        dist.metadata.write_file(open(my_file, 'w'))
 
     def test_empty_options(self):
         # an empty options dictionary should not stay in the
     def test_finalize_options(self):
 
         attrs = {'keywords': 'one,two',
-                 'platforms': 'one,two'}
+                 'platform': 'one,two'}
 
         dist = Distribution(attrs=attrs)
         dist.finalize_options()
 
         # finalize_option splits platforms and keywords
-        self.assertEquals(dist.metadata.platforms, ['one', 'two'])
-        self.assertEquals(dist.metadata.keywords, ['one', 'two'])
+        self.assertEquals(dist.metadata['platform'], ['one', 'two'])
+        self.assertEquals(dist.metadata['keywords'], ['one', 'two'])
 
     def test_get_command_packages(self):
         dist = Distribution()
         self.assertTrue("requires:" not in meta.lower())
         self.assertTrue("obsoletes:" not in meta.lower())
 
-    def test_provides(self):
+    def test_provides_dist(self):
         attrs = {"name": "package",
                  "version": "1.0",
-                 "provides": ["package", "package.sub"]}
+                 "provides_dist": ["package", "package.sub"]}
         dist = Distribution(attrs)
-        self.assertEqual(dist.metadata.get_provides(),
-                         ["package", "package.sub"])
-        self.assertEqual(dist.get_provides(),
+        self.assertEqual(dist.metadata['Provides-Dist'],
                          ["package", "package.sub"])
         meta = self.format_metadata(dist)
-        self.assertTrue("Metadata-Version: 1.1" in meta)
+        self.assertTrue("Metadata-Version: 1.2" in meta)
         self.assertTrue("requires:" not in meta.lower())
         self.assertTrue("obsoletes:" not in meta.lower())
 
-    def test_provides_illegal(self):
+    def _test_provides_illegal(self):
+        # XXX to do: check the versions
         self.assertRaises(ValueError, Distribution,
                           {"name": "package",
                            "version": "1.0",
-                           "provides": ["my.pkg (splat)"]})
+                           "provides_dist": ["my.pkg (splat)"]})
 
-    def test_requires(self):
+    def test_requires_dist(self):
         attrs = {"name": "package",
                  "version": "1.0",
-                 "requires": ["other", "another (==1.0)"]}
+                 "requires_dist": ["other", "another (==1.0)"]}
         dist = Distribution(attrs)
-        self.assertEqual(dist.metadata.get_requires(),
-                         ["other", "another (==1.0)"])
-        self.assertEqual(dist.get_requires(),
+        self.assertEqual(dist.metadata['Requires-Dist'],
                          ["other", "another (==1.0)"])
         meta = self.format_metadata(dist)
-        self.assertTrue("Metadata-Version: 1.1" in meta)
+        self.assertTrue("Metadata-Version: 1.2" in meta)
         self.assertTrue("provides:" not in meta.lower())
-        self.assertTrue("Requires: other" in meta)
-        self.assertTrue("Requires: another (==1.0)" in meta)
+        self.assertTrue("Requires-Dist: other" in meta)
+        self.assertTrue("Requires-Dist: another (==1.0)" in meta)
         self.assertTrue("obsoletes:" not in meta.lower())
 
-    def test_requires_illegal(self):
+    def _test_requires_illegal(self):
+        # XXX
         self.assertRaises(ValueError, Distribution,
                           {"name": "package",
                            "version": "1.0",
                            "requires": ["my.pkg (splat)"]})
 
-    def test_obsoletes(self):
+    def test_obsoletes_dist(self):
         attrs = {"name": "package",
                  "version": "1.0",
-                 "obsoletes": ["other", "another (<1.0)"]}
+                 "obsoletes_dist": ["other", "another (<1.0)"]}
         dist = Distribution(attrs)
-        self.assertEqual(dist.metadata.get_obsoletes(),
-                         ["other", "another (<1.0)"])
-        self.assertEqual(dist.get_obsoletes(),
+        self.assertEqual(dist.metadata['Obsoletes-Dist'],
                          ["other", "another (<1.0)"])
         meta = self.format_metadata(dist)
-        self.assertTrue("Metadata-Version: 1.1" in meta)
+        self.assertTrue("Metadata-Version: 1.2" in meta)
         self.assertTrue("provides:" not in meta.lower())
         self.assertTrue("requires:" not in meta.lower())
-        self.assertTrue("Obsoletes: other" in meta)
-        self.assertTrue("Obsoletes: another (<1.0)" in meta)
+        self.assertTrue("Obsoletes-Dist: other" in meta)
+        self.assertTrue("Obsoletes-Dist: another (<1.0)" in meta)
 
-    def test_obsoletes_illegal(self):
+    def _test_obsoletes_illegal(self):
+        # XXX
         self.assertRaises(ValueError, Distribution,
                           {"name": "package",
                            "version": "1.0",
 
     def format_metadata(self, dist):
         sio = StringIO.StringIO()
-        dist.metadata.write_pkg_file(sio)
+        dist.metadata.write_file(sio)
         return sio.getvalue()
 
     def test_custom_pydistutils(self):
                  "description": "xxx",
                  "download_url": "http://example.com",
                  "keywords": ['one', 'two'],
-                 "requires": ['foo']}
+                 "requires_dist": ['foo']}
 
         dist = Distribution(attrs)
         metadata = dist.metadata
 
         # write it then reloads it
         PKG_INFO = StringIO.StringIO()
-        metadata.write_pkg_file(PKG_INFO)
+        metadata.write_file(PKG_INFO)
         PKG_INFO.seek(0)
-        metadata.read_pkg_file(PKG_INFO)
 
-        self.assertEquals(metadata.name, "package")
-        self.assertEquals(metadata.version, "1.0")
-        self.assertEquals(metadata.description, "xxx")
-        self.assertEquals(metadata.download_url, 'http://example.com')
-        self.assertEquals(metadata.keywords, ['one', 'two'])
-        self.assertEquals(metadata.platforms, ['UNKNOWN'])
-        self.assertEquals(metadata.obsoletes, None)
-        self.assertEquals(metadata.requires, ['foo'])
+        metadata.read_file(PKG_INFO)
+        self.assertEquals(metadata['name'], "package")
+        self.assertEquals(metadata['version'], "1.0")
+        self.assertEquals(metadata['description'], "xxx")
+        self.assertEquals(metadata['download_url'], 'http://example.com')
+        self.assertEquals(metadata['keywords'], ['one', 'two'])
+        self.assertEquals(metadata['platform'], [])
+        self.assertEquals(metadata['obsoletes'], [])
+        self.assertEquals(metadata['requires-dist'], ['foo'])
 
 def test_suite():
     suite = unittest2.TestSuite()

src/distutils2/tests/test_metadata.py

 import unittest2
 import os
 import sys
+from StringIO import StringIO
 
 from distutils2.metadata import DistributionMetadata, _interpret
 
         assert _interpret("'buuuu' not in os.name and '%s' in os.name" \
                             % os_name)
 
+
+    def test_metadata_read_write(self):
+
+        PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
+        metadata = DistributionMetadata(PKG_INFO)
+        res = StringIO()
+        metadata.write_file(res)
+        res.seek(0)
+        res = res.read()
+        f = open(PKG_INFO)
+        wanted = f.read()
+        f.close()
+
 def test_suite():
     return unittest2.makeSuite(DistributionMetadataTestCase)
 

src/distutils2/tests/test_sdist.py

         cmd.ensure_finalized()
         cmd.run()
         warnings = self.get_logs(WARN)
-        self.assertEquals(len(warnings), 2)
+        self.assertEquals(len(warnings), 1)
 
         # trying with a complete set of metadata
         self.clear_logs()

src/distutils2/tests/test_upload.py

         # what did we send ?
         self.assertIn('dédé', self.last_open.req.data)
         headers = dict(self.last_open.req.headers)
-        self.assertEquals(headers['Content-length'], '2085')
+        self.assert_(headers['Content-length'] > 2000)
         self.assertTrue(headers['Content-type'].startswith('multipart/form-data'))
         self.assertEquals(self.last_open.req.get_method(), 'POST')
         self.assertEquals(self.last_open.req.get_full_url(),