[Python-checkins] distutils2: XML-RPC client for indexes.
tarek.ziade
python-checkins at python.org
Sun Aug 8 11:50:46 CEST 2010
tarek.ziade pushed 2413e88ec79e to distutils2:
http://hg.python.org/distutils2/rev/2413e88ec79e
changeset: 455:2413e88ec79e
user: Alexis Metaireau <ametaireau at gmail.com>
date: Tue Jul 20 18:18:40 2010 +0200
summary: XML-RPC client for indexes.
files: docs/source/projects-index.client.rst, docs/source/projects-index.dist.rst, docs/source/projects-index.xmlrpc.rst, src/distutils2/index/dist.py, src/distutils2/index/errors.py, src/distutils2/index/simple.py, src/distutils2/index/xmlrpc.py, src/distutils2/tests/pypi_server.py, src/distutils2/tests/test_index_xmlrpc.py
diff --git a/docs/source/projects-index.client.rst b/docs/source/projects-index.client.rst
--- a/docs/source/projects-index.client.rst
+++ b/docs/source/projects-index.client.rst
@@ -8,6 +8,9 @@
The aim of this module is to choose the best way to query the API, using the
less possible XML-RPC, and when possible the simple index.
+.. note:: This index is not yet available, so please rely on XMLRPC client or
+ Simple Crawler to browse indexes.
+
API
===
diff --git a/docs/source/projects-index.dist.rst b/docs/source/projects-index.dist.rst
--- a/docs/source/projects-index.dist.rst
+++ b/docs/source/projects-index.dist.rst
@@ -84,4 +84,30 @@
>>> r['sdist'].url
{'url': 'http://example.org/foobar-1.0.tar.gz', 'hashname': None, 'hashval':
None, 'is_external': True}
+
+Attributes Lazy loading
+-----------------------
+.. note:: This is not currently available. So you have to rely on the indexes by
+ yourself to fill in the fields !
+
+To abstract a maximum the way of querying informations to the indexes,
+attributes and releases informations can be retrieved "on demand", in a "lazy"
+way.
+
+For instance, if you have a release instance that does not contain the metadata
+attribute, it can be build directly when accedded::
+
+ >>> r = Release("FooBar", "1.1")
+ >>> r.has_metadata()
+ False # metadata field is actually set to "None"
+ >>> r.metadata
+ <Metadata for FooBar 1.1>
+
+Like this, it's possible to retrieve project's releases, releases metadata and
+releases distributions informations.
+
+Internally, this is possible because while retrieving for the first time
+informations about projects, releases or distributions, a reference to the
+client used is stored in the objects. Then, while trying to access undefined
+fields, it will be used if necessary.
diff --git a/docs/source/projects-index.xmlrpc.rst b/docs/source/projects-index.xmlrpc.rst
--- a/docs/source/projects-index.xmlrpc.rst
+++ b/docs/source/projects-index.xmlrpc.rst
@@ -2,21 +2,148 @@
Query indexes via XML-RPC
=========================
-Indexes can be queried by using XML-RPC calls, and Distutils2 provides a simple
-way to use this methods.
+Indexes can be queried using XML-RPC calls, and Distutils2 provides a simple
+way to interface with XML-RPC.
-The :class:`distutils2.xmlrpc.Client` have some specificities, that would be
-described here.
+You should **use** XML-RPC when:
-You should use XML-RPC for:
+ * Searching the index for projects **on other fields than project
+ names**. For instance, you can search for projects based on the
+ author_email field.
+ * Searching all the versions that have existed for a project.
+ * you want to retrive METADATAs informations from releases or
+ distributions.
- * XXX TODO
+You should **avoid using** XML-RPC method calls when:
+
+ * Retrieving the last version of a project
+ * Getting the projects with a specific name and version.
+ * The simple index can match your needs
+
+When dealing with indexes, keep in mind that the index queriers will always
+return you :class:`distutils2.index.ReleaseInfo` and
+:class:`distutils2.index.ReleasesList` objects.
+
+Some methods here share common APIs with the one you can find on
+:class:`distutils2.index.simple`, internally, :class:`distutils2.index.client`
+is inherited by :class:`distutils2.index.xmlrpc.Client`
API
====
-::
- >>> from distutils2.index import XmlRpcClient()
- >>> client = XmlRpcClient()
+
+.. autoclass:: distutils2.index.xmlrpc.Client
+ :members:
Usage examples
===============
+
+Use case described here are use case that are not common to the other clients.
+If you want to see all the methods, please refer to API or to usage examples
+described in :class:`distutils2.index.client.Client`
+
+Finding releases
+----------------
+
+It's a common use case to search for "things" within the index.
+We can basically search for projects by their name, which is the
+most used way for users (eg. "give me the last version of the FooBar project").
+This can be accomplished using the following syntax::
+
+ >>> client = XMLRPCClient()
+ >>> client.get("Foobar (<= 1.3))
+ <FooBar 1.2.1>
+ >>> client.find("FooBar (<= 1.3)")
+ [FooBar 1.1, FooBar 1.1.1, FooBar 1.2, FooBar 1.2.1]
+
+And we also can find for specific fields::
+
+ >>> client.find_by(field=value)
+
+You could specify the operator to use, default is "or"::
+
+ >>> client.find_by(field=value, operator="and")
+
+The specific fields you can search are:
+
+ * name
+ * version
+ * author
+ * author_email
+ * maintainer
+ * maintainer_email
+ * home_page
+ * license
+ * summary
+ * description
+ * keywords
+ * platform
+ * download_url
+
+Getting metadata informations
+-----------------------------
+
+XML-RPC is a prefered way to retrieve metadata informations from indexes.
+It's really simple to do so::
+
+ >>> client = XMLRPCClient()
+ >>> client.get_metadata("FooBar", "1.1")
+ <ReleaseInfo FooBar 1.1>
+
+Assuming we already have a :class:`distutils2.index.ReleaseInfo` object defined,
+it's possible to pass it ot the xmlrpc client to retrieve and complete it's
+metadata::
+
+ >>> foobar11 = ReleaseInfo("FooBar", "1.1")
+ >>> client = XMLRPCClient()
+ >>> returned_release = client.get_metadata(release=foobar11)
+ >>> returned_release
+ <ReleaseInfo FooBar 1.1>
+
+Get all the releases of a project
+---------------------------------
+
+To retrieve all the releases for a project, you can build them using
+`get_releases`::
+
+ >>> client = XMLRPCClient()
+ >>> client.get_releases("FooBar")
+ [<ReleaseInfo FooBar 0.9>, <ReleaseInfo FooBar 1.0>, <ReleaseInfo 1.1>]
+
+Get informations about distributions
+------------------------------------
+
+Indexes have informations about projects, releases **and** distributions.
+If you're not familiar with those, please refer to the documentation of
+:mod:`distutils2.index.dist`.
+
+It's possible to retrive informations about distributions, e.g "what are the
+existing distributions for this release ? How to retrieve them ?"::
+
+ >>> client = XMLRPCClient()
+ >>> release = client.get_distributions("FooBar", "1.1")
+ >>> release.dists
+ {'sdist': <FooBar 1.1 sdist>, 'bdist': <FooBar 1.1 bdist>}
+
+As you see, this does not return a list of distributions, but a release,
+because a release can be used like a list of distributions.
+
+Lazy load information from project, releases and distributions.
+----------------------------------------------------------------
+
+.. note:: The lazy loading feature is not currently available !
+
+As :mod:`distutils2.index.dist` classes support "lazy" loading of
+informations, you can use it while retrieving informations from XML-RPC.
+
+For instance, it's possible to get all the releases for a project, and to access
+directly the metadata of each release, without making
+:class:`distutils2.index.xmlrpc.Client` directly (they will be made, but they're
+invisible to the you)::
+
+ >>> client = XMLRPCClient()
+ >>> releases = client.get_releases("FooBar")
+ >>> releases.get_release("1.1").metadata
+ <Metadata for FooBar 1.1>
+
+Refer to the :mod:`distutils2.index.dist` documentation for more information
+about attributes lazy loading.
diff --git a/src/distutils2/index/dist.py b/src/distutils2/index/dist.py
--- a/src/distutils2/index/dist.py
+++ b/src/distutils2/index/dist.py
@@ -20,6 +20,7 @@
except ImportError:
from distutils2._backport import hashlib
+from distutils2.errors import IrrationalVersionError
from distutils2.index.errors import (HashDoesNotMatch, UnsupportedHashName,
CantParseArchiveName)
from distutils2.version import suggest_normalized_version, NormalizedVersion
@@ -47,14 +48,30 @@
:param kwargs: optional arguments for a new distribution.
"""
self.name = name
- self.version = NormalizedVersion(version)
- self.metadata = DistributionMetadata() # XXX from_dict=metadata)
+ self._version = None
+ self.version = version
+ self.metadata = DistributionMetadata(mapping=metadata)
self.dists = {}
self.hidden = hidden
-
+
if 'dist_type' in kwargs:
dist_type = kwargs.pop('dist_type')
self.add_distribution(dist_type, **kwargs)
+
+ def set_version(self, version):
+ try:
+ self._version = NormalizedVersion(version)
+ except IrrationalVersionError:
+ suggestion = suggest_normalized_version(version)
+ if suggestion:
+ self.version = suggestion
+ else:
+ raise IrrationalVersionError(version)
+
+ def get_version(self):
+ return self._version
+
+ version = property(get_version, set_version)
@property
def is_final(self):
@@ -111,6 +128,9 @@
return self.get_distribution(prefer_source=prefer_source)\
.download(path=temp_path)
+ def set_metadata(self, metadata):
+ self.metadata.update(metadata)
+
def __getitem__(self, item):
"""distributions are available using release["sdist"]"""
return self.dists[item]
@@ -262,13 +282,12 @@
Provides useful methods and facilities to sort and filter releases.
"""
- def __init__(self, name, list=[], contains_hidden=False):
+ def __init__(self, name, releases=[], contains_hidden=False):
super(ReleasesList, self).__init__()
- for item in list:
- self.append(item)
self.name = name
self.contains_hidden = contains_hidden
-
+ self.add_releases(releases)
+
def filter(self, predicate):
"""Filter and return a subset of releases matching the given predicate.
"""
@@ -286,6 +305,14 @@
releases.sort_releases(prefer_final, reverse=True)
return releases[0]
+ def add_releases(self, releases):
+ """Add releases in the release list.
+
+ :param: releases is a list of ReleaseInfo objects.
+ """
+ for r in releases:
+ self.add_release(release=r)
+
def add_release(self, version=None, dist_type='sdist', release=None,
**dist_args):
"""Add a release to the list.
@@ -302,6 +329,9 @@
if release.name != self.name:
raise ValueError(release.name)
version = '%s' % release.version
+ if not version in self.get_versions():
+ # append only if not already exists
+ self.append(release)
for dist in release.dists.values():
for url in dist.urls:
self.add_release(version, dist.dist_type, **url)
@@ -350,6 +380,12 @@
"""Return a list of releases versions contained"""
return ["%s" % r.version for r in self]
+ def __repr__(self):
+ string = 'Project "%s"' % self.name
+ if self.get_versions():
+ string += ' versions: %s' % ', '.join(self.get_versions())
+ return '<%s>' % string
+
def get_infos_from_url(url, probable_dist_name=None, is_external=True):
"""Get useful informations from an URL.
diff --git a/src/distutils2/index/errors.py b/src/distutils2/index/errors.py
--- a/src/distutils2/index/errors.py
+++ b/src/distutils2/index/errors.py
@@ -9,6 +9,10 @@
"""The base class for errors of the index python package."""
+class ProjectNotFound(IndexError):
+ """Project has not been found"""
+
+
class DistributionNotFound(IndexError):
"""No distribution match the given requirements."""
@@ -31,3 +35,7 @@
class UnableToDownload(IndexError):
"""All mirrors have been tried, without success"""
+
+
+class InvalidSearchField(IndexError):
+ """An invalid search field has been used"""
diff --git a/src/distutils2/index/simple.py b/src/distutils2/index/simple.py
--- a/src/distutils2/index/simple.py
+++ b/src/distutils2/index/simple.py
@@ -223,7 +223,7 @@
try:
infos = get_infos_from_url(link, project_name,
is_external=not self.index_url in url)
- except CantParseArchiveName as e:
+ except CantParseArchiveName, e:
logging.warning("version has not been parsed: %s"
% e)
else:
diff --git a/src/distutils2/index/xmlrpc.py b/src/distutils2/index/xmlrpc.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/index/xmlrpc.py
@@ -0,0 +1,165 @@
+import logging
+import xmlrpclib
+
+from distutils2.errors import IrrationalVersionError
+from distutils2.index.base import IndexClient
+from distutils2.index.errors import ProjectNotFound, InvalidSearchField
+from distutils2.index.dist import ReleaseInfo, ReleasesList
+
+
+PYPI_XML_RPC_URL = 'http://python.org/pypi'
+
+_SEARCH_FIELDS = ['name', 'version', 'author', 'author_email', 'maintainer',
+ 'maintainer_email', 'home_page', 'license', 'summary',
+ 'description', 'keywords', 'platform', 'download_url']
+
+
+class Client(IndexClient):
+ """Client to query indexes using XML-RPC method calls.
+
+ If no server_url is specified, use the default PyPI XML-RPC URL,
+ defined in the PYPI_XML_RPC_URL constant::
+
+ >>> client = XMLRPCClient()
+ >>> client.server_url == PYPI_XML_RPC_URL
+ True
+
+ >>> client = XMLRPCClient("http://someurl/")
+ >>> client.server_url
+ 'http://someurl/'
+ """
+
+ def __init__(self, server_url=PYPI_XML_RPC_URL, prefer_final=False):
+ self.server_url = server_url
+ self._projects = {}
+ self._prefer_final = prefer_final
+
+ def _search_for_releases(self, requirements):
+ return self.get_releases(requirements.name)
+
+ def _get_release(self, requirements, prefer_final=False):
+ releases = self.get_releases(requirements.name)
+ release = releases.get_last(requirements, prefer_final)
+ self.get_metadata(release.name, "%s" % release.version)
+ self.get_distributions(release.name, "%s" % release.version)
+ return release
+
+ @property
+ def proxy(self):
+ """Property used to return the XMLRPC server proxy.
+
+ If no server proxy is defined yet, creates a new one::
+
+ >>> client = XmlRpcClient()
+ >>> client.proxy()
+ <ServerProxy for python.org/pypi>
+
+ """
+ if not hasattr(self, '_server_proxy'):
+ self._server_proxy = xmlrpclib.ServerProxy(self.server_url)
+
+ return self._server_proxy
+
+ def _get_project(self, project_name):
+ """Return an project instance, create it if necessary"""
+ return self._projects.setdefault(project_name,
+ ReleasesList(project_name))
+
+ def get_releases(self, project_name, show_hidden=True, force_update=False):
+ """Return the list of existing releases for a specific project.
+
+ Cache the results from one call to another.
+
+ If show_hidden is True, return the hidden releases too.
+ If force_update is True, reprocess the index to update the
+ informations (eg. make a new XML-RPC call).
+ ::
+
+ >>> client = XMLRPCClient()
+ >>> client.get_releases('Foo')
+ ['1.1', '1.2', '1.3']
+
+ If no such project exists, raise a ProjectNotFound exception::
+
+ >>> client.get_project_versions('UnexistingProject')
+ ProjectNotFound: UnexistingProject
+
+ """
+ def get_versions(project_name, show_hidden):
+ return self.proxy.package_releases(project_name, show_hidden)
+
+ if not force_update and (project_name in self._projects):
+ project = self._projects[project_name]
+ if not project.contains_hidden and show_hidden:
+ # if hidden releases are requested, and have an existing
+ # list of releases that does not contains hidden ones
+ all_versions = get_versions(project_name, show_hidden)
+ existing_versions = project.get_versions()
+ hidden_versions = list(set(all_versions) -
+ set(existing_versions))
+ for version in hidden_versions:
+ project.add_release(release=ReleaseInfo(project_name,
+ version))
+ return project
+ else:
+ versions = get_versions(project_name, show_hidden)
+ if not versions:
+ raise ProjectNotFound(project_name)
+ project = self._get_project(project_name)
+ project.add_releases([ReleaseInfo(project_name, version)
+ for version in versions])
+ return project
+
+ def get_distributions(self, project_name, version):
+ """Grab informations about distributions from XML-RPC.
+
+ Return a ReleaseInfo object, with distribution-related informations
+ filled in.
+ """
+ url_infos = self.proxy.release_urls(project_name, version)
+ project = self._get_project(project_name)
+ if version not in project.get_versions():
+ project.add_release(release=ReleaseInfo(project_name, version))
+ release = project.get_release(version)
+ for info in url_infos:
+ packagetype = info['packagetype']
+ dist_infos = {'url': info['url'],
+ 'hashval': info['md5_digest'],
+ 'hashname': 'md5',
+ 'is_external': False}
+ release.add_distribution(packagetype, **dist_infos)
+ return release
+
+ def get_metadata(self, project_name, version):
+ """Retreive project metadatas.
+
+ Return a ReleaseInfo object, with metadata informations filled in.
+ """
+ metadata = self.proxy.release_data(project_name, version)
+ project = self._get_project(project_name)
+ if version not in project.get_versions():
+ project.add_release(release=ReleaseInfo(project_name, version))
+ release = project.get_release(version)
+ release.set_metadata(metadata)
+ return release
+
+ def search(self, name=None, operator="or", **kwargs):
+ """Find using the keys provided in kwargs.
+
+ You can set operator to "and" or "or".
+ """
+ for key in kwargs:
+ if key not in _SEARCH_FIELDS:
+ raise InvalidSearchField(key)
+ if name:
+ kwargs["name"] = name
+ projects = self.proxy.search(kwargs, operator)
+ for p in projects:
+ project = self._get_project(p['name'])
+ try:
+ project.add_release(release=ReleaseInfo(p['name'],
+ p['version'], metadata={'summary':p['summary']}))
+ except IrrationalVersionError, e:
+ logging.warn("Irrational version error found: %s" % e)
+
+ return [self._projects[p['name']] for p in projects]
diff --git a/src/distutils2/tests/pypi_server.py b/src/distutils2/tests/pypi_server.py
--- a/src/distutils2/tests/pypi_server.py
+++ b/src/distutils2/tests/pypi_server.py
@@ -1,4 +1,4 @@
-"""Mocked PyPI Server implementation, to use in tests.
+"""Mock PyPI Server implementation, to use in tests.
This module also provides a simple test case to extend if you need to use
the PyPIServer all along your test case. Be sure to read the documentation
@@ -6,18 +6,28 @@
"""
import Queue
+import SocketServer
+import os.path
+import select
+import socket
import threading
-import time
-import urllib2
+
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
-import os.path
-import select
+from SimpleXMLRPCServer import SimpleXMLRPCServer
from distutils2.tests.support import unittest
PYPI_DEFAULT_STATIC_PATH = os.path.dirname(os.path.abspath(__file__)) + "/pypiserver"
+def use_xmlrpc_server(*server_args, **server_kwargs):
+ server_kwargs['serve_xmlrpc'] = True
+ return use_pypi_server(*server_args, **server_kwargs)
+
+def use_http_server(*server_args, **server_kwargs):
+ server_kwargs['serve_xmlrpc'] = False
+ return use_pypi_server(*server_args, **server_kwargs)
+
def use_pypi_server(*server_args, **server_kwargs):
"""Decorator to make use of the PyPIServer for test methods,
just when needed, and not for the entire duration of the testcase.
@@ -45,45 +55,65 @@
self.pypi.stop()
class PyPIServer(threading.Thread):
- """PyPI Mocked server.
+ """PyPI Mock server.
Provides a mocked version of the PyPI API's, to ease tests.
Support serving static content and serving previously given text.
"""
def __init__(self, test_static_path=None,
- static_filesystem_paths=["default"], static_uri_paths=["simple"]):
+ static_filesystem_paths=["default"],
+ static_uri_paths=["simple"], serve_xmlrpc=False) :
"""Initialize the server.
+
+ Default behavior is to start the HTTP server. You can either start the
+ xmlrpc server by setting xmlrpc to True. Caution: Only one server will
+ be started.
static_uri_paths and static_base_path are parameters used to provides
respectively the http_paths to serve statically, and where to find the
matching files on the filesystem.
"""
+ # we want to launch the server in a new dedicated thread, to not freeze
+ # tests.
threading.Thread.__init__(self)
self._run = True
- self.httpd = HTTPServer(('', 0), PyPIRequestHandler)
- self.httpd.RequestHandlerClass.log_request = lambda *_: None
- self.httpd.RequestHandlerClass.pypi_server = self
- self.address = (self.httpd.server_name, self.httpd.server_port)
- self.request_queue = Queue.Queue()
- self._requests = []
- self.default_response_status = 200
- self.default_response_headers = [('Content-type', 'text/plain')]
- self.default_response_data = "hello"
-
- # initialize static paths / filesystems
- self.static_uri_paths = static_uri_paths
- if test_static_path is not None:
- static_filesystem_paths.append(test_static_path)
- self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
- for path in static_filesystem_paths]
+ self._serve_xmlrpc = serve_xmlrpc
+
+ if not self._serve_xmlrpc:
+ self.server = HTTPServer(('', 0), PyPIRequestHandler)
+ self.server.RequestHandlerClass.pypi_server = self
+
+ self.request_queue = Queue.Queue()
+ self._requests = []
+ self.default_response_status = 200
+ self.default_response_headers = [('Content-type', 'text/plain')]
+ self.default_response_data = "hello"
+
+ # initialize static paths / filesystems
+ self.static_uri_paths = static_uri_paths
+ if test_static_path is not None:
+ static_filesystem_paths.append(test_static_path)
+ self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
+ for path in static_filesystem_paths]
+ else:
+ # xmlrpc server
+ self.server = PyPIXMLRPCServer(('', 0))
+ self.xmlrpc = XMLRPCMockIndex()
+ # register the xmlrpc methods
+ self.server.register_introspection_functions()
+ self.server.register_instance(self.xmlrpc)
+
+ self.address = (self.server.server_name, self.server.server_port)
+ # to not have unwanted outputs.
+ self.server.RequestHandlerClass.log_request = lambda *_: None
def run(self):
# loop because we can't stop it otherwise, for python < 2.6
while self._run:
- r, w, e = select.select([self.httpd], [], [], 0.5)
+ r, w, e = select.select([self.server], [], [], 0.5)
if r:
- self.httpd.handle_request()
+ self.server.handle_request()
def stop(self):
"""self shutdown is not supported for python < 2.6"""
@@ -193,3 +223,180 @@
self.send_header(header, value)
self.end_headers()
self.wfile.write(data)
+
+class PyPIXMLRPCServer(SimpleXMLRPCServer):
+ def server_bind(self):
+ """Override server_bind to store the server name."""
+ SocketServer.TCPServer.server_bind(self)
+ host, port = self.socket.getsockname()[:2]
+ self.server_name = socket.getfqdn(host)
+ self.server_port = port
+
+class MockDist(object):
+ """Fake distribution, used in the Mock PyPI Server"""
+ def __init__(self, name, version="1.0", hidden=False, url="http://url/",
+ type="source", filename="", size=10000,
+ digest="123456", downloads=7, has_sig=False,
+ python_version="source", comment="comment",
+ author="John Doe", author_email="john at doe.name",
+ maintainer="Main Tayner", maintainer_email="maintainer_mail",
+ project_url="http://project_url/", homepage="http://homepage/",
+ keywords="", platform="UNKNOWN", classifiers=[], licence="",
+ description="Description", summary="Summary", stable_version="",
+ ordering="", documentation_id="", code_kwalitee_id="",
+ installability_id="", obsoletes=[], obsoletes_dist=[],
+ provides=[], provides_dist=[], requires=[], requires_dist=[],
+ requires_external=[], requires_python=""):
+
+ # basic fields
+ self.name = name
+ self.version = version
+ self.hidden = hidden
+
+ # URL infos
+ self.url = url
+ self.digest = digest
+ self.downloads = downloads
+ self.has_sig = has_sig
+ self.python_version = python_version
+ self.comment = comment
+ self.type = type
+
+ # metadata
+ self.author = author
+ self.author_email = author_email
+ self.maintainer = maintainer
+ self.maintainer_email = maintainer_email
+ self.project_url = project_url
+ self.homepage = homepage
+ self.keywords = keywords
+ self.platform = platform
+ self.classifiers = classifiers
+ self.licence = licence
+ self.description = description
+ self.summary = summary
+ self.stable_version = stable_version
+ self.ordering = ordering
+ self.cheesecake_documentation_id = documentation_id
+ self.cheesecake_code_kwalitee_id = code_kwalitee_id
+ self.cheesecake_installability_id = installability_id
+
+ self.obsoletes = obsoletes
+ self.obsoletes_dist = obsoletes_dist
+ self.provides = provides
+ self.provides_dist = provides_dist
+ self.requires = requires
+ self.requires_dist = requires_dist
+ self.requires_external = requires_external
+ self.requires_python = requires_python
+
+ def url_infos(self):
+ return {
+ 'url': self.url,
+ 'packagetype': self.type,
+ 'filename': 'filename.tar.gz',
+ 'size': '6000',
+ 'md5_digest': self.digest,
+ 'downloads': self.downloads,
+ 'has_sig': self.has_sig,
+ 'python_version': self.python_version,
+ 'comment_text': self.comment,
+ }
+
+ def metadata(self):
+ return {
+ 'maintainer': self.maintainer,
+ 'project_url': [self.project_url],
+ 'maintainer_email': self.maintainer_email,
+ 'cheesecake_code_kwalitee_id': self.cheesecake_code_kwalitee_id,
+ 'keywords': self.keywords,
+ 'obsoletes_dist': self.obsoletes_dist,
+ 'requires_external': self.requires_external,
+ 'author': self.author,
+ 'author_email': self.author_email,
+ 'download_url': self.url,
+ 'platform': self.platform,
+ 'version': self.version,
+ 'obsoletes': self.obsoletes,
+ 'provides': self.provides,
+ 'cheesecake_documentation_id': self.cheesecake_documentation_id,
+ '_pypi_hidden': self.hidden,
+ 'description': self.description,
+ '_pypi_ordering': 19,
+ 'requires_dist': self.requires_dist,
+ 'requires_python': self.requires_python,
+ 'classifiers': [],
+ 'name': self.name,
+ 'licence': self.licence,
+ 'summary': self.summary,
+ 'home_page': self.homepage,
+ 'stable_version': self.stable_version,
+ 'provides_dist': self.provides_dist,
+ 'requires': self.requires,
+ 'cheesecake_installability_id': self.cheesecake_installability_id,
+ }
+
+ def search_result(self):
+ return {
+ '_pypi_ordering': 0,
+ 'version': self.version,
+ 'name': self.name,
+ 'summary': self.summary,
+ }
+
+class XMLRPCMockIndex(object):
+ """Mock XMLRPC server"""
+
+ def __init__(self, dists=[]):
+ self._dists = dists
+
+ def add_distributions(self, dists):
+ for dist in dists:
+ self._dists.append(MockDist(**dist))
+
+ def set_distributions(self, dists):
+ self._dists = []
+ self.add_distributions(dists)
+
+ def set_search_result(self, result):
+ """set a predefined search result"""
+ self._search_result = result
+
+ def _get_search_results(self):
+ results = []
+ for name in self._search_result:
+ found_dist = [d for d in self._dists if d.name == name]
+ if found_dist:
+ results.append(found_dist[0])
+ else:
+ dist = MockDist(name)
+ results.append(dist)
+ self._dists.append(dist)
+ return [r.search_result() for r in results]
+
+ def list_package(self):
+ return [d.name for d in self._dists]
+
+ def package_releases(self, package_name, show_hidden=False):
+ if show_hidden:
+ # return all
+ return [d.version for d in self._dists if d.name == package_name]
+ else:
+ # return only un-hidden
+ return [d.version for d in self._dists if d.name == package_name
+ and not d.hidden]
+
+ def release_urls(self, package_name, version):
+ return [d.url_infos() for d in self._dists
+ if d.name == package_name and d.version == version]
+
+ def release_data(self, package_name, version):
+ release = [d for d in self._dists
+ if d.name == package_name and d.version == version]
+ if release:
+ return release[0].metadata()
+ else:
+ return {}
+
+ def search(self, spec, operator="and"):
+ return self._get_search_results()
diff --git a/src/distutils2/tests/test_index_xmlrpc.py b/src/distutils2/tests/test_index_xmlrpc.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/tests/test_index_xmlrpc.py
@@ -0,0 +1,114 @@
+"""Tests for the distutils2.index.xmlrpc module."""
+
+from distutils2.tests.pypi_server import use_xmlrpc_server
+from distutils2.tests import run_unittest
+from distutils2.tests.support import unittest
+from distutils2.index.xmlrpc import Client, InvalidSearchField, ProjectNotFound
+
+
+class TestXMLRPCClient(unittest.TestCase):
+ def _get_client(self, server, *args, **kwargs):
+ return Client(server.full_address, *args, **kwargs)
+
+ @use_xmlrpc_server()
+ def test_search(self, server):
+ # test that the find method return a list of ReleasesList
+ client = self._get_client(server)
+ server.xmlrpc.set_search_result(['FooBar', 'Foo', 'FooFoo'])
+ results = [r.name for r in client.search(name='Foo')]
+ self.assertEqual(3, len(results))
+ self.assertIn('FooBar', results)
+ self.assertIn('Foo', results)
+ self.assertIn('FooFoo', results)
+
+ def test_search_bad_fields(self):
+ client = Client()
+ self.assertRaises(InvalidSearchField, client.search, invalid="test")
+
+ @use_xmlrpc_server()
+ def test_find(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name': 'FooBar', 'version': '1.1'},
+ {'name': 'FooBar', 'version': '1.2', 'url': 'http://some/url/'},
+ {'name': 'FooBar', 'version': '1.3', 'url': 'http://other/url/'},
+ ])
+
+ # use a lambda here to avoid an useless mock call
+ server.xmlrpc.list_releases = lambda *a, **k: ['1.1', '1.2', '1.3']
+
+ releases = client.find('FooBar (<=1.2)')
+ # dont call release_data and release_url; just return name and version.
+ self.assertEqual(2, len(releases))
+ versions = releases.get_versions()
+ self.assertIn('1.1', versions)
+ self.assertIn('1.2', versions)
+ self.assertNotIn('1.3', versions)
+
+ self.assertRaises(ProjectNotFound, client.find,'Foo')
+
+ @use_xmlrpc_server()
+ def test_get_releases(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name':'FooBar', 'version': '0.8', 'hidden': True},
+ {'name':'FooBar', 'version': '0.9', 'hidden': True},
+ {'name':'FooBar', 'version': '1.1'},
+ {'name':'FooBar', 'version': '1.2'},
+ ])
+ releases = client.get_releases('FooBar', False)
+ versions = releases.get_versions()
+ self.assertEqual(2, len(versions))
+ self.assertIn('1.1', versions)
+ self.assertIn('1.2', versions)
+
+ releases2 = client.get_releases('FooBar', True)
+ versions = releases2.get_versions()
+ self.assertEqual(4, len(versions))
+ self.assertIn('0.8', versions)
+ self.assertIn('0.9', versions)
+ self.assertIn('1.1', versions)
+ self.assertIn('1.2', versions)
+
+ @use_xmlrpc_server()
+ def test_get_distributions(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name':'FooBar', 'version': '1.1', 'url':
+ 'http://example.org/foobar-1.1-sdist.tar.gz',
+ 'digest': '1234567', 'type': 'sdist'},
+ {'name':'FooBar', 'version': '1.1', 'url':
+ 'http://example.org/foobar-1.1-bdist.tar.gz',
+ 'digest': '8912345', 'type': 'bdist'},
+ ])
+
+ releases = client.get_releases('FooBar', '1.1')
+ client.get_distributions('FooBar', '1.1')
+ release = releases.get_release('1.1')
+ self.assertTrue('http://example.org/foobar-1.1-sdist.tar.gz',
+ release['sdist'].url['url'])
+ self.assertTrue('http://example.org/foobar-1.1-bdist.tar.gz',
+ release['bdist'].url['url'])
+
+ @use_xmlrpc_server()
+ def test_get_metadata(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name':'FooBar',
+ 'version': '1.1',
+ 'keywords': '',
+ 'obsoletes_dist': ['FooFoo'],
+ 'requires_external': ['Foo'],
+ }])
+ release = client.get_metadata('FooBar', '1.1')
+ self.assertEqual(['Foo'], release.metadata['requires_external'])
+ self.assertEqual(['FooFoo'], release.metadata['obsoletes_dist'])
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestXMLRPCClient))
+ return suite
+
+if __name__ == '__main__':
+ run_unittest(test_suite())
--
Repository URL: http://hg.python.org/distutils2
More information about the Python-checkins
mailing list