[Python-checkins] distutils2: merged with Alexis

tarek.ziade python-checkins at python.org
Sat Nov 13 18:32:53 CET 2010


tarek.ziade pushed c9e22dae0036 to distutils2:

http://hg.python.org/distutils2/rev/c9e22dae0036
changeset:   823:c9e22dae0036
tag:         tip
parent:      818:a99e29d63071
parent:      822:1244d4ab6090
user:        Tarek Ziade <tarek at ziade.org>
date:        Sat Nov 13 18:32:47 2010 +0100
summary:     merged with Alexis
files:       

diff --git a/distutils2/install.py b/distutils2/install.py
--- a/distutils2/install.py
+++ b/distutils2/install.py
@@ -1,26 +1,30 @@
+from tempfile import mkdtemp
 import logging
+import shutil
+import os
+import errno
+
+from distutils2._backport.pkgutil import get_distributions
+from distutils2.depgraph import generate_graph
 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 the installation script.
+"""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 command to install.
-
-Please note that this installation *script* iis different of the installation
-*command*. While the command only install one distribution, the script installs
-all the dependencies from a distribution, in a secure way.
+installed distributions, and rely on the instalation commands to install.
 """
 
 
 class InstallationException(Exception):
-    pass
+    """Base exception for installation scripts"""
+
+
+class InstallationConflict(InstallationException):
+    """Raised when a conflict is detected"""
 
 
 def _update_infos(infos, new_infos):
@@ -32,8 +36,113 @@
             infos[key].extend(new_infos[key])
 
 
-def get_infos(requirements, index=None, installed=None,
-                     prefer_final=True):
+def move_files(files, destination=None):
+    """Move the list of files in the destination folder, keeping the same
+    structure.
+
+    Return a list of tuple (old, new) emplacement of files
+
+    :param files: a list of files to move.
+    :param destination: the destination directory to put on the files.
+                        if not defined, create a new one, using mkdtemp
+    """
+    if not destination:
+        destination = mkdtemp()
+
+    for old in files:
+        new = '%s%s' % (destination, old)
+
+        # try to make the paths.
+        try:
+            os.makedirs(os.path.dirname(new))
+        except OSError, e:
+            if e.errno == errno.EEXIST:
+                pass
+            else:
+                raise e
+        os.rename(old, new)
+        yield(old, new)
+
+
+def install_dists(dists, path=None):
+    """Install all distributions provided in dists, with the given prefix.
+
+    If an error occurs while installing one of the distributions, uninstall all
+    the installed distribution (in the context if this function).
+
+    Return a list of installed files.
+
+    :param dists: distributions to install
+    :param path: base path to install distribution on
+    """
+    if not path:
+        path = mkdtemp()
+
+    installed_dists, installed_files = [], []
+    for d in dists:
+        try:
+            installed_files.extend(d.install(path))
+            installed_dists.append(d)
+        except Exception, e :
+            for d in installed_dists:
+                d.uninstall()
+            raise e
+    return installed_files
+
+
+def install_from_infos(install=[], remove=[], conflicts=[], install_path=None):
+    """Install and remove the given distributions.
+
+    The function signature is made to be compatible with the one of get_infos.
+    The aim of this script is to povide a way to install/remove what's asked,
+    and to rollback if needed.
+
+    So, it's not possible to be in an inconsistant state, it could be either
+    installed, either uninstalled, not half-installed.
+
+    The process follow those steps:
+
+        1. Move all distributions that will be removed in a temporary location
+        2. Install all the distributions that will be installed in a temp. loc.
+        3. If the installation fails, rollback (eg. move back) those
+           distributions, or remove what have been installed.
+        4. Else, move the distributions to the right locations, and remove for
+           real the distributions thats need to be removed.
+
+    :param install: list of distributions that will be installed.
+    :param remove: list of distributions that will be removed.
+    :param conflicts: list of conflicting distributions, eg. that will be in
+                      conflict once the install and remove distribution will be
+                      processed.
+    :param install_path: the installation path where we want to install the
+                         distributions.
+    """
+    # first of all, if we have conflicts, stop here.
+    if conflicts:
+        raise InstallationConflict(conflicts)
+
+    # before removing the files, we will start by moving them away
+    # then, if any error occurs, we could replace them in the good place.
+    temp_files = {}  # contains lists of {dist: (old, new)} paths
+    if remove:
+        for dist in remove:
+            files = dist.get_installed_files()
+            temp_files[dist] = move_files(files)
+    try:
+        if install:
+            installed_files = install_dists(install, install_path)  # install to tmp first
+        for files in temp_files.values():
+            for old, new in files:
+                os.remove(new)
+
+    except Exception,e:
+        # if an error occurs, put back the files in the good place.
+        for files in temp_files.values():
+            for old, new in files:
+                shutil.move(new, old)
+
+
+def get_infos(requirements, index=None, installed=None, prefer_final=True):
     """Return the informations on what's going to be installed and upgraded.
 
     :param requirements: is a *string* containing the requirements for this
@@ -63,14 +172,13 @@
     # Get all the releases that match the requirements
     try:
         releases = index.get_releases(requirements)
-    except (ReleaseNotFound, ProjectNotFound):
+    except (ReleaseNotFound, ProjectNotFound), e:
         raise InstallationException('Release not found: "%s"' % requirements)
 
     # Pick up a release, and try to get the dependency tree
     release = releases.get_last(requirements, prefer_final=prefer_final)
 
     # Iter since we found something without conflicts
-    # XXX the metadata object is not used, remove the call or the binding
     metadata = release.fetch_metadata()
 
     # Get the distributions already_installed on the system
diff --git a/distutils2/pysetup b/distutils2/pysetup
new file mode 100755
--- /dev/null
+++ b/distutils2/pysetup
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+from distutils2.run import main
+
+if __name__ == "__main__":
+    main()
diff --git a/distutils2/run.py b/distutils2/run.py
--- a/distutils2/run.py
+++ b/distutils2/run.py
@@ -63,7 +63,12 @@
         attrs['script_name'] = os.path.basename(sys.argv[0])
 
     if 'script_args' not in attrs:
-        attrs['script_args'] = sys.argv[1:]
+        if sys.argv[1] == "help":
+            script_args = sys.argv[2:]
+            script_args.append("--help")
+        else:
+            script_args = sys.argv[1:]
+        attrs['script_args'] = script_args
 
     # Create the Distribution instance, using the remaining arguments
     # (ie. everything except distclass) to initialize it
diff --git a/distutils2/tests/test_install.py b/distutils2/tests/test_install.py
--- a/distutils2/tests/test_install.py
+++ b/distutils2/tests/test_install.py
@@ -1,14 +1,20 @@
-"""Tests for the distutils2.index.xmlrpc module."""
+"""Tests for the distutils2.install module."""
 
+import os
+from tempfile import mkstemp
+
+from distutils2 import install
+from distutils2.index.xmlrpc import Client
+from distutils2.metadata import DistributionMetadata
+from distutils2.tests import run_unittest
+from distutils2.tests.support import TempdirManager
 from distutils2.tests.pypi_server import use_xmlrpc_server
-from distutils2.tests import unittest, run_unittest
-from distutils2.index.xmlrpc import Client
-from distutils2.install import (get_infos, InstallationException)
-from distutils2.metadata import DistributionMetadata
+from distutils2.tests.support import unittest
 
 
-class FakeDist(object):
-    """A fake distribution object, for tests"""
+class InstalledDist(object):
+    """Distribution object, represent distributions currently installed on the
+    system"""
     def __init__(self, name, version, deps):
         self.name = name
         self.version = version
@@ -17,17 +23,49 @@
         self.metadata['Provides-Dist'] = ['%s (%s)' % (name, version)]
 
     def __repr__(self):
-        return '<FakeDist %s>' % self.name
+        return '<InstalledDist %s>' % self.name
 
 
-def get_fake_dists(dists):
+class ToInstallDist(object):
+    """Distribution that will be installed"""
+
+    def __init__(self, raise_error=False, files=False):
+        self._raise_error = raise_error
+        self._files = files
+        self.install_called = False
+        self.install_called_with = {}
+        self.uninstall_called = False
+        self._real_files = []
+        if files:
+            for f in range(0,3):
+               self._real_files.append(mkstemp())
+
+    def install(self, *args):
+        self.install_called = True
+        self.install_called_with = args
+        if self._raise_error:
+            raise Exception('Oops !')
+        return ['/path/to/foo', '/path/to/bar']
+
+    def uninstall(self, **args):
+        self.uninstall_called = True
+
+    def get_installed_files(self, **args):
+        if self._files:
+            return [f[1] for f in self._real_files]
+
+    def get_install(self, **args):
+        return self.get_installed_files()
+
+
+def get_installed_dists(dists):
     objects = []
     for (name, version, deps) in dists:
-        objects.append(FakeDist(name, version, deps))
+        objects.append(InstalledDist(name, version, deps))
     return objects
 
 
-class TestInstallWithDeps(unittest.TestCase):
+class TestInstall(TempdirManager, unittest.TestCase):
     def _get_client(self, server, *args, **kwargs):
         return Client(server.full_address, *args, **kwargs)
 
@@ -62,8 +100,8 @@
              'requires_dist': [],
              'url': archive_path},
             ])
-        installed = get_fake_dists([('bacon', '0.1', []),])
-        output = get_infos("choxie", index=client,
+        installed = get_installed_dists([('bacon', '0.1', []),])
+        output = install.get_infos("choxie", index=client,
                            installed=installed)
 
         # we dont have installed bacon as it's already installed on the system.
@@ -94,8 +132,8 @@
              'url': archive_path},
             ])
 
-        output = get_infos("choxie", index=client, installed=
-                           get_fake_dists([('bacon', '0.1', []),]))
+        output = install.get_infos("choxie", index=client, installed=
+                           get_installed_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.
@@ -128,8 +166,8 @@
             ])
         already_installed = [('bacon', '0.1', []),
                              ('chicken', '1.1', ['bacon (0.1)'])]
-        output = get_infos("choxie", index=client, installed=
-                           get_fake_dists(already_installed))
+        output = install.get_infos("choxie", index=client, installed=
+                           get_installed_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.
@@ -145,13 +183,112 @@
         # Test that the isntalled raises an exception if the project does not
         # exists.
         client = self._get_client(server)
-        self.assertRaises(InstallationException, get_infos,
+        self.assertRaises(install.InstallationException,
+                          install.get_infos,
                           'unexistant project', index=client)
 
+    def test_move_files(self):
+        # test that the files are really moved, and that the new path is
+        # returned.
+        path = self.mkdtemp()
+        newpath = self.mkdtemp()
+        files = [os.path.join(path, '%s' % x) for x in range(1, 20)]
+        for f in files:
+            file(f, 'a+')
+        output = [o for o in install.move_files(files, newpath)]
+
+        # check that output return the list of old/new places
+        for f in files:
+            self.assertIn((f, '%s%s' % (newpath, f)), output)
+
+        # remove the files
+        for f in [o[1] for o in output]:  # o[1] is the new place
+            os.remove(f)
+
+    def test_update_infos(self):
+        tests = [[{'foo': ['foobar', 'foo', 'baz'], 'baz': ['foo', 'foo']},
+                  {'foo': ['additional_content', 'yeah'],
+                   'baz': ['test', 'foo']},
+                  {'foo': ['foobar', 'foo', 'baz', 'additional_content', 'yeah'],
+                   'baz': ['foo', 'foo', 'test', 'foo']}],]
+
+        for dict1, dict2, expect in tests:
+            install._update_infos(dict1, dict2)
+            for key in expect.keys():
+                self.assertEqual(expect[key], dict1[key])
+
+    def test_install_dists_rollback(self):
+        # if one of the distribution installation fails, call uninstall on all
+        # installed distributions.
+
+        d1 = ToInstallDist()
+        d2 = ToInstallDist(raise_error=True)
+        self.assertRaises(Exception, install.install_dists, [d1, d2])
+        for dist in (d1, d2):
+            self.assertTrue(dist.install_called)
+        self.assertTrue(d1.uninstall_called)
+        self.assertFalse(d2.uninstall_called)
+
+    def test_install_dists_success(self):
+        # test that the install method is called on each of the distributions.
+        d1 = ToInstallDist()
+        d2 = ToInstallDist()
+        install.install_dists([d1, d2])
+        for dist in (d1, d2):
+            self.assertTrue(dist.install_called)
+        self.assertFalse(d1.uninstall_called)
+        self.assertFalse(d2.uninstall_called)
+
+    def test_install_from_infos_conflict(self):
+        # assert conflicts raise an exception
+        self.assertRaises(install.InstallationConflict,
+            install.install_from_infos,
+            conflicts=[ToInstallDist()])
+
+    def test_install_from_infos_remove_success(self):
+        old_install_dists = install.install_dists
+        install.install_dists = lambda x,y=None: None
+        try:
+            dists = []
+            for i in range(0,2):
+                dists.append(ToInstallDist(files=True))
+            install.install_from_infos(remove=dists)
+
+            # assert that the files have been removed
+            for dist in dists:
+                for f in dist.get_installed_files():
+                    self.assertFalse(os.path.exists(f))
+        finally:
+            install.install_dists = old_install_dists
+
+    def test_install_from_infos_remove_rollback(self):
+        # assert that if an error occurs, the removed files are restored.
+        remove = []
+        for i in range(0,2):
+            remove.append(ToInstallDist(files=True, raise_error=True))
+        to_install = [ToInstallDist(raise_error=True),
+                   ToInstallDist()]
+
+        install.install_from_infos(remove=remove, install=to_install)
+        # assert that the files are in the same place
+        # assert that the files have been removed
+        for dist in remove:
+            for f in dist.get_installed_files():
+                self.assertTrue(os.path.exists(f))
+
+    def test_install_from_infos_install_succes(self):
+        # assert that the distribution can be installed
+        install_path = "my_install_path"
+        to_install = [ToInstallDist(), ToInstallDist()]
+
+        install.install_from_infos(install=to_install,
+                                         install_path=install_path)
+        for dist in to_install:
+            self.assertEquals(dist.install_called_with, (install_path,))
 
 def test_suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TestInstallWithDeps))
+    suite.addTest(unittest.makeSuite(TestInstall))
     return suite
 
 if __name__ == '__main__':
diff --git a/docs/source/library/distutils2.install.rst b/docs/source/library/distutils2.install.rst
new file mode 100644
--- /dev/null
+++ b/docs/source/library/distutils2.install.rst
@@ -0,0 +1,25 @@
+==================
+Installation tools
+==================
+
+In addition to the install commands, distutils2 provides a set of tools to deal
+with installation of distributions.
+
+Basically, they're intended to download the distribution from indexes, to
+resolve the dependencies, and to provide a safe way to install all the
+distributions.
+
+You can find those tools in :module distutils2.install_tools:.
+
+
+API
+---
+
+.. automodule:: distutils2.install
+   :members:
+
+Example usage
+--------------
+
+Get the scheme of what's gonna be installed if we install "foobar":
+

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


More information about the Python-checkins mailing list