Commit db7b0027 authored by Fred Drake's avatar Fred Drake

PEP 314 implementation (client side):

added support for the provides, requires, and obsoletes metadata fields
parent 54398d6a
......@@ -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.
......
......@@ -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}
......
......@@ -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):
......
......@@ -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',
......
......@@ -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
......
......@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment