Commit a4781e45 authored by Rafael Oliveira's avatar Rafael Oliveira

Fix #153: buildout should cache working set environments

parent 5fbec349
...@@ -4,7 +4,8 @@ Change History ...@@ -4,7 +4,8 @@ Change History
2.0.4 (unreleased) 2.0.4 (unreleased)
================== ==================
- Nothing changed yet. - Fix #153: buildout should cache working set environments
[rafaelbco]
2.0.3 (2015-10-02) 2.0.3 (2015-10-02)
......
...@@ -43,6 +43,8 @@ setup( ...@@ -43,6 +43,8 @@ setup(
+ '\n' + + '\n' +
read('src', 'zc', 'recipe', 'egg', 'api.rst') read('src', 'zc', 'recipe', 'egg', 'api.rst')
+ '\n' + + '\n' +
read('src', 'zc', 'recipe', 'egg', 'working_set_caching.rst')
+ '\n' +
'Download\n' 'Download\n'
'*********\n' '*********\n'
), ),
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"""Install packages as eggs """Install packages as eggs
""" """
import copy
import logging import logging
import os import os
import re import re
...@@ -23,6 +24,8 @@ import zc.buildout.easy_install ...@@ -23,6 +24,8 @@ import zc.buildout.easy_install
class Eggs(object): class Eggs(object):
_WORKING_SET_CACHE_ATTR_NAME = '_zc_recipe_egg_working_set_cache'
def __init__(self, buildout, name, options): def __init__(self, buildout, name, options):
self.buildout = buildout self.buildout = buildout
self.name = name self.name = name
...@@ -57,30 +60,27 @@ class Eggs(object): ...@@ -57,30 +60,27 @@ class Eggs(object):
This is intended for reuse by similar recipes. This is intended for reuse by similar recipes.
""" """
options = self.options options = self.options
buildout_section = self.buildout['buildout']
# Backward compat. :( # Backward compat. :(
options['executable'] = sys.executable options['executable'] = sys.executable
distributions = [ orig_distributions = [
r.strip() r.strip()
for r in options.get('eggs', self.name).split('\n') for r in options.get('eggs', self.name).split('\n')
if r.strip()] if r.strip()
orig_distributions = distributions[:] ]
distributions.extend(extra)
ws = self._working_set(
if self.buildout['buildout'].get('offline') == 'true': distributions=orig_distributions + list(extra),
ws = zc.buildout.easy_install.working_set( develop_eggs_dir=options['develop-eggs-directory'],
distributions, eggs_dir=options['eggs-directory'],
[options['develop-eggs-directory'], options['eggs-directory']] offline=(buildout_section.get('offline') == 'true'),
) newest=(buildout_section.get('newest') == 'true'),
else: links=self.links,
ws = zc.buildout.easy_install.install( index=self.index,
distributions, options['eggs-directory'], allow_hosts=self.allow_hosts,
links=self.links, )
index=self.index,
path=[options['develop-eggs-directory']],
newest=self.buildout['buildout'].get('newest') == 'true',
allow_hosts=self.allow_hosts)
return orig_distributions, ws return orig_distributions, ws
...@@ -90,6 +90,73 @@ class Eggs(object): ...@@ -90,6 +90,73 @@ class Eggs(object):
update = install update = install
def _working_set(
self,
distributions,
eggs_dir,
develop_eggs_dir,
offline=False,
newest=True,
links=(),
index=None,
allow_hosts=('*',),
):
"""Helper function to build a working set.
Return an instance of `pkg_resources.WorkingSet`.
Results are cached. The cache key is composed by all the arguments
passed to the function. See also `self._get_cache_storage()`.
"""
cache_storage = self._get_cache_storage()
cache_key = (
tuple(distributions),
eggs_dir,
develop_eggs_dir,
offline,
newest,
tuple(links),
index,
tuple(allow_hosts),
)
if cache_key not in cache_storage:
if offline:
ws = zc.buildout.easy_install.working_set(
distributions,
[develop_eggs_dir, eggs_dir]
)
else:
ws = zc.buildout.easy_install.install(
distributions, eggs_dir,
links=links,
index=index,
path=[develop_eggs_dir],
newest=newest,
allow_hosts=allow_hosts)
cache_storage[cache_key] = ws
# `pkg_resources.WorkingSet` instances are mutable, so we need to return
# a copy.
return copy.deepcopy(cache_storage[cache_key])
def _get_cache_storage(self):
"""Return a mapping where to store generated working sets.
The cache storage is stored in an attribute of `self.buildout` with
name given by `self._WORKING_SET_CACHE_ATTR_NAME`.
"""
cache_storage = getattr(
self.buildout,
self._WORKING_SET_CACHE_ATTR_NAME,
None)
if cache_storage is None:
cache_storage = {}
setattr(
self.buildout,
self._WORKING_SET_CACHE_ATTR_NAME,
cache_storage)
return cache_storage
class Scripts(Eggs): class Scripts(Eggs):
......
...@@ -109,6 +109,16 @@ def test_suite(): ...@@ -109,6 +109,16 @@ def test_suite():
''), ''),
]), ]),
), ),
doctest.DocFileSuite(
'working_set_caching.rst',
setUp=setUp, tearDown=zc.buildout.testing.buildoutTearDown,
optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS,
checker=renormalizing.RENormalizing([
zc.buildout.testing.normalize_path,
zc.buildout.testing.normalize_endings,
zc.buildout.testing.not_found,
])
),
)) ))
return suite return suite
......
Working set caching
===================
Working sets are cached, to improve speed on buildouts with multiple similar
parts based on ``zc.recipe.egg``.
The egg-recipe instance's ``_working_set`` helper method is used to make
the caching easier. It does the same job as ``working_set()`` but with some
differences:
- The signature is different: all information needed to build the working set
is passed as parameters.
- The return value is simpler: only an instance of ``pkg_resources.WorkingSet``
is returned.
Here's an example:
>>> from zc.buildout import testing
>>> from zc.recipe.egg.egg import Eggs
>>> import os
>>> import pkg_resources
>>> recipe = Eggs(buildout=testing.Buildout(), name='fake-part', options={})
>>> eggs_dir = os.path.join(sample_buildout, 'eggs')
>>> develop_eggs_dir = os.path.join(sample_buildout, 'develop-eggs')
>>> testing.install_develop('zc.recipe.egg', develop_eggs_dir)
>>> ws = recipe._working_set(
... distributions=['zc.recipe.egg', 'demo<0.3'],
... eggs_dir=eggs_dir,
... develop_eggs_dir=develop_eggs_dir,
... index=link_server,
... )
Getting...
>>> isinstance(ws, pkg_resources.WorkingSet)
True
>>> sorted(dist.project_name for dist in ws)
['demo', 'demoneeded', 'setuptools', 'zc.buildout', 'zc.recipe.egg']
We'll monkey patch a method in the ``easy_install`` module in order to verify if
the cache is working:
>>> import zc.buildout.easy_install
>>> old_install = zc.buildout.easy_install.Installer.install
>>> def new_install(*args, **kwargs):
... print('Building working set.')
... return old_install(*args, **kwargs)
>>> zc.buildout.easy_install.Installer.install = new_install
Now we check if the caching is working by verifying if the same working set is
built only once.
>>> ws_args_1 = dict(
... distributions=['demo>=0.1'],
... eggs_dir=eggs_dir,
... develop_eggs_dir=develop_eggs_dir,
... offline=True,
... )
>>> ws_args_2 = dict(ws_args_1)
>>> ws_args_2['distributions'] = ['demoneeded']
>>> recipe._working_set(**ws_args_1)
Building working set.
<pkg_resources.WorkingSet object at ...>
>>> recipe._working_set(**ws_args_1)
<pkg_resources.WorkingSet object at ...>
>>> recipe._working_set(**ws_args_2)
Building working set.
<pkg_resources.WorkingSet object at ...>
>>> recipe._working_set(**ws_args_1)
<pkg_resources.WorkingSet object at ...>
>>> recipe._working_set(**ws_args_2)
<pkg_resources.WorkingSet object at ...>
Undo monkey patch:
>>> zc.buildout.easy_install.Installer.install = old_install
Since ``pkg_resources.WorkingSet`` instances are mutable, we must ensure that
``working_set()`` always returns a pristine copy. Otherwise callers would be
able to modify instances inside the cache.
Let's create a working set:
>>> ws = recipe._working_set(**ws_args_1)
>>> sorted(dist.project_name for dist in ws)
['demo', 'demoneeded']
Now we add a distribution to it:
>>> dist = pkg_resources.get_distribution('zc.recipe.egg')
>>> ws.add(dist)
>>> sorted(dist.project_name for dist in ws)
['demo', 'demoneeded', 'zc.recipe.egg']
Let's call the working_set function again and see if the result remains valid:
>>> ws = recipe._working_set(**ws_args_1)
>>> sorted(dist.project_name for dist in ws)
['demo', 'demoneeded']
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