diff --git a/CHANGES.md b/CHANGES.md index 4d65d94a..a79be2c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ context. This variable contains a comment block that signals that the file content is generated by batou. (#356) - Development: update Sphinx to 5.3.0 to fix readthedocs build failures. +- Component object model: Class variable .instances renamed to ._instances + to avoid collisions with user-applications. ## 2.5.0b3 (2024-08-05) ----------------------- diff --git a/src/batou/__init__.py b/src/batou/__init__.py index 62636c97..c79faf3f 100644 --- a/src/batou/__init__.py +++ b/src/batou/__init__.py @@ -521,6 +521,43 @@ def report(self): output.tabular("Root", self.root_name, red=True) +class ComponentWithUpdateWithoutVerify(ConfigurationError): + """Some components have an update method but no verify method.""" + + sort_key = (5, "without_verify") + + @classmethod + def from_context(cls, components, roots): + self = cls() + self.components = [] + for component in components: + self.components.append(repr(component.__class__.__name__)) + self.roots = [] + for root in roots: + self.roots.append(root.name) + return self + + def __str__(self): + out_str = "Some components have an update method but no verify method:" + for idx, component in enumerate(self.components): + out_str += f"\n {component}" + out_str += f"\nRoot: {self.roots[idx]}" + out_str += f"\nThe update() method may not be called by batou if the verify() method is missing." + return out_str + + def report(self): + output.error( + f"Some components have an update method but no verify method:" + ) + for component in self.components: + output.line(f" {component}", red=True) + output.tabular("Root", self.root_name, red=True) + output.line( + f"The update() method may not be called by batou if the verify() method is missing.", + red=True, + ) + + class UnsatisfiedResources(ConfigurationError): """Some required resources were never provided.""" diff --git a/src/batou/component.py b/src/batou/component.py index 0e4548bd..de6adf57 100644 --- a/src/batou/component.py +++ b/src/batou/component.py @@ -159,7 +159,7 @@ class Component(object): #: After the configuration phase, this list is checked for #: components that have component._prepared == False and #: warns about them. - instances: List["Component"] = [] + _instances: List["Component"] = [] @property def defdir(self): @@ -229,7 +229,7 @@ def __init__(self, namevar=None, **kw): self._init_breadcrumbs = init_breadcrumbs - Component.instances.append(self) + Component._instances.append(self) self.timer = batou.utils.Timer(self.__class__.__name__) # Are any keyword arguments undefined attributes? # This is a somewhat rough implementation as it allows overriding diff --git a/src/batou/environment.py b/src/batou/environment.py index 0b8477de..611a8af6 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -15,6 +15,7 @@ import batou.vfs from batou import ( ComponentLoadingError, + ComponentWithUpdateWithoutVerify, ConfigurationError, CycleErrorDetected, DuplicateHostError, @@ -492,13 +493,14 @@ def configure(self): while working_set: exceptions = [] + components_without_verify = [] previous_working_sets.append(working_set.copy()) retry = set() self.resources.dirty_dependencies.clear() for root in working_set: try: - Component.instances.clear() + Component._instances.clear() self.resources.reset_component_resources(root) root.overrides = self.overrides.get(root.name, {}) root.prepare() @@ -518,8 +520,10 @@ def configure(self): ) ) else: + # warnings: does not fail the deployment + # 1. unprepared component warning unprepared_components = [] - for component in Component.instances: + for component in Component._instances: if not component._prepared: unprepared_components.append(component) if unprepared_components: @@ -531,6 +535,21 @@ def configure(self): ) # exceptions.append(unused_exception) output.warn(str(unused_exception)) + + # 2. a component has .update() but no .verify() + def has_original_update_method(component): + return type(component).update == Component.update + + def has_original_verify_method(component): + return type(component).verify == Component.verify + + for component in Component._instances: + if ( + not has_original_update_method(component) + and has_original_verify_method(component) + and (component not in components_without_verify) + ): + components_without_verify.append((component, root)) # configured this component successfully # we won't have to retry it later continue @@ -573,6 +592,15 @@ def configure(self): working_set = retry + # warn if a component has .update() but no .verify() + if components_without_verify: + component_without_verify_exception = ( + ComponentWithUpdateWithoutVerify.from_context( + components_without_verify, root + ) + ) + output.warn(str(component_without_verify_exception)) + # We managed to converge on a working set. However, some resource were # provided but never used. We're rather picky here and report this as # an error.