Skip to content

Commit

Permalink
Merge pull request #420 from flyingcircusio/278-track-component-initi…
Browse files Browse the repository at this point in the history
…alisations-for

Track component initialisations for error messages
  • Loading branch information
zagy authored Jun 25, 2024
2 parents 1b5fab7 + b4b878b commit 7e13886
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions src/batou/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,54 @@ 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 = []
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

def __str__(self):
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"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
)
output.line(
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)


class UnsatisfiedResources(ConfigurationError):
"""Some required resources were never provided."""

Expand Down
35 changes: 35 additions & 0 deletions src/batou/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import types
import weakref
from typing import List

import batou
import batou.c
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -188,6 +196,33 @@ def root(self):
_prepared = False

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
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?
# This is a somewhat rough implementation as it allows overriding
Expand Down
17 changes: 16 additions & 1 deletion src/batou/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
SuperfluousSection,
UnknownComponentConfigurationError,
UnsatisfiedResources,
UnusedComponentsInitialized,
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
Expand Down Expand Up @@ -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()
Expand All @@ -516,6 +518,19 @@ def configure(self):
)
)
else:
unprepared_components = []
for component in Component.instances:
if not component._prepared:
unprepared_components.append(component)
if unprepared_components:
# 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
Expand Down
6 changes: 6 additions & 0 deletions src/batou/remote_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[environment]
connect_method = local

[hosts]
localhost = badunused
11 changes: 11 additions & 0 deletions src/batou/tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import batou
import batou.utils
from batou.component import Component, RootComponent
from batou.environment import Config, Environment
from batou.host import Host

Expand Down Expand Up @@ -331,3 +332,13 @@ 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, output):
e = Environment("test-unused")
e.load()

output.backend.output = ""
e.configure()

assert "'Unused': BadUnused" in output.backend.output

0 comments on commit 7e13886

Please sign in to comment.