diff options
-rw-r--r-- | Doc/dist/dist.tex | 76 | ||||
-rw-r--r-- | Doc/whatsnew/whatsnew25.tex | 8 | ||||
-rw-r--r-- | Lib/distutils/command/register.py | 6 | ||||
-rw-r--r-- | Lib/distutils/core.py | 4 | ||||
-rw-r--r-- | Lib/distutils/dist.py | 109 | ||||
-rw-r--r-- | Lib/distutils/tests/test_dist.py | 91 |
6 files changed, 269 insertions, 25 deletions
diff --git a/Doc/dist/dist.tex b/Doc/dist/dist.tex index 93cc59cfc2..bcff1a62ef 100644 --- a/Doc/dist/dist.tex +++ b/Doc/dist/dist.tex @@ -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. diff --git a/Doc/whatsnew/whatsnew25.tex b/Doc/whatsnew/whatsnew25.tex index 869eb58712..113fa3278b 100644 --- a/Doc/whatsnew/whatsnew25.tex +++ b/Doc/whatsnew/whatsnew25.tex @@ -53,6 +53,14 @@ Raymond Hettinger.} %====================================================================== +\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} Here are all of the changes that Python 2.5 makes to the core Python diff --git a/Lib/distutils/command/register.py b/Lib/distutils/command/register.py index 8104ce0656..6e9a8d4297 100644 --- a/Lib/distutils/command/register.py +++ b/Lib/distutils/command/register.py @@ -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): diff --git a/Lib/distutils/core.py b/Lib/distutils/core.py index eba94559d9..c9c6f037a7 100644 --- a/Lib/distutils/core.py +++ b/Lib/distutils/core.py @@ -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', diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py index 4f4bae5218..c5dd5cbf78 100644 --- a/Lib/distutils/dist.py +++ b/Lib/distutils/dist.py @@ -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 diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py index 695f6d8192..7675fbfa93 100644 --- a/Lib/distutils/tests/test_dist.py +++ b/Lib/distutils/tests/test_dist.py @@ -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 |