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}
|
will automatically add \code{initmodule}
|
||||||
to the list of exported symbols.
|
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}
|
\section{Installing Scripts}
|
||||||
|
|
||||||
So far we have been dealing with pure and non-pure Python modules,
|
So far we have been dealing with pure and non-pure Python modules,
|
||||||
which are usually not run by themselves but imported by scripts.
|
which are usually not run by themselves but imported by scripts.
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,14 @@ Raymond Hettinger.}
|
||||||
\end{seealso}
|
\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}
|
\section{Other Language Changes}
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,13 @@ def build_post_data(self, action):
|
||||||
'platform': meta.get_platforms(),
|
'platform': meta.get_platforms(),
|
||||||
'classifiers': meta.get_classifiers(),
|
'classifiers': meta.get_classifiers(),
|
||||||
'download_url': meta.get_download_url(),
|
'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
|
return data
|
||||||
|
|
||||||
def post_to_server(self, data, auth=None):
|
def post_to_server(self, data, auth=None):
|
||||||
|
|
|
@ -47,7 +47,9 @@ def gen_usage (script_name):
|
||||||
'name', 'version', 'author', 'author_email',
|
'name', 'version', 'author', 'author_email',
|
||||||
'maintainer', 'maintainer_email', 'url', 'license',
|
'maintainer', 'maintainer_email', 'url', 'license',
|
||||||
'description', 'long_description', 'keywords',
|
'description', 'long_description', 'keywords',
|
||||||
'platforms', 'classifiers', 'download_url',)
|
'platforms', 'classifiers', 'download_url',
|
||||||
|
'requires', 'provides', 'obsoletes',
|
||||||
|
)
|
||||||
|
|
||||||
# Legal keyword arguments for the Extension constructor
|
# Legal keyword arguments for the Extension constructor
|
||||||
extension_keywords = ('name', 'sources', 'include_dirs',
|
extension_keywords = ('name', 'sources', 'include_dirs',
|
||||||
|
|
|
@ -106,6 +106,12 @@ class Distribution:
|
||||||
"print the list of classifiers"),
|
"print the list of classifiers"),
|
||||||
('keywords', None,
|
('keywords', None,
|
||||||
"print the list of keywords"),
|
"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_option_names = map(lambda x: translate_longopt(x[0]),
|
||||||
display_options)
|
display_options)
|
||||||
|
@ -210,7 +216,6 @@ def __init__ (self, attrs=None):
|
||||||
# distribution options.
|
# distribution options.
|
||||||
|
|
||||||
if attrs:
|
if attrs:
|
||||||
|
|
||||||
# Pull out the set of command options and work on them
|
# Pull out the set of command options and work on them
|
||||||
# specifically. Note that this order guarantees that aliased
|
# specifically. Note that this order guarantees that aliased
|
||||||
# command options will override any supplied redundantly
|
# 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
|
# Now work on the rest of the attributes. Any attribute that's
|
||||||
# not already defined is invalid!
|
# not already defined is invalid!
|
||||||
for (key,val) in attrs.items():
|
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)
|
setattr(self.metadata, key, val)
|
||||||
elif hasattr(self, key):
|
elif hasattr(self, key):
|
||||||
setattr(self, key, val)
|
setattr(self, key, val)
|
||||||
|
@ -678,7 +685,8 @@ def handle_display_options (self, option_order):
|
||||||
value = getattr(self.metadata, "get_"+opt)()
|
value = getattr(self.metadata, "get_"+opt)()
|
||||||
if opt in ['keywords', 'platforms']:
|
if opt in ['keywords', 'platforms']:
|
||||||
print string.join(value, ',')
|
print string.join(value, ',')
|
||||||
elif opt == 'classifiers':
|
elif opt in ('classifiers', 'provides', 'requires',
|
||||||
|
'obsoletes'):
|
||||||
print string.join(value, '\n')
|
print string.join(value, '\n')
|
||||||
else:
|
else:
|
||||||
print value
|
print value
|
||||||
|
@ -1024,7 +1032,10 @@ class DistributionMetadata:
|
||||||
"license", "description", "long_description",
|
"license", "description", "long_description",
|
||||||
"keywords", "platforms", "fullname", "contact",
|
"keywords", "platforms", "fullname", "contact",
|
||||||
"contact_email", "license", "classifiers",
|
"contact_email", "license", "classifiers",
|
||||||
"download_url")
|
"download_url",
|
||||||
|
# PEP 314
|
||||||
|
"provides", "requires", "obsoletes",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__ (self):
|
def __init__ (self):
|
||||||
self.name = None
|
self.name = None
|
||||||
|
@ -1041,41 +1052,59 @@ def __init__ (self):
|
||||||
self.platforms = None
|
self.platforms = None
|
||||||
self.classifiers = None
|
self.classifiers = None
|
||||||
self.download_url = None
|
self.download_url = None
|
||||||
|
# PEP 314
|
||||||
|
self.provides = None
|
||||||
|
self.requires = None
|
||||||
|
self.obsoletes = None
|
||||||
|
|
||||||
def write_pkg_info (self, base_dir):
|
def write_pkg_info (self, base_dir):
|
||||||
"""Write the PKG-INFO file into the release tree.
|
"""Write the PKG-INFO file into the release tree.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pkg_info = open( os.path.join(base_dir, 'PKG-INFO'), 'w')
|
pkg_info = open( os.path.join(base_dir, 'PKG-INFO'), 'w')
|
||||||
|
|
||||||
pkg_info.write('Metadata-Version: 1.0\n')
|
self.write_pkg_file(pkg_info)
|
||||||
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 )
|
|
||||||
|
|
||||||
pkg_info.close()
|
pkg_info.close()
|
||||||
|
|
||||||
# write_pkg_info ()
|
# 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 ----------------------------------------
|
# -- Metadata query methods ----------------------------------------
|
||||||
|
|
||||||
def get_name (self):
|
def get_name (self):
|
||||||
|
@ -1134,6 +1163,40 @@ def get_classifiers(self):
|
||||||
def get_download_url(self):
|
def get_download_url(self):
|
||||||
return self.download_url or "UNKNOWN"
|
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
|
# class DistributionMetadata
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import distutils.dist
|
import distutils.dist
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import StringIO
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -96,5 +97,93 @@ def test_command_packages_configfile(self):
|
||||||
os.unlink(TESTFN)
|
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():
|
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