[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