summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFred Drake <fdrake@acm.org>2005-03-20 22:19:47 +0000
committerFred Drake <fdrake@acm.org>2005-03-20 22:19:47 +0000
commit5769a316dffbf9c0d0f4b3ceb36c1e5deec15ab2 (patch)
tree7fc3c0b2ff602dcdb8bb53dd100416794ad8bc15
parentf56bef0093f4af0628daa6f957c1ab13bafc87c2 (diff)
downloadcpython-5769a316dffbf9c0d0f4b3ceb36c1e5deec15ab2.tar.gz
PEP 314 implementation (client side):
added support for the provides, requires, and obsoletes metadata fields
-rw-r--r--Doc/dist/dist.tex76
-rw-r--r--Doc/whatsnew/whatsnew25.tex8
-rw-r--r--Lib/distutils/command/register.py6
-rw-r--r--Lib/distutils/core.py4
-rw-r--r--Lib/distutils/dist.py109
-rw-r--r--Lib/distutils/tests/test_dist.py91
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