From cf00cb2a6b633b565affbd52b865c20a7557c69c Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Sat, 9 Sep 2023 03:57:10 +0200 Subject: [PATCH 01/10] track component initializations --- src/batou/component.py | 9 +++++++++ src/batou/environment.py | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/batou/component.py b/src/batou/component.py index 2bda8af6..226c4d48 100644 --- a/src/batou/component.py +++ b/src/batou/component.py @@ -7,6 +7,7 @@ import sys import types import weakref +from typing import List import batou import batou.c @@ -146,6 +147,13 @@ class Component(object): #: working directory to this. workdir: str = None + #: A list of all component instances that have been created. When + #: a component is created (__init__) it is added to this list. + #: After the configuration phase, this list is checked for + #: components that have component._prepared == False and + #: warns about them. + instances: List["Component"] = [] + @property def defdir(self): """(*readonly*) The definition directory @@ -188,6 +196,7 @@ def root(self): _prepared = False def __init__(self, namevar=None, **kw): + 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 b52a373b..c2ae2037 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -31,7 +31,7 @@ UnusedResources, ) from batou._output import output -from batou.component import ComponentDefinition, RootComponent +from batou.component import Component, ComponentDefinition, RootComponent from batou.provision import Provisioner from batou.repository import Repository from batou.utils import CycleError, cmd @@ -566,6 +566,16 @@ def configure(self): UnusedResources.from_context(self.resources.unused) ) + # if any of Component.instances has ._prepared == False, then + # warn via output.annotate + for component in Component.instances: + if not component._prepared: + output.annotate( + "Component {} was not prepared.".format( + component.__class__.__name__ + ), + ) + for root in order: root.log_finish_configure() From c672bc2a467e85bb5d44e0c08476e37ca602a6b3 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 19 Dec 2023 01:50:47 +0100 Subject: [PATCH 02/10] Add warning for unprepared (not configured) components --- src/batou/environment.py | 15 ++++++++------- src/batou/remote_core.py | 6 ++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/batou/environment.py b/src/batou/environment.py index c2ae2037..592fb281 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -568,13 +568,14 @@ def configure(self): # if any of Component.instances has ._prepared == False, then # warn via output.annotate - for component in Component.instances: - if not component._prepared: - output.annotate( - "Component {} was not prepared.".format( - component.__class__.__name__ - ), - ) + if not exceptions: + for component in Component.instances: + if not component._prepared: + output.warn( + "A Component '{}' was initialized but was not prepared (configured).\nThis may not be what you want".format( + component.__class__.__name__ + ), + ) for root in order: root.log_finish_configure() diff --git a/src/batou/remote_core.py b/src/batou/remote_core.py index b9b1cd85..9cc2b2d5 100644 --- a/src/batou/remote_core.py +++ b/src/batou/remote_core.py @@ -107,6 +107,12 @@ def error(self, message, exc_info=None, debug=False): out = " " + out.replace("\n", "\n ") + "\n" self.backend.write(out, red=True) + def warn(self, message, debug=False): + if debug and not self.enable_debug: + return + self.flush_buffer() + self.step("WARN", message, yellow=True) + class ChannelBackend(object): def __init__(self, channel): From 92bee65269d4cce6159e07f1eeb9562672c06062 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 19 Dec 2023 02:38:52 +0100 Subject: [PATCH 03/10] reset application state in test_environment.py test --- src/batou/tests/test_environment.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/batou/tests/test_environment.py b/src/batou/tests/test_environment.py index f72d5060..a40fdc29 100644 --- a/src/batou/tests/test_environment.py +++ b/src/batou/tests/test_environment.py @@ -4,6 +4,7 @@ import batou import batou.utils +from batou.component import Component from batou.environment import Config, Environment from batou.host import Host @@ -286,6 +287,10 @@ def _get_components(hostname): @mock.patch("batou.remote_core.Output.line") def test_log_in_component_configure_is_put_out(output, sample_service): + # we are deploying in this test + # so we will reset some application state + Component.instances = [] + # this is for tracking which components are used and not e = Environment("test-with-provide-require") e.load() e.configure() From 22e61ee5b9eed9fa3d73c9a40adea484078190f3 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Wed, 20 Dec 2023 15:12:37 +0100 Subject: [PATCH 04/10] Report root node for unused components, test --- src/batou/__init__.py | 21 +++++++++++++ src/batou/environment.py | 30 ++++++++++--------- .../components/hello/component.py | 9 ++++++ .../environments/test-unused/environment.cfg | 5 ++++ src/batou/tests/test_environment.py | 10 ++++++- 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 src/batou/tests/fixture/sample_service/environments/test-unused/environment.cfg diff --git a/src/batou/__init__.py b/src/batou/__init__.py index 454f25a1..3b8222fc 100644 --- a/src/batou/__init__.py +++ b/src/batou/__init__.py @@ -473,6 +473,27 @@ def report(self): ) +class UnusedComponentsInitialized(ConfigurationError): + """Some components were initialized but never used.""" + + sort_key = (5, "unused") + + @classmethod + def from_context(cls, components, root): + self = cls() + self.unused_components = [] + for component in components: + self.unused_components.append(repr(component.__class__.__name__)) + self.root_name = root.name + return self + + def __str__(self): + return f"Unused components: {', '.join(self.unused_components)}" + + def report(self): + output.error(f"Unused components: {', '.join(self.unused_components)}") + + class UnsatisfiedResources(ConfigurationError): """Some required resources were never provided.""" diff --git a/src/batou/environment.py b/src/batou/environment.py index 592fb281..1be15b8d 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -28,6 +28,7 @@ SuperfluousSection, UnknownComponentConfigurationError, UnsatisfiedResources, + UnusedComponentsInitialized, UnusedResources, ) from batou._output import output @@ -497,6 +498,7 @@ def configure(self): for root in working_set: try: + Component.instances.clear() self.resources.reset_component_resources(root) root.overrides = self.overrides.get(root.name, {}) root.prepare() @@ -516,9 +518,20 @@ def configure(self): ) ) else: - # configured this component successfully - # we won't have to retry it later - continue + unprepared_components = [] + for component in Component.instances: + if not component._prepared: + unprepared_components.append(component) + if unprepared_components: + exceptions.append( + UnusedComponentsInitialized.from_context( + unprepared_components, root + ) + ) + if not exceptions: + # configured this component successfully + # we won't have to retry it later + continue retry.add(root) retry.update(self.resources.dirty_dependencies) @@ -566,17 +579,6 @@ def configure(self): UnusedResources.from_context(self.resources.unused) ) - # if any of Component.instances has ._prepared == False, then - # warn via output.annotate - if not exceptions: - for component in Component.instances: - if not component._prepared: - output.warn( - "A Component '{}' was initialized but was not prepared (configured).\nThis may not be what you want".format( - component.__class__.__name__ - ), - ) - for root in order: root.log_finish_configure() diff --git a/src/batou/tests/fixture/sample_service/components/hello/component.py b/src/batou/tests/fixture/sample_service/components/hello/component.py index c829c267..844ac859 100644 --- a/src/batou/tests/fixture/sample_service/components/hello/component.py +++ b/src/batou/tests/fixture/sample_service/components/hello/component.py @@ -62,3 +62,12 @@ def configure(self): class Sub(Component): def configure(self): self.log("Sub!") + + +class Unused(Component): + pass + + +class BadUnused(Component): + def configure(self): + Unused() diff --git a/src/batou/tests/fixture/sample_service/environments/test-unused/environment.cfg b/src/batou/tests/fixture/sample_service/environments/test-unused/environment.cfg new file mode 100644 index 00000000..c67a1166 --- /dev/null +++ b/src/batou/tests/fixture/sample_service/environments/test-unused/environment.cfg @@ -0,0 +1,5 @@ +[environment] +connect_method = local + +[hosts] +localhost = badunused diff --git a/src/batou/tests/test_environment.py b/src/batou/tests/test_environment.py index a40fdc29..b9557bba 100644 --- a/src/batou/tests/test_environment.py +++ b/src/batou/tests/test_environment.py @@ -4,7 +4,7 @@ import batou import batou.utils -from batou.component import Component +from batou.component import Component, RootComponent from batou.environment import Config, Environment from batou.host import Host @@ -336,3 +336,11 @@ def test_resolver_overrides_invalid_address(sample_service): errors = e.configure() assert len(errors) == 1 assert "thisisinvalid" == errors[0].address + + +def test_unused_components_get_reported(sample_service): + e = Environment("test-unused") + e.load() + errors = e.configure() + + assert any(isinstance(e, batou.UnusedComponentsInitialized) for e in errors) From 939cdeb6b3f0391cf2998d7120bd423aa433f637 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Wed, 20 Dec 2023 15:21:39 +0100 Subject: [PATCH 05/10] Remove unnecessary code in test_environment.py --- src/batou/tests/test_environment.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/batou/tests/test_environment.py b/src/batou/tests/test_environment.py index b9557bba..bd96c983 100644 --- a/src/batou/tests/test_environment.py +++ b/src/batou/tests/test_environment.py @@ -287,10 +287,6 @@ def _get_components(hostname): @mock.patch("batou.remote_core.Output.line") def test_log_in_component_configure_is_put_out(output, sample_service): - # we are deploying in this test - # so we will reset some application state - Component.instances = [] - # this is for tracking which components are used and not e = Environment("test-with-provide-require") e.load() e.configure() From 5380434ffc083b06d3762f5ed76838086736da0a Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Wed, 20 Dec 2023 15:26:51 +0100 Subject: [PATCH 06/10] Fix component configuration retry logic if unused components exist --- src/batou/environment.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/batou/environment.py b/src/batou/environment.py index 1be15b8d..103376da 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -528,10 +528,9 @@ def configure(self): unprepared_components, root ) ) - if not exceptions: - # configured this component successfully - # we won't have to retry it later - continue + # configured this component successfully + # we won't have to retry it later + continue retry.add(root) retry.update(self.resources.dirty_dependencies) From 0c0a8d3dc7d773f1552ba094f9acb5e7661dac20 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Thu, 20 Jun 2024 03:45:01 +0200 Subject: [PATCH 07/10] save stack on component init for better unused error message/warning --- src/batou/__init__.py | 14 ++++++++++++-- src/batou/component.py | 23 +++++++++++++++++++++++ src/batou/environment.py | 5 ++++- src/batou/tests/test_environment.py | 8 +++++--- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/batou/__init__.py b/src/batou/__init__.py index 3b8222fc..1d84907e 100644 --- a/src/batou/__init__.py +++ b/src/batou/__init__.py @@ -482,16 +482,26 @@ class UnusedComponentsInitialized(ConfigurationError): def from_context(cls, components, root): self = cls() self.unused_components = [] + self.breadcrumbs = [] for component in components: self.unused_components.append(repr(component.__class__.__name__)) + self.breadcrumbs.append(component._init_breadcrumbs) self.root_name = root.name return self def __str__(self): - return f"Unused components: {', '.join(self.unused_components)}" + out_str = "Unused components: " + for i, component in enumerate(self.unused_components): + out_str += f"\n {component}: {' -> '.join(self.breadcrumbs[i])}" + return out_str def report(self): - output.error(f"Unused components: {', '.join(self.unused_components)}") + output.error(f"Unused components:") + for i, component in enumerate(self.unused_components): + output.line( + f" {component}: {' -> '.join(self.breadcrumbs[i])}", red=True + ) + output.tabular("Root", self.root_name, red=True) class UnsatisfiedResources(ConfigurationError): diff --git a/src/batou/component.py b/src/batou/component.py index 226c4d48..6df0f52b 100644 --- a/src/batou/component.py +++ b/src/batou/component.py @@ -196,6 +196,29 @@ def root(self): _prepared = False def __init__(self, namevar=None, **kw): + init_stack = inspect.stack() + init_stack.reverse() + init_breadcrumbs = [] + for frame in init_stack: + if ( + "self" in frame.frame.f_locals + and isinstance(frame.frame.f_locals["self"], Component) + and frame.frame.f_code.co_name == "configure" + ): + component = frame.frame.f_locals["self"] + try: + init_breadcrumbs.append(component._breadcrumb) + except AttributeError: + # some ._breadcrumb are broken + breadcrumb = component.__class__.__name__ + if component.namevar: + breadcrumb += ( + f"({getattr(component, component.namevar, None)})" + ) + init_breadcrumbs.append(breadcrumb) + + self._init_breadcrumbs = init_breadcrumbs + Component.instances.append(self) self.timer = batou.utils.Timer(self.__class__.__name__) # Are any keyword arguments undefined attributes? diff --git a/src/batou/environment.py b/src/batou/environment.py index 103376da..2f9cfee3 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -523,11 +523,14 @@ def configure(self): if not component._prepared: unprepared_components.append(component) if unprepared_components: - exceptions.append( + # TODO: activate in future + unused_exception = ( UnusedComponentsInitialized.from_context( unprepared_components, root ) ) + # exceptions.append(unused_exception) + output.warn(str(unused_exception)) # configured this component successfully # we won't have to retry it later continue diff --git a/src/batou/tests/test_environment.py b/src/batou/tests/test_environment.py index bd96c983..8b42e6e2 100644 --- a/src/batou/tests/test_environment.py +++ b/src/batou/tests/test_environment.py @@ -334,9 +334,11 @@ def test_resolver_overrides_invalid_address(sample_service): assert "thisisinvalid" == errors[0].address -def test_unused_components_get_reported(sample_service): +def test_unused_components_get_reported(sample_service, output): e = Environment("test-unused") e.load() - errors = e.configure() - assert any(isinstance(e, batou.UnusedComponentsInitialized) for e in errors) + output.backend.output = "" + e.configure() + + assert "'Unused': BadUnused" in output.backend.output From 1f916470799628e56d85eb4f9e248ccd654c3639 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Thu, 20 Jun 2024 03:46:17 +0200 Subject: [PATCH 08/10] changelog: report unused Components as warnings --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 15967029..32a9c970 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ----------------------- - batou migrate now writes .batou.json with a newline at the end as `pre-commit` hooks expect (usually). +- Unused Components, that is, Components that are initialized, but not used in the deployment, are now reported as warnings. ## 2.5.0b2 (2024-05-15) From acd98c625b4bb0c1bc717154d2f332b4b0fdfb18 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 25 Jun 2024 04:01:00 +0200 Subject: [PATCH 09/10] Add initialization file paths and line numbers to UnusedComponentsInitialized error message --- src/batou/__init__.py | 9 +++++++++ src/batou/component.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/batou/__init__.py b/src/batou/__init__.py index 1d84907e..1c849fdd 100644 --- a/src/batou/__init__.py +++ b/src/batou/__init__.py @@ -483,9 +483,13 @@ def from_context(cls, components, root): self = cls() self.unused_components = [] self.breadcrumbs = [] + self.init_file_paths = [] + self.init_line_numbers = [] for component in components: self.unused_components.append(repr(component.__class__.__name__)) self.breadcrumbs.append(component._init_breadcrumbs) + self.init_file_paths.append(component._init_file_path) + self.init_line_numbers.append(component._init_line_number) self.root_name = root.name return self @@ -493,6 +497,7 @@ def __str__(self): out_str = "Unused components: " for i, component in enumerate(self.unused_components): out_str += f"\n {component}: {' -> '.join(self.breadcrumbs[i])}" + out_str += f"\n initialized in {self.init_file_paths[i]}:{self.init_line_numbers[i]}" return out_str def report(self): @@ -501,6 +506,10 @@ def report(self): output.line( f" {component}: {' -> '.join(self.breadcrumbs[i])}", red=True ) + output.line( + f" initialized in {self.init_file_paths[i]}:{self.init_line_numbers[i]}", + red=True, + ) output.tabular("Root", self.root_name, red=True) diff --git a/src/batou/component.py b/src/batou/component.py index 6df0f52b..73ae7ccb 100644 --- a/src/batou/component.py +++ b/src/batou/component.py @@ -199,6 +199,9 @@ def __init__(self, namevar=None, **kw): init_stack = inspect.stack() init_stack.reverse() init_breadcrumbs = [] + call_site = init_stack[-2] + self._init_file_path = call_site.filename + self._init_line_number = call_site.lineno for frame in init_stack: if ( "self" in frame.frame.f_locals From b4b878b360f989110c15ce77cc24add2c7cccf9c Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 25 Jun 2024 04:11:31 +0200 Subject: [PATCH 10/10] Update UnusedComponentsInitialized error message format --- src/batou/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/batou/__init__.py b/src/batou/__init__.py index 1c849fdd..62636c97 100644 --- a/src/batou/__init__.py +++ b/src/batou/__init__.py @@ -494,14 +494,18 @@ def from_context(cls, components, root): return self def __str__(self): - out_str = "Unused components: " + out_str = "Some components were initialized but never added to the environment:" for i, component in enumerate(self.unused_components): out_str += f"\n {component}: {' -> '.join(self.breadcrumbs[i])}" out_str += f"\n initialized in {self.init_file_paths[i]}:{self.init_line_numbers[i]}" + out_str += f"\nRoot: {self.root_name}" + out_str += f"\nAdd the components to the environment using `self += component`." return out_str def report(self): - output.error(f"Unused components:") + output.error( + f"Some components were initialized but never added to the environment:" + ) for i, component in enumerate(self.unused_components): output.line( f" {component}: {' -> '.join(self.breadcrumbs[i])}", red=True @@ -510,6 +514,10 @@ def report(self): f" initialized in {self.init_file_paths[i]}:{self.init_line_numbers[i]}", red=True, ) + output.line( + f"Add the components to the environment using `self += component`.", + red=True, + ) output.tabular("Root", self.root_name, red=True)