Commit da688181 authored by jim's avatar jim

Features:

- 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.

- Added a remove testing helper function that removes files or directories.


git-svn-id: http://svn.zope.org/repos/main/zc.buildout/trunk@73006 62d5b8a3-27da-0310-9561-8e5933582275
parent 984767c8
...@@ -23,10 +23,22 @@ Change History ...@@ -23,10 +23,22 @@ Change History
Feature Changes 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 - Adjusted the output for verbosity levels. Using a single -v option
no longer causes voluminous setuptools output. Uisng -vv and -vvv no longer causes voluminous setuptools output. Uisng -vv and -vvv
now triggers extra setuptools output. now triggers extra setuptools output.
- Added a remove testing helper function that removes files or directories.
1.0.0b20 (2007-02-08) 1.0.0b20 (2007-02-08)
===================== =====================
......
...@@ -1598,8 +1598,10 @@ database is shown. ...@@ -1598,8 +1598,10 @@ database is shown.
>>> print system(buildout+' -v'), >>> print system(buildout+' -v'),
zc.buildout.easy_install: Installing ['zc.buildout', 'setuptools'] zc.buildout.easy_install: Installing ['zc.buildout', 'setuptools']
zc.buildout.easy_install: We have a develop egg for zc.buildout 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 zc.buildout.easy_install: We have the best distribution that satisfies
setuptools setuptools
zc.buildout.easy_install: Picked version for setuptools = 0.6
<BLANKLINE> <BLANKLINE>
Configuration data: Configuration data:
[buildout] [buildout]
......
...@@ -45,6 +45,10 @@ buildout_and_setuptools_path = [ ...@@ -45,6 +45,10 @@ buildout_and_setuptools_path = [
pkg_resources.Requirement.parse('zc.buildout')).location, 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]} _versions = {sys.executable: '%d.%d' % sys.version_info[:2]}
def _get_version(executable): def _get_version(executable):
try: try:
...@@ -109,6 +113,7 @@ class Installer: ...@@ -109,6 +113,7 @@ class Installer:
always_unzip=False, always_unzip=False,
path=None, path=None,
newest=True, newest=True,
versions=None,
): ):
self._dest = dest self._dest = dest
self._links = list(links) self._links = list(links)
...@@ -123,6 +128,7 @@ class Installer: ...@@ -123,6 +128,7 @@ class Installer:
self._env = pkg_resources.Environment(path, self._env = pkg_resources.Environment(path,
python=_get_version(executable)) python=_get_version(executable))
self._index = _get_index(executable, index, links) self._index = _get_index(executable, index, links)
self._versions = versions or {}
def _satisfied(self, req): def _satisfied(self, req):
dists = [dist for dist in self._env[req.project_name] if dist in req] dists = [dist for dist in self._env[req.project_name] if dist in req]
...@@ -246,7 +252,7 @@ class Installer: ...@@ -246,7 +252,7 @@ class Installer:
if self._dest is not None: if self._dest is not None:
logger.info("Getting new distribution for %s", requirement) logger.info("Getting new distribution for %s", requirement)
# Retrieve the dist: # Retrieve the dist:grokonepage
index = self._index index = self._index
dist = index.obtain(requirement) dist = index.obtain(requirement)
if dist is None: if dist is None:
...@@ -336,6 +342,13 @@ class Installer: ...@@ -336,6 +342,13 @@ class Installer:
self._index = _get_index(self._executable, self._index = _get_index(self._executable,
self._index_url, self._links) 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 return dist
def _maybe_add_setuptools(self, ws, dist): def _maybe_add_setuptools(self, ws, dist):
...@@ -351,12 +364,27 @@ class Installer: ...@@ -351,12 +364,27 @@ class Installer:
"uses namespace packages but the distribution " "uses namespace packages but the distribution "
"does not require setuptools.", "does not require setuptools.",
dist) dist)
requirement = pkg_resources.Requirement.parse('setuptools') requirement = self._constrain(
pkg_resources.Requirement.parse('setuptools')
)
if ws.find(requirement) is None: if ws.find(requirement) is None:
dist = self._get_dist(requirement, ws, False) dist = self._get_dist(requirement, ws, False)
ws.add(dist) 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): def install(self, specs, working_set=None):
logger.debug('Installing %r', specs) logger.debug('Installing %r', specs)
...@@ -366,9 +394,11 @@ class Installer: ...@@ -366,9 +394,11 @@ class Installer:
if dest is not None and dest not in path: if dest is not None and dest not in path:
path.insert(0, dest) path.insert(0, dest)
requirements = [pkg_resources.Requirement.parse(spec) requirements = [self._constrain(pkg_resources.Requirement.parse(spec))
for spec in specs] for spec in specs]
if working_set is None: if working_set is None:
ws = pkg_resources.WorkingSet([]) ws = pkg_resources.WorkingSet([])
else: else:
...@@ -392,6 +422,7 @@ class Installer: ...@@ -392,6 +422,7 @@ class Installer:
ws.resolve(requirements) ws.resolve(requirements)
except pkg_resources.DistributionNotFound, err: except pkg_resources.DistributionNotFound, err:
[requirement] = err [requirement] = err
requirement = self._constrain(requirement)
if dest: if dest:
logger.debug('Getting required %s', requirement) logger.debug('Getting required %s', requirement)
dist = self._get_dist(requirement, ws, self._always_unzip) dist = self._get_dist(requirement, ws, self._always_unzip)
...@@ -405,7 +436,7 @@ class Installer: ...@@ -405,7 +436,7 @@ class Installer:
def build(self, spec, build_ext): def build(self, spec, build_ext):
logger.debug('Building %r', spec) logger.debug('Building %r', spec)
requirement = pkg_resources.Requirement.parse(spec) requirement = self._constrain(pkg_resources.Requirement.parse(spec))
dist = self._satisfied(requirement) dist = self._satisfied(requirement)
if dist is not None: if dist is not None:
...@@ -465,17 +496,18 @@ class Installer: ...@@ -465,17 +496,18 @@ class Installer:
def install(specs, dest, def install(specs, dest,
links=(), index=None, links=(), index=None,
executable=sys.executable, always_unzip=False, 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, installer = Installer(dest, links, index, executable, always_unzip, path,
newest) newest, versions)
return installer.install(specs, working_set) return installer.install(specs, working_set)
def build(spec, dest, build_ext, def build(spec, dest, build_ext,
links=(), index=None, links=(), index=None,
executable=sys.executable, executable=sys.executable,
path=None, newest=True): path=None, newest=True, versions=None):
installer = Installer(dest, links, index, executable, True, path, newest) installer = Installer(dest, links, index, executable, True, path, newest,
versions)
return installer.build(spec, build_ext) return installer.build(spec, build_ext)
......
...@@ -78,6 +78,11 @@ newest ...@@ -78,6 +78,11 @@ newest
not None, then the install function will search for the newest not None, then the install function will search for the newest
distributions that satisfy the requirements. 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 The install method returns a working set containing the distributions
needed to meet the given requirements. needed to meet the given requirements.
...@@ -185,6 +190,76 @@ can be useful when debugging. ...@@ -185,6 +190,76 @@ can be useful when debugging.
d demo-0.3-py2.4.egg d demo-0.3-py2.4.egg
d demoneeded-1.1-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 Script generation
----------------- -----------------
...@@ -506,6 +581,11 @@ newest ...@@ -506,6 +581,11 @@ newest
not None, then the install function will search for the newest not None, then the install function will search for the newest
distributions that satisfy the requirements. 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 Our link server included a source distribution that includes a simple
extension, extdemo.c:: extension, extdemo.c::
...@@ -553,7 +633,9 @@ The function returns the list of eggs ...@@ -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: Now if we look in our destination directory, we see we have an extdemo egg:
>>> ls(dest) >>> ls(dest)
- demo-0.2-py2.4.egg
d demo-0.3-py2.4.egg d demo-0.3-py2.4.egg
- demoneeded-1.0-py2.4.egg
d demoneeded-1.1-py2.4.egg d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.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: ...@@ -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' '/sample-install/extdemo-1.4-py2.4-linux-i686.egg'
>>> ls(dest) >>> ls(dest)
- demo-0.2-py2.4.egg
d demo-0.3-py2.4.egg d demo-0.3-py2.4.egg
- demoneeded-1.0-py2.4.egg
d demoneeded-1.1-py2.4.egg d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.egg d extdemo-1.4-py2.4-unix-i686.egg
...@@ -602,11 +686,32 @@ get an updated egg: ...@@ -602,11 +686,32 @@ get an updated egg:
... links=[link_server], index=link_server+'index/') ... links=[link_server], index=link_server+'index/')
'/sample-install/extdemo-1.5-py2.4-unix-i686.egg' '/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 d demo-0.3-py2.4.egg
- demoneeded-1.0-py2.4.egg
d demoneeded-1.1-py2.4.egg d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.egg d extdemo-1.4-py2.4-unix-i686.egg
d extdemo-1.5-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 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 ...@@ -657,10 +762,7 @@ Now if we look in our destination directory, we see we have an extdemo
egg link: egg link:
>>> ls(dest) >>> ls(dest)
d demo-0.3-py2.4.egg d extdemo-1.4-py2.4-unix-i686.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
- extdemo.egg-link - extdemo.egg-link
And that the source directory contains the compiled extension: And that the source directory contains the compiled extension:
......
...@@ -55,6 +55,13 @@ def ls(dir, *subs): ...@@ -55,6 +55,13 @@ def ls(dir, *subs):
def mkdir(*path): def mkdir(*path):
os.mkdir(os.path.join(*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): def rmdir(*path):
shutil.rmtree(os.path.join(*path)) shutil.rmtree(os.path.join(*path))
...@@ -202,6 +209,7 @@ def buildoutSetUp(test): ...@@ -202,6 +209,7 @@ def buildoutSetUp(test):
cat = cat, cat = cat,
mkdir = mkdir, mkdir = mkdir,
rmdir = rmdir, rmdir = rmdir,
remove = remove,
tmpdir = tmpdir, tmpdir = tmpdir,
write = write, write = write,
system = system, system = system,
......
...@@ -37,6 +37,10 @@ number of names to the test namespace: ...@@ -37,6 +37,10 @@ number of names to the test namespace:
Remove a directory. The directory path is provided as one or Remove a directory. The directory path is provided as one or
more strings, to be joined with os.path.join. 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)`` ``tmpdir(name)``
Create a temporary directory with the given name. The directory Create a temporary directory with the given name. The directory
will be automatically removed at the end of the test. The path of will be automatically removed at the end of the test. The path of
......
...@@ -1436,6 +1436,8 @@ def test_suite(): ...@@ -1436,6 +1436,8 @@ def test_suite():
'zc.buildout.egg'), 'zc.buildout.egg'),
(re.compile('creating \S*setup.cfg'), 'creating setup.cfg'), (re.compile('creating \S*setup.cfg'), 'creating setup.cfg'),
(re.compile('hello\%ssetup' % os.path.sep), 'hello/setup'), (re.compile('hello\%ssetup' % os.path.sep), 'hello/setup'),
(re.compile('Picked version for (\S+) = \S+'),
'Picked version for \\1 = V.V'),
]) ])
), ),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment