]> granicus.if.org Git - python/commitdiff
PEP 314 implementation (client side):
authorFred Drake <fdrake@acm.org>
Sun, 20 Mar 2005 22:19:47 +0000 (22:19 +0000)
committerFred Drake <fdrake@acm.org>
Sun, 20 Mar 2005 22:19:47 +0000 (22:19 +0000)
added support for the provides, requires, and obsoletes metadata fields

Doc/dist/dist.tex
Doc/whatsnew/whatsnew25.tex
Lib/distutils/command/register.py
Lib/distutils/core.py
Lib/distutils/dist.py
Lib/distutils/tests/test_dist.py

index 93cc59cfc2d1a4c4532f043409ae07cc9da9c79c..bcff1a62efcfb3d1a2e1c4fc6208cbd9f8c5e156 100644 (file)
@@ -631,7 +631,83 @@ is not needed when building compiled extensions: Distutils
 will automatically add \code{initmodule}
 to the list of exported symbols.
 
+\section{Relationships between Distributions and Packages}
+
+A distribution may relate to packages in three specific ways:
+
+\begin{enumerate}
+  \item It can require packages or modules.
+
+  \item It can provide packages or modules.
+
+  \item It can obsolete packages or modules.
+\end{enumerate}
+
+These relationships can be specified using keyword arguments to the
+\function{distutils.core.setup()} function.
+
+Dependencies on other Python modules and packages can be specified by
+supplying the \var{requires} keyword argument to \function{setup()}.
+The value must be a list of strings.  Each string specifies a package
+that is required, and optionally what versions are sufficient.
+
+To specify that any version of a module or package is required, the
+string should consist entirely of the module or package name.
+Examples include \code{'mymodule'} and \code{'xml.parsers.expat'}.
+
+If specific versions are required, a sequence of qualifiers can be
+supplied in parentheses.  Each qualifier may consist of a comparison
+operator and a version number.  The accepted comparison operators are:
+
+\begin{verbatim}
+<    >    ==
+<=   >=   !=
+\end{verbatim}
+
+These can be combined by using multiple qualifiers separated by commas
+(and optional whitespace).  In this case, all of the qualifiers must
+be matched; a logical AND is used to combine the evaluations.
+
+Let's look at a bunch of examples:
+
+\begin{tableii}{l|l}{code}{Requires Expression}{Explanation}
+  \lineii{==1.0}               {Only version \code{1.0} is compatible}
+  \lineii{>1.0, !=1.5.1, <2.0} {Any version after \code{1.0} and before
+                                \code{2.0} is compatible, except
+                                \code{1.5.1}}
+\end{tableii}
+
+Now that we can specify dependencies, we also need to be able to
+specify what we provide that other distributions can require.  This is
+done using the \var{provides} keyword argument to \function{setup()}.
+The value for this keyword is a list of strings, each of which names a
+Python module or package, and optionally identifies the version.  If
+the version is not specified, it is assumed to match that of the
+distribution.
+
+Some examples:
+
+\begin{tableii}{l|l}{code}{Provides Expression}{Explanation}
+  \lineii{mypkg}      {Provide \code{mypkg}, using the distribution version}
+  \lineii{mypkg (1.1} {Provide \code{mypkg} version 1.1, regardless of the
+                       distribution version}
+\end{tableii}
+
+A package can declare that it obsoletes other packages using the
+\var{obsoletes} keyword argument.  The value for this is similar to
+that of the \var{requires} keyword: a list of strings giving module or
+package specifiers.  Each specifier consists of a module or package
+name optionally followed by one or more version qualifiers.  Version
+qualifiers are given in parentheses after the module or package name.
+
+The versions identified by the qualifiers are those that are obsoleted
+by the distribution being described.  If no qualifiers are given, all
+versions of the named module or package are understood to be
+obsoleted.
+
+
 \section{Installing Scripts}
+
 So far we have been dealing with pure and non-pure Python modules,
 which are usually not run by themselves but imported by scripts.
 
index 869eb58712055e79ce87122022626faf0a8b363e..113fa3278b0468c7cfc053e81a9f889e132e9006 100644 (file)
@@ -52,6 +52,14 @@ Raymond Hettinger.}
 \end{seealso}
 
 
+%======================================================================
+\section{PEP 314: Metadata for Python Software Packages v1.1}
+
+XXX describe this PEP.
+    distutils \function{setup()} now supports the \var{provides},
+    \var{requires}, \var{obsoletes} keywords.
+
+
 %======================================================================
 \section{Other Language Changes}
 
index 8104ce06560bde48704409cffd45a0d8afe12a73..6e9a8d42973ad396fc41cf01642718a11a25c276 100644 (file)
@@ -231,7 +231,13 @@ Your selection [default 1]: ''',
             '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(),
         }
+        if data['provides'] or data['requires'] or data['obsoletes']:
+            data['metadata_version'] = '1.1'
         return data
 
     def post_to_server(self, data, auth=None):
index eba94559d902f305b626577adf1737ba25349840..c9c6f037a73f0792cde31166ba5366e23a146c68 100644 (file)
@@ -47,7 +47,9 @@ setup_keywords = ('distclass', 'script_name', 'script_args', 'options',
                   'name', 'version', 'author', 'author_email',
                   'maintainer', 'maintainer_email', 'url', 'license',
                   'description', 'long_description', 'keywords',
-                  'platforms', 'classifiers', 'download_url',)
+                  'platforms', 'classifiers', 'download_url',
+                  'requires', 'provides', 'obsoletes',
+                  )
 
 # Legal keyword arguments for the Extension constructor
 extension_keywords = ('name', 'sources', 'include_dirs',
index 4f4bae5218d7b6b487210c4cd5c2e96b9d71b014..c5dd5cbf78568a1c5cb357bbe33faf327e9f9947 100644 (file)
@@ -106,6 +106,12 @@ Common commands: (see '--help-commands' for more)
          "print the list of classifiers"),
         ('keywords', None,
          "print the list of keywords"),
+        ('provides', None,
+         "print the list of packages/modules provided"),
+        ('requires', None,
+         "print the list of packages/modules required"),
+        ('obsoletes', None,
+         "print the list of packages/modules made obsolete")
         ]
     display_option_names = map(lambda x: translate_longopt(x[0]),
                                display_options)
@@ -210,7 +216,6 @@ Common commands: (see '--help-commands' for more)
         # distribution options.
 
         if attrs:
-
             # 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
@@ -235,7 +240,9 @@ Common commands: (see '--help-commands' for more)
             # 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, key):
+                if hasattr(self.metadata, "set_" + key):
+                    getattr(self.metadata, "set_" + key)(val)
+                elif hasattr(self.metadata, key):
                     setattr(self.metadata, key, val)
                 elif hasattr(self, key):
                     setattr(self, key, val)
@@ -678,7 +685,8 @@ Common commands: (see '--help-commands' for more)
                 value = getattr(self.metadata, "get_"+opt)()
                 if opt in ['keywords', 'platforms']:
                     print string.join(value, ',')
-                elif opt == 'classifiers':
+                elif opt in ('classifiers', 'provides', 'requires',
+                             'obsoletes'):
                     print string.join(value, '\n')
                 else:
                     print value
@@ -1024,7 +1032,10 @@ class DistributionMetadata:
                          "license", "description", "long_description",
                          "keywords", "platforms", "fullname", "contact",
                          "contact_email", "license", "classifiers",
-                         "download_url")
+                         "download_url",
+                         # PEP 314
+                         "provides", "requires", "obsoletes",
+                         )
 
     def __init__ (self):
         self.name = None
@@ -1041,40 +1052,58 @@ class DistributionMetadata:
         self.platforms = None
         self.classifiers = None
         self.download_url = None
+        # PEP 314
+        self.provides = None
+        self.requires = 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')
 
-        pkg_info.write('Metadata-Version: 1.0\n')
-        pkg_info.write('Name: %s\n' % self.get_name() )
-        pkg_info.write('Version: %s\n' % self.get_version() )
-        pkg_info.write('Summary: %s\n' % self.get_description() )
-        pkg_info.write('Home-page: %s\n' % self.get_url() )
-        pkg_info.write('Author: %s\n' % self.get_contact() )
-        pkg_info.write('Author-email: %s\n' % self.get_contact_email() )
-        pkg_info.write('License: %s\n' % self.get_license() )
+        self.write_pkg_file(pkg_info)
+
+        pkg_info.close()
+
+    # write_pkg_info ()
+
+    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'
+
+        file.write('Metadata-Version: %s\n' % version)
+        file.write('Name: %s\n' % self.get_name() )
+        file.write('Version: %s\n' % self.get_version() )
+        file.write('Summary: %s\n' % self.get_description() )
+        file.write('Home-page: %s\n' % self.get_url() )
+        file.write('Author: %s\n' % self.get_contact() )
+        file.write('Author-email: %s\n' % self.get_contact_email() )
+        file.write('License: %s\n' % self.get_license() )
         if self.download_url:
-            pkg_info.write('Download-URL: %s\n' % self.download_url)
+            file.write('Download-URL: %s\n' % self.download_url)
 
         long_desc = rfc822_escape( self.get_long_description() )
-        pkg_info.write('Description: %s\n' % long_desc)
+        file.write('Description: %s\n' % long_desc)
 
         keywords = string.join( self.get_keywords(), ',')
         if keywords:
-            pkg_info.write('Keywords: %s\n' % keywords )
-
-        for platform in self.get_platforms():
-            pkg_info.write('Platform: %s\n' % platform )
+            file.write('Keywords: %s\n' % keywords )
 
-        for classifier in self.get_classifiers():
-            pkg_info.write('Classifier: %s\n' % classifier )
+        self._write_list(file, 'Platform', self.get_platforms())
+        self._write_list(file, 'Classifier', self.get_classifiers())
 
-        pkg_info.close()
+        # 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())
 
-    # write_pkg_info ()
+    def _write_list (self, file, name, values):
+        for value in values:
+            file.write('%s: %s\n' % (name, value))
 
     # -- Metadata query methods ----------------------------------------
 
@@ -1134,6 +1163,40 @@ class DistributionMetadata:
     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 distutils.versionpredicate
+        for v in value:
+            distutils.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 distutils.versionpredicate
+            ver = distutils.versionpredicate.check_provision(v)
+            if ver:
+                import distutils.version
+                sv = distutils.version.StrictVersion()
+                sv.parse(ver.strip()[1:-1])
+        self.provides = value
+
+    def get_obsoletes(self):
+        return self.obsoletes or []
+
+    def set_obsoletes(self, value):
+        import distutils.versionpredicate
+        for v in value:
+            distutils.versionpredicate.VersionPredicate(v)
+        self.obsoletes = value
+
 # class DistributionMetadata
 
 
index 695f6d8192679c00c4c85977d34bea584b50619c..7675fbfa93f1fdd85a720697dde26cbc4a4c078b 100644 (file)
@@ -4,6 +4,7 @@ import distutils.cmd
 import distutils.dist
 import os
 import shutil
+import StringIO
 import sys
 import tempfile
 import unittest
@@ -96,5 +97,93 @@ class DistributionTestCase(unittest.TestCase):
             os.unlink(TESTFN)
 
 
+class MetadataTestCase(unittest.TestCase):
+
+    def test_simple_metadata(self):
+        attrs = {"name": "package",
+                 "version": "1.0"}
+        dist = distutils.dist.Distribution(attrs)
+        meta = self.format_metadata(dist)
+        self.assert_("Metadata-Version: 1.0" in meta)
+        self.assert_("provides:" not in meta.lower())
+        self.assert_("requires:" not in meta.lower())
+        self.assert_("obsoletes:" not in meta.lower())
+
+    def test_provides(self):
+        attrs = {"name": "package",
+                 "version": "1.0",
+                 "provides": ["package", "package.sub"]}
+        dist = distutils.dist.Distribution(attrs)
+        self.assertEqual(dist.metadata.get_provides(),
+                         ["package", "package.sub"])
+        self.assertEqual(dist.get_provides(),
+                         ["package", "package.sub"])
+        meta = self.format_metadata(dist)
+        self.assert_("Metadata-Version: 1.1" in meta)
+        self.assert_("requires:" not in meta.lower())
+        self.assert_("obsoletes:" not in meta.lower())
+
+    def test_provides_illegal(self):
+        self.assertRaises(ValueError,
+                          distutils.dist.Distribution,
+                          {"name": "package",
+                           "version": "1.0",
+                           "provides": ["my.pkg (splat)"]})
+
+    def test_requires(self):
+        attrs = {"name": "package",
+                 "version": "1.0",
+                 "requires": ["other", "another (==1.0)"]}
+        dist = distutils.dist.Distribution(attrs)
+        self.assertEqual(dist.metadata.get_requires(),
+                         ["other", "another (==1.0)"])
+        self.assertEqual(dist.get_requires(),
+                         ["other", "another (==1.0)"])
+        meta = self.format_metadata(dist)
+        self.assert_("Metadata-Version: 1.1" in meta)
+        self.assert_("provides:" not in meta.lower())
+        self.assert_("Requires: other" in meta)
+        self.assert_("Requires: another (==1.0)" in meta)
+        self.assert_("obsoletes:" not in meta.lower())
+
+    def test_requires_illegal(self):
+        self.assertRaises(ValueError,
+                          distutils.dist.Distribution,
+                          {"name": "package",
+                           "version": "1.0",
+                           "requires": ["my.pkg (splat)"]})
+
+    def test_obsoletes(self):
+        attrs = {"name": "package",
+                 "version": "1.0",
+                 "obsoletes": ["other", "another (<1.0)"]}
+        dist = distutils.dist.Distribution(attrs)
+        self.assertEqual(dist.metadata.get_obsoletes(),
+                         ["other", "another (<1.0)"])
+        self.assertEqual(dist.get_obsoletes(),
+                         ["other", "another (<1.0)"])
+        meta = self.format_metadata(dist)
+        self.assert_("Metadata-Version: 1.1" in meta)
+        self.assert_("provides:" not in meta.lower())
+        self.assert_("requires:" not in meta.lower())
+        self.assert_("Obsoletes: other" in meta)
+        self.assert_("Obsoletes: another (<1.0)" in meta)
+
+    def test_obsoletes_illegal(self):
+        self.assertRaises(ValueError,
+                          distutils.dist.Distribution,
+                          {"name": "package",
+                           "version": "1.0",
+                           "obsoletes": ["my.pkg (splat)"]})
+    def format_metadata(self, dist):
+        sio = StringIO.StringIO()
+        dist.metadata.write_pkg_file(sio)
+        return sio.getvalue()
+
+
 def test_suite():
-    return unittest.makeSuite(DistributionTestCase)
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(DistributionTestCase))
+    suite.addTest(unittest.makeSuite(MetadataTestCase))
+    return suite