[Python-checkins] distutils2: Added firsts bit of work about the installation script.

tarek.ziade python-checkins at python.org
Sun Aug 8 11:50:47 CEST 2010


tarek.ziade pushed 3d5db5aa326a to distutils2:

http://hg.python.org/distutils2/rev/3d5db5aa326a
changeset:   503:3d5db5aa326a
user:        Alexis Metaireau <ametaireau at gmail.com>
date:        Fri Aug 06 17:14:32 2010 +0200
summary:     Added firsts bit of work about the installation script.
files:       src/distutils2/install_with_deps.py, src/distutils2/tests/test_install_with_deps.py

diff --git a/src/distutils2/install_with_deps.py b/src/distutils2/install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/install_with_deps.py
@@ -0,0 +1,171 @@
+import logging
+from distutils2.index import wrapper
+from distutils2.index.errors import ProjectNotFound, ReleaseNotFound
+from distutils2.depgraph import generate_graph
+from distutils2._backport.pkgutil import get_distributions
+
+
+"""Provides installations scripts.
+
+The goal of this script is to install a release from the indexes (eg.
+PyPI), including the dependencies of the releases if needed.
+
+It uses the work made in pkgutil and by the index crawlers to browse the
+installed distributions, and rely on the instalation commands to install.
+"""
+
+
+def get_deps(requirements, index):
+    """Return the dependencies of a project, as a depgraph object.
+
+    Build a :class depgraph.DependencyGraph: for the given requirements
+
+    If the project does not uses Metadata < 1.1, dependencies can't be handled
+    from here, so it returns an empty list of dependencies.
+
+    :param requirements: is a string containing the version predicate to take
+                         the project name and version specifier from.
+    :param index: the index to use for making searches.
+    """
+    deps = []
+    release = index.get_release(requirements)
+    requires = release.metadata['Requires-Dist'] + release.metadata['Requires']
+    deps.append(release) # include the release we are computing deps.
+    for req in requires:
+        deps.extend(get_deps(req, index))
+    return deps
+
+
+def install(requirements, index=None, interactive=True, upgrade=True,
+            prefer_source=True, prefer_final=True):
+    """Given a list of distributions to install, a list of distributions to
+    remove, and a list of conflicts, proceed and do what's needed to be done.
+
+    :param requirements: is a *string* containing the requirements for this
+                         project (for instance "FooBar 1.1" or "BarBaz (<1.2)
+    :param index: If an index is specified, use this one, otherwise, use
+                  :class index.ClientWrapper: to get project metadatas.
+    :param interactive: if set to True, will prompt the user for interactions
+                        of needed. If false, use the default values.
+    :param upgrade: If a project exists in a newer version, does the script
+                    need to install the new one, or keep the already installed
+                    version.
+    :param prefer_source: used to tell if the user prefer source distributions
+                          over built dists.
+    :param prefer_final: if set to true, pick up the "final" versions (eg.
+                         stable) over the beta, alpha (not final) ones.
+    """
+    # get the default index if none is specified
+    if not index:
+        index = wrapper.WrapperClient()
+
+    # check if the project is already installed.
+    installed_release = get_installed_release(requirements)
+
+    # if a version that satisfy the requirements is already installed
+    if installed_release and (interactive or upgrade):
+        new_releases = index.get_releases(requirements)
+        if (new_releases.get_last(requirements).version >
+            installed_release.version):
+            if interactive:
+                # prompt the user to install the last version of the package.
+                # set upgrade here.
+                print "You want to install a package already installed on your"
+                "system. A new version exists, you could just use the version"
+                "you have, or upgrade to the latest version"
+
+                upgrade = raw_input("Do you want to install the most recent one ? (Y/n)") or "Y"
+                if upgrade in ('Y', 'y'):
+                    upgrade = True
+                else:
+                    upgrade = False
+            if not upgrade:
+                return
+
+    # create the depgraph from the dependencies of the release we want to
+    # install
+    graph = generate_graph(get_deps(requirements, index))
+    from ipdb import set_trace
+    set_trace()
+    installed = [] # to uninstall on errors
+    try:
+        for release in graph.adjacency_list:
+            dist = release.get_distribution()
+            install(dist)
+            installed.append(dist)
+            print "%s have been installed on your system" % requirements
+    except:
+        print "an error has occured, uninstalling"
+        for dist in installed:
+            uninstall_dist(dist)
+
+class InstallationException(Exception):
+    pass
+
+def get_install_info(requirements, index=None, already_installed=None):
+    """Return the informations on what's going to be installed and upgraded.
+
+    :param requirements: is a *string* containing the requirements for this
+                         project (for instance "FooBar 1.1" or "BarBaz (<1.2)")
+    :param index: If an index is specified, use this one, otherwise, use
+                  :class index.ClientWrapper: to get project metadatas.
+    :param already_installed: a list of already installed distributions.
+
+    The results are returned in a dict. For instance::
+
+        >>> get_install_info("FooBar (<=1.2)")
+        {'install': [<FooBar 1.1>], 'remove': [], 'conflict': []}
+
+    Conflict contains all the conflicting distributions, if there is a
+    conflict.
+
+    """
+    def update_infos(new_infos, infos):
+        for key, value in infos.items():
+            if key in new_infos:
+                infos[key].extend(new_infos[key])
+        return new_infos
+
+    if not index:
+        index = wrapper.ClientWrapper()
+    logging.info("searching releases for %s" % requirements)
+
+    # 1. get all the releases that match the requirements
+    try:
+        releases = index.get_releases(requirements)
+    except (ReleaseNotFound, ProjectNotFound), e:
+       raise InstallationException('Release not found: "%s"' % requirements)
+
+    # 2. pick up a release, and try to get the dependency tree
+    release = releases.get_last(requirements)
+    metadata = release.fetch_metadata()
+
+    # 3. get the distributions already_installed on the system
+    # 4. and add the one we want to install
+    if not already_installed:
+        already_installed = get_distributions()
+
+    logging.info("fetching %s %s dependencies" % (
+                 release.name, release.version))
+    distributions = already_installed + [release]
+    depgraph = generate_graph(distributions)
+
+    # store all the already_installed packages in a list, in case of rollback.
+    infos = {'install':[], 'remove': [], 'conflict': []}
+
+    # 5. get what the missing deps are
+    for dists in depgraph.missing.values():
+        if dists:
+            logging.info("missing dependencies found, installing them")
+            # we have missing deps
+            for dist in dists:
+                update_infos(get_install_info(dist, index, already_installed),
+                             infos)
+
+    # 6. fill in the infos
+    existing = [d for d in already_installed if d.name == release.name]
+    if existing:
+        infos['remove'].append(existing[0])
+        infos['conflict'].extend(depgraph.reverse_list[existing[0]])
+    infos['install'].append(release)
+    return infos
diff --git a/src/distutils2/tests/test_install_with_deps.py b/src/distutils2/tests/test_install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/tests/test_install_with_deps.py
@@ -0,0 +1,152 @@
+"""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
+from distutils2.install_with_deps import (get_install_info, 
+                                          InstallationException)
+from distutils2.metadata import DistributionMetadata
+
+class FakeDist(object):
+    """A fake distribution object, for tests"""
+    def __init__(self, name, version, deps):
+        self.name = name
+        self.version = version
+        self.metadata = DistributionMetadata()
+        self.metadata['Requires-Dist'] = deps
+        self.metadata['Provides-Dist'] = ['%s (%s)' % (name, version)]
+
+    def __repr__(self):
+        return '<FakeDist %s>' % self.name
+
+def get_fake_dists(dists):
+    objects = []
+    for (name, version, deps) in dists:
+       objects.append(FakeDist(name, version, deps))
+    return objects
+
+class TestInstallWithDeps(unittest.TestCase):
+    def _get_client(self, server, *args, **kwargs):
+        return Client(server.full_address, *args, **kwargs)
+
+    @use_xmlrpc_server()
+    def test_existing_deps(self, server):
+        # Test that the installer get the dependencies from the metadatas
+        # and ask the index for this dependencies.
+        # In this test case, we have choxie that is dependent from towel-stuff
+        # 0.1, which is in-turn dependent on bacon <= 0.2:
+        # choxie -> towel-stuff -> bacon.
+        # Each release metadata is not provided in metadata 1.2.
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (<= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.1',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        installed = get_fake_dists([('bacon', '0.1', []),])
+        output = get_install_info("choxie", index=client, 
+                                  already_installed=installed)
+
+        # we dont have installed bacon as it's already installed on the system.
+        self.assertEqual(0, len(output['remove']))
+        self.assertEqual(2, len(output['install']))
+        readable_output = [(o.name, '%s' % o.version) 
+                           for o in output['install']]
+        self.assertIn(('towel-stuff', '0.1'), readable_output)
+        self.assertIn(('choxie', '2.0.0.9'), readable_output)
+   
+    @use_xmlrpc_server()
+    def test_upgrade_existing_deps(self, server):
+        # Tests that the existing distributions can be upgraded if needed.
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (>= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.2',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        
+        output = get_install_info("choxie", index=client, already_installed= 
+                         get_fake_dists([('bacon', '0.1', []),]))
+        installed = [(o.name, '%s' % o.version) for o in output['install']]
+        
+        # we need bacon 0.2, but 0.1 is installed.
+        # So we expect to remove 0.1 and to install 0.2 instead.
+        remove = [(o.name, '%s' % o.version) for o in output['remove']]
+        self.assertIn(('choxie', '2.0.0.9'), installed)
+        self.assertIn(('towel-stuff', '0.1'), installed)
+        self.assertIn(('bacon', '0.2'), installed)
+        self.assertIn(('bacon', '0.1'), remove)
+        self.assertEqual(0, len(output['conflict']))
+
+    @use_xmlrpc_server()
+    def test_conflicts(self, server):
+        # Tests that conflicts are detected 
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (>= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.2',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        already_installed = [('bacon', '0.1', []), 
+                             ('chicken', '1.1', ['bacon (0.1)'])] 
+        output = get_install_info("choxie", index=client, already_installed= 
+                         get_fake_dists(already_installed))
+        
+        # we need bacon 0.2, but 0.1 is installed.
+        # So we expect to remove 0.1 and to install 0.2 instead.
+        installed = [(o.name, '%s' % o.version) for o in output['install']]
+        remove = [(o.name, '%s' % o.version) for o in output['remove']]
+        conflict = [(o.name, '%s' % o.version) for o in output['conflict']]
+        self.assertIn(('choxie', '2.0.0.9'), installed)
+        self.assertIn(('towel-stuff', '0.1'), installed)
+        self.assertIn(('bacon', '0.2'), installed)
+        self.assertIn(('bacon', '0.1'), remove)
+        self.assertIn(('chicken', '1.1'), conflict)
+
+    @use_xmlrpc_server()
+    def test_installation_unexisting_project(self, server):
+        # Test that the isntalled raises an exception if the project does not
+        # exists.
+        client = self._get_client(server)
+        self.assertRaises(InstallationException, get_install_info, 
+                          'unexistant project', index=client)
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestInstallWithDeps))
+    return suite
+
+if __name__ == '__main__':
+    run_unittest(test_suite())

--
Repository URL: http://hg.python.org/distutils2


More information about the Python-checkins mailing list