[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