Commit 5238c83a authored by Kazuhiko Shiozaki's avatar Kazuhiko Shiozaki Committed by Xavier Thompson

[feat] Add dependencies in __buildout_signature__

Add referred parts' hash strings in __buildout_signature__, that
invokes rebuild of a part when one of its (recursive) dependencies
are modified.

Also remove duplicates and sort entries in __buildout_signature__.
parent 75b53392
......@@ -465,6 +465,8 @@ class Buildout(DictMixin):
self._raw = _unannotate(data)
self._data = {}
self._parts = []
self._initializing = []
self._signature_cache = {}
# provide some defaults before options are parsed
# because while parsing options those attributes might be
......@@ -824,9 +826,7 @@ class Buildout(DictMixin):
_save_options(section, self[section], sys.stdout)
print_()
# compute new part recipe signatures
self._compute_part_signatures(install_parts)
del self._signature_cache
# uninstall parts that are no-longer used or who's configs
# have changed
......@@ -1024,17 +1024,6 @@ class Buildout(DictMixin):
self._logger.warning(
"Unexpected entry, %r, in develop-eggs directory.", f)
def _compute_part_signatures(self, parts):
# Compute recipe signature and add to options
for part in parts:
options = self.get(part)
if options is None:
options = self[part] = {}
recipe, entry = _recipe(options)
req = pkg_resources.Requirement.parse(recipe)
sig = _dists_sig(pkg_resources.working_set.resolve([req]))
options['__buildout_signature__'] = ' '.join(sig)
def _read_installed_part_options(self):
old = self['buildout']['installed']
if old and os.path.isfile(old):
......@@ -1413,13 +1402,27 @@ class Buildout(DictMixin):
v = v.replace(os.getcwd(), base_path)
print_("%s =%s" % (k, v))
def initialize(self, options, reqs, entry):
recipe_class = _install_and_load(reqs, 'zc.buildout', entry, self)
try:
sig = self._signature_cache[reqs]
except KeyError:
req = pkg_resources.Requirement.parse(reqs)
sig = self._signature_cache[reqs] = sorted(set(
_dists_sig(pkg_resources.working_set.resolve([req]))))
self._initializing.append((options, sig))
try:
recipe = recipe_class(self, options.name, options)
options['__buildout_signature__']
finally:
del self._initializing[-1]
return recipe
def __getitem__(self, section):
__doing__ = 'Getting section %s.', section
try:
return self._data[section]
options = self._data[section]
except KeyError:
pass
try:
data = self._raw[section]
except KeyError:
......@@ -1428,6 +1431,15 @@ class Buildout(DictMixin):
options = self.Options(self, section, data)
self._data[section] = options
options._initialize()
if self._initializing:
caller = self._initializing[-1][0]
if 'buildout' != section and not (
section in caller.depends or
# Do not only check the caller,
# because of circular dependencies during substitutions.
section in (x[0].name for x in self._initializing)):
caller.depends.add(section)
return options
def __setitem__(self, name, data):
......@@ -1510,6 +1522,7 @@ class Options(DictMixin):
self._raw = data
self._cooked = {}
self._data = {}
self.depends = set()
def _initialize(self):
name = self.name
......@@ -1531,16 +1544,15 @@ class Options(DictMixin):
self.buildout[dname]
if self.get('recipe'):
self.initialize()
self.recipe = self.buildout.initialize(self, *_recipe(self._data))
self.buildout._parts.append(name)
def initialize(self):
reqs, entry = _recipe(self._data)
buildout = self.buildout
recipe_class = _install_and_load(reqs, 'zc.buildout', entry, buildout)
name = self.name
self.recipe = recipe_class(buildout, name, self)
m = md5()
# access values through .get() instead of .items() to detect unused keys
for key in sorted(self.keys()):
value = self._data.get(key, self._cooked.get(key, self._raw.get(key)))
m.update(('%r\0%r\0' % (key, value)).encode())
self.items_signature = '%s:%s' % (name, m.hexdigest())
def _do_extend_raw(self, name, data, doing):
if name == 'buildout':
......@@ -1593,6 +1605,16 @@ class Options(DictMixin):
if v is None:
v = self._raw.get(option)
if v is None:
if option == '__buildout_signature__':
buildout = self.buildout
options, sig = buildout._initializing[-1]
if options is self:
self.depends = frozenset(self.depends)
v = self._data[option] = ' '.join(sig + [
buildout[dependency].items_signature
for dependency in sorted(self.depends)])
return v
raise zc.buildout.UserError("premature access to " + option)
return default
__doing__ = 'Getting option %s:%s.', self.name, option
......@@ -1648,7 +1670,14 @@ class Options(DictMixin):
section, option = s
if not section:
section = self.name
v = self.buildout[section].get(option, None, seen, last=last)
options = self
else:
self.buildout._initializing.append((self,))
try:
options = self.buildout[section]
finally:
del self.buildout._initializing[-1]
v = options.get(option, None, seen, last=last)
if v is None:
if option == '_buildout_section_name_':
v = self.name
......
......@@ -222,6 +222,9 @@ class Buildout(zc.buildout.buildout.Buildout):
Options = TestOptions
def initialize(self, *args):
pass
def buildoutSetUp(test):
test.globs['__tear_downs'] = __tear_downs = []
......
......@@ -2700,7 +2700,7 @@ were created.
The ``.installed.cfg`` is only updated for the recipes that ran::
>>> cat(sample_buildout, '.installed.cfg')
... # doctest: +NORMALIZE_WHITESPACE
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
[buildout]
installed_develop_eggs = /sample-buildout/develop-eggs/recipes.egg-link
parts = debug d1 d2 d3 d4
......@@ -2730,7 +2730,7 @@ The ``.installed.cfg`` is only updated for the recipes that ran::
<BLANKLINE>
[d4]
__buildout_installed__ = /sample-buildout/data2-extra
__buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg==
__buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== d2:...
path = /sample-buildout/data2-extra
recipe = recipes:mkdir
......
......@@ -2261,6 +2261,82 @@ def dealing_with_extremely_insane_dependencies():
Error: Couldn't find a distribution for 'pack5'.
"""
def test_part_pulled_by_recipe():
"""
>>> mkdir(sample_buildout, 'recipes')
>>> write(sample_buildout, 'recipes', 'test.py',
... '''
... class Recipe:
...
... def __init__(self, buildout, name, options):
... self.x = buildout[options['x']][name]
...
... def install(self):
... print(self.x)
... return ()
...
... update = install
... ''')
>>> write(sample_buildout, 'recipes', 'setup.py',
... '''
... from setuptools import setup
... setup(
... name = "recipes",
... entry_points = {'zc.buildout': ['test = test:Recipe']},
... )
... ''')
>>> write(sample_buildout, 'buildout.cfg',
... '''
... [buildout]
... develop = recipes
... parts = a
... [a]
... recipe = recipes:test
... x = b
... [b]
... <= a
... a = A
... b = B
... c = ${c:x}
... [c]
... x = c
... ''')
>>> os.chdir(sample_buildout)
>>> buildout = os.path.join(sample_buildout, 'bin', 'buildout')
>>> print_(system(buildout), end='')
Develop: '/sample-buildout/recipes'
Installing b.
B
Section `b` contains unused option(s): 'c'.
This may be an indication for either a typo in the option's name or a bug in the used recipe.
Installing a.
A
>>> print_(system(buildout), end='')
Develop: '/sample-buildout/recipes'
Updating b.
B
Updating a.
A
>>> cat('.installed.cfg') # doctest: +ELLIPSIS
[buildout]
...
[b]
__buildout_installed__ =
__buildout_signature__ = recipes-... c:...
...
[a]
__buildout_installed__ =
__buildout_signature__ = recipes-... b:...
...
"""
def read_find_links_to_load_extensions():
r"""
We'll create a wacky buildout extension that just announces itself when used:
......
......@@ -97,14 +97,14 @@ of extra requirements to be included in the working set.
We can see that the options were augmented with additional data
computed by the egg recipe by looking at .installed.cfg:
>>> cat(sample_buildout, '.installed.cfg')
>>> cat(sample_buildout, '.installed.cfg') # doctest: +ELLIPSIS
[buildout]
installed_develop_eggs = /sample-buildout/develop-eggs/sample.egg-link
parts = sample-part
<BLANKLINE>
[sample-part]
__buildout_installed__ =
__buildout_signature__ = ...
__buildout_signature__ = sample-... setuptools-... zc.buildout-... zc.recipe.egg-...
_b = /sample-buildout/bin
_d = /sample-buildout/develop-eggs
_e = /sample-buildout/eggs
......
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