mirror of https://github.com/python/cpython.git
PEP 314 implementation (client side):
added support for the provides, requires, and obsoletes metadata fields
This commit is contained in:
parent
54398d6afb
commit
db7b0027dc
|
@ -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 @@ def build_post_data(self, action):
|
|||
'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 @@ def gen_usage (script_name):
|
|||
'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 @@ class Distribution:
|
|||
"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 @@ def __init__ (self, attrs=None):
|
|||
# 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 @@ def __init__ (self, attrs=None):
|
|||
# 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 @@ def handle_display_options (self, option_order):
|
|||
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,41 +1052,59 @@ def __init__ (self):
|
|||
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() )
|
||||
if self.download_url:
|
||||
pkg_info.write('Download-URL: %s\n' % self.download_url)
|
||||
|
||||
long_desc = rfc822_escape( self.get_long_description() )
|
||||
pkg_info.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 )
|
||||
|
||||
for classifier in self.get_classifiers():
|
||||
pkg_info.write('Classifier: %s\n' % classifier )
|
||||
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:
|
||||
file.write('Download-URL: %s\n' % self.download_url)
|
||||
|
||||
long_desc = rfc822_escape( self.get_long_description() )
|
||||
file.write('Description: %s\n' % long_desc)
|
||||
|
||||
keywords = string.join( self.get_keywords(), ',')
|
||||
if keywords:
|
||||
file.write('Keywords: %s\n' % 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 _write_list (self, file, name, values):
|
||||
for value in values:
|
||||
file.write('%s: %s\n' % (name, value))
|
||||
|
||||
# -- Metadata query methods ----------------------------------------
|
||||
|
||||
def get_name (self):
|
||||
|
@ -1134,6 +1163,40 @@ def get_classifiers(self):
|
|||
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.dist
|
||||
import os
|
||||
import shutil
|
||||
import StringIO
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
@ -96,5 +97,93 @@ def test_command_packages_configfile(self):
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue