diff --git a/CHANGES.txt b/CHANGES.txt index 6ccfc1b315b381bd7f72b3a970a1e4a92defe6ad..24bcd6ae61558061802073d02432dbeb826d57ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,10 +23,22 @@ Change History Feature Changes --------------- +- The easy_install module install and build functions now accept a + versions argument that supplied to mapping from project name to + version numbers. This can be used to fix version numbers for + required distributions and their depenencies. + + When a version isn't fixed, using either a versions option or using + a fixed version number in a requirement, then a debug log message is + emitted indicating the version picked. This is useful for setting + versions options. + - Adjusted the output for verbosity levels. Using a single -v option no longer causes voluminous setuptools output. Uisng -vv and -vvv now triggers extra setuptools output. +- Added a remove testing helper function that removes files or directories. + 1.0.0b20 (2007-02-08) ===================== diff --git a/src/zc/buildout/buildout.txt b/src/zc/buildout/buildout.txt index 7e15121e42f4dd71eff2baad95cdb95b5fe06ed6..65bdf4e5992730720e4f4d7b8778ae4e2f0757d9 100644 --- a/src/zc/buildout/buildout.txt +++ b/src/zc/buildout/buildout.txt @@ -1598,8 +1598,10 @@ database is shown. >>> print system(buildout+' -v'), zc.buildout.easy_install: Installing ['zc.buildout', 'setuptools'] zc.buildout.easy_install: We have a develop egg for zc.buildout + zc.buildout.easy_install: Picked version for zc.buildout = 1.0.0 zc.buildout.easy_install: We have the best distribution that satisfies setuptools + zc.buildout.easy_install: Picked version for setuptools = 0.6 <BLANKLINE> Configuration data: [buildout] diff --git a/src/zc/buildout/easy_install.py b/src/zc/buildout/easy_install.py index cc609ad6484b8f7281373efc1d26f957125f76ba..eff8e9b83145adfa197007b25204c95fde2a0055 100644 --- a/src/zc/buildout/easy_install.py +++ b/src/zc/buildout/easy_install.py @@ -45,6 +45,10 @@ buildout_and_setuptools_path = [ pkg_resources.Requirement.parse('zc.buildout')).location, ] +class IncompatibleVersionError(zc.buildout.UserError): + """A specified version is incompatible with a given requirement. + """ + _versions = {sys.executable: '%d.%d' % sys.version_info[:2]} def _get_version(executable): try: @@ -109,6 +113,7 @@ class Installer: always_unzip=False, path=None, newest=True, + versions=None, ): self._dest = dest self._links = list(links) @@ -123,6 +128,7 @@ class Installer: self._env = pkg_resources.Environment(path, python=_get_version(executable)) self._index = _get_index(executable, index, links) + self._versions = versions or {} def _satisfied(self, req): dists = [dist for dist in self._env[req.project_name] if dist in req] @@ -246,7 +252,7 @@ class Installer: if self._dest is not None: logger.info("Getting new distribution for %s", requirement) - # Retrieve the dist: + # Retrieve the dist:grokonepage index = self._index dist = index.obtain(requirement) if dist is None: @@ -336,6 +342,13 @@ class Installer: self._index = _get_index(self._executable, self._index_url, self._links) + # Check whether we picked a version and, if we did, report it: + if not ( + len(requirement.specs) == 1 and requirement.specs[0][0] == '==' + ): + logger.debug('Picked version for %s = %s', + dist.project_name, dist.version) + return dist def _maybe_add_setuptools(self, ws, dist): @@ -351,12 +364,27 @@ class Installer: "uses namespace packages but the distribution " "does not require setuptools.", dist) - requirement = pkg_resources.Requirement.parse('setuptools') + requirement = self._constrain( + pkg_resources.Requirement.parse('setuptools') + ) if ws.find(requirement) is None: dist = self._get_dist(requirement, ws, False) ws.add(dist) + def _constrain(self, requirement): + version = self._versions.get(requirement.project_name) + if version: + if version not in requirement: + logger.error("The version, %s, is not consistent with the " + "requirement, %s", version, requirement) + raise IncompatibleVersionError("Bad version", version) + + requirement = pkg_resources.Requirement.parse( + "%s ==%s" % (requirement.project_name, version)) + + return requirement + def install(self, specs, working_set=None): logger.debug('Installing %r', specs) @@ -366,9 +394,11 @@ class Installer: if dest is not None and dest not in path: path.insert(0, dest) - requirements = [pkg_resources.Requirement.parse(spec) + requirements = [self._constrain(pkg_resources.Requirement.parse(spec)) for spec in specs] + + if working_set is None: ws = pkg_resources.WorkingSet([]) else: @@ -392,6 +422,7 @@ class Installer: ws.resolve(requirements) except pkg_resources.DistributionNotFound, err: [requirement] = err + requirement = self._constrain(requirement) if dest: logger.debug('Getting required %s', requirement) dist = self._get_dist(requirement, ws, self._always_unzip) @@ -405,7 +436,7 @@ class Installer: def build(self, spec, build_ext): logger.debug('Building %r', spec) - requirement = pkg_resources.Requirement.parse(spec) + requirement = self._constrain(pkg_resources.Requirement.parse(spec)) dist = self._satisfied(requirement) if dist is not None: @@ -465,17 +496,18 @@ class Installer: def install(specs, dest, links=(), index=None, executable=sys.executable, always_unzip=False, - path=None, working_set=None, newest=True): + path=None, working_set=None, newest=True, versions=None): installer = Installer(dest, links, index, executable, always_unzip, path, - newest) + newest, versions) return installer.install(specs, working_set) def build(spec, dest, build_ext, links=(), index=None, executable=sys.executable, - path=None, newest=True): - installer = Installer(dest, links, index, executable, True, path, newest) + path=None, newest=True, versions=None): + installer = Installer(dest, links, index, executable, True, path, newest, + versions) return installer.build(spec, build_ext) diff --git a/src/zc/buildout/easy_install.txt b/src/zc/buildout/easy_install.txt index 1c2e00780a2259fce0d6fa3de555a640cc9ac79e..0afc0da49b7ca98940b0ea3b52fa5d75a22d66b0 100644 --- a/src/zc/buildout/easy_install.txt +++ b/src/zc/buildout/easy_install.txt @@ -78,6 +78,11 @@ newest not None, then the install function will search for the newest distributions that satisfy the requirements. +versions + A dictionary mapping project names to version numbers to be used + when selecting distributions. This can be used to specify a set of + distribution versions independent of other requirements. + The install method returns a working set containing the distributions needed to meet the given requirements. @@ -185,6 +190,76 @@ can be useful when debugging. d demo-0.3-py2.4.egg d demoneeded-1.1-py2.4.egg +Specifying version information indepenent of requirements +--------------------------------------------------------- + +Sometimes it's useful to specify version information indepenent of +normal requirements specifications. For example, a buildout may need +to lock down a set of versions, without having to put put version +numbers in setup files or part definitions. If a dictionary is passed +to the install function, mapping project names to version numbers, +then the versions numbers will be used. + + >>> ws = zc.buildout.easy_install.install( + ... ['demo'], dest, links=[link_server], index=link_server+'index/', + ... versions = dict(demo='0.2', demoneeded='1.0')) + >>> [d.version for d in ws] + ['0.2', '1.0'] + +In this example, we specified a version for demoneeded, even though we +didn't define a requirement for it. The versions specified apply to +depenencies as well as the specified requirements. + +If we specify a version that's incompatible with a requirement, then +we'll get an error: + + >>> from zope.testing.loggingsupport import InstalledHandler + >>> handler = InstalledHandler('zc.buildout.easy_install') + >>> import logging + >>> logging.getLogger('zc.buildout.easy_install').propagate = False + + >>> ws = zc.buildout.easy_install.install( + ... ['demo >0.2'], dest, links=[link_server], + ... index=link_server+'index/', + ... versions = dict(demo='0.2', demoneeded='1.0')) + Traceback (most recent call last): + ... + IncompatibleVersionError: Bad version 0.2 + + >>> print handler + zc.buildout.easy_install DEBUG + Installing ['demo >0.2'] + zc.buildout.easy_install ERROR + The version, 0.2, is not consistent with the requirement, demo>0.2 + + >>> handler.clear() + +If no versions are specified, a debugging message will be output +reporting that a version was picked automatically: + + >>> ws = zc.buildout.easy_install.install( + ... ['demo'], dest, links=[link_server], index=link_server+'index/', + ... ) + + >>> print handler + zc.buildout.easy_install DEBUG + Installing ['demo'] + zc.buildout.easy_install DEBUG + We have the best distribution that satisfies + demo + zc.buildout.easy_install DEBUG + Picked version for demo = 0.3 + zc.buildout.easy_install DEBUG + Getting required demoneeded + zc.buildout.easy_install DEBUG + We have the best distribution that satisfies + demoneeded + zc.buildout.easy_install DEBUG + Picked version for demoneeded = 1.1 + + >>> handler.uninstall() + >>> logging.getLogger('zc.buildout.easy_install').propagate = True + Script generation ----------------- @@ -506,6 +581,11 @@ newest not None, then the install function will search for the newest distributions that satisfy the requirements. +versions + A dictionary mapping project names to version numbers to be used + when selecting distributions. This can be used to specify a set of + distribution versions independent of other requirements. + Our link server included a source distribution that includes a simple extension, extdemo.c:: @@ -553,7 +633,9 @@ The function returns the list of eggs Now if we look in our destination directory, we see we have an extdemo egg: >>> ls(dest) + - demo-0.2-py2.4.egg d demo-0.3-py2.4.egg + - demoneeded-1.0-py2.4.egg d demoneeded-1.1-py2.4.egg d extdemo-1.4-py2.4-unix-i686.egg @@ -589,7 +671,9 @@ If we run build with newest set to False, we won't get an update: '/sample-install/extdemo-1.4-py2.4-linux-i686.egg' >>> ls(dest) + - demo-0.2-py2.4.egg d demo-0.3-py2.4.egg + - demoneeded-1.0-py2.4.egg d demoneeded-1.1-py2.4.egg d extdemo-1.4-py2.4-unix-i686.egg @@ -602,11 +686,32 @@ get an updated egg: ... links=[link_server], index=link_server+'index/') '/sample-install/extdemo-1.5-py2.4-unix-i686.egg' + >>> ls(dest) + - demo-0.2-py2.4.egg d demo-0.3-py2.4.egg + - demoneeded-1.0-py2.4.egg d demoneeded-1.1-py2.4.egg d extdemo-1.4-py2.4-unix-i686.egg d extdemo-1.5-py2.4-unix-i686.egg +The versions option also influences the versions used. For example, +if we specify a version for extdemo, then that will be used, even +though it isn't the newest. Let's clean out the destimation directory +first: + + >>> import os + >>> for name in os.listdir(dest): + ... remove(dest, name) + + >>> zc.buildout.easy_install.build( + ... 'extdemo', dest, + ... {'include-dirs': os.path.join(sample_buildout, 'include')}, + ... links=[link_server], index=link_server+'index/', + ... versions=dict(extdemo='1.4')) + '/sample-install/extdemo-1.4-py2.4-unix-i686.egg' + + >>> ls(dest) + d extdemo-1.4-py2.4-unix-i686.egg Handling custom build options for extensions in develop eggs ------------------------------------------------------------ @@ -657,10 +762,7 @@ Now if we look in our destination directory, we see we have an extdemo egg link: >>> ls(dest) - d demo-0.3-py2.4.egg - d demoneeded-1.1-py2.4.egg - d extdemo-1.4-py2.4-linux-i686.egg - d extdemo-1.5-py2.4-linux-i686.egg + d extdemo-1.4-py2.4-unix-i686.egg - extdemo.egg-link And that the source directory contains the compiled extension: diff --git a/src/zc/buildout/testing.py b/src/zc/buildout/testing.py index a7e55122328e8e94e061a0748edd334ffa10b2de..a423931ffbe83df56a57a1b7e2e3a53a6a622366 100644 --- a/src/zc/buildout/testing.py +++ b/src/zc/buildout/testing.py @@ -55,6 +55,13 @@ def ls(dir, *subs): def mkdir(*path): os.mkdir(os.path.join(*path)) +def remove(*path): + path = os.path.join(*path) + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + def rmdir(*path): shutil.rmtree(os.path.join(*path)) @@ -202,6 +209,7 @@ def buildoutSetUp(test): cat = cat, mkdir = mkdir, rmdir = rmdir, + remove = remove, tmpdir = tmpdir, write = write, system = system, diff --git a/src/zc/buildout/testing.txt b/src/zc/buildout/testing.txt index c3e3281f06ad056c16bbde4542e7165f7ea2df82..68bd58c350fb1d2620bc9dd6c7158e11e12c3052 100644 --- a/src/zc/buildout/testing.txt +++ b/src/zc/buildout/testing.txt @@ -37,6 +37,10 @@ number of names to the test namespace: Remove a directory. The directory path is provided as one or more strings, to be joined with os.path.join. +``remove(*path)`` + Remove a directory or file. The path is provided as one or + more strings, to be joined with os.path.join. + ``tmpdir(name)`` Create a temporary directory with the given name. The directory will be automatically removed at the end of the test. The path of diff --git a/src/zc/buildout/tests.py b/src/zc/buildout/tests.py index b6d82bc051c11b67bde3ff287370f0d62bff7e82..8e5825f511cf5918d9704596260b8b71af2e7b74 100644 --- a/src/zc/buildout/tests.py +++ b/src/zc/buildout/tests.py @@ -1436,6 +1436,8 @@ def test_suite(): 'zc.buildout.egg'), (re.compile('creating \S*setup.cfg'), 'creating setup.cfg'), (re.compile('hello\%ssetup' % os.path.sep), 'hello/setup'), + (re.compile('Picked version for (\S+) = \S+'), + 'Picked version for \\1 = V.V'), ]) ),