Repository: flyingcircusio/batou

Issue:
AttributeError('No override and no default given.') does not give us the name of the attribute
Maybe investigate somewhere here

https://github.com/flyingcircusio/batou/blob/b1cf309f4b681d79e76ad24dbba78de8a05af381/src/batou/component.py#L1110-L1121

Discussion:
ctheune: The unstable test appears to be a missing backport from main branch (where we show all retries from the cycle). Please backport that fix selectively to this PR.

Patch:
diff --git a/src/batou/component.py b/src/batou/component.py
index e16cdde7..037a2fd1 100644
--- a/src/batou/component.py
+++ b/src/batou/component.py
@@ -1106,6 +1106,10 @@ class Attribute(object):
         self.expand = expand
         self.map = map
         self.instances = weakref.WeakKeyDictionary()
+        self.names = dict()
+
+    def __set_name__(self, owner, name):
+        self.names[owner] = name
 
     def __get__(self, obj, objtype=None):
         if obj is None:
@@ -1116,7 +1120,15 @@ class Attribute(object):
             if isinstance(self.default, ConfigString):
                 value = self.from_config_string(obj, value)
             elif value is NO_DEFAULT:
-                raise AttributeError("No override and no default given.")
+                name = self.names.get(obj.__class__, None)
+                if name:
+                    raise AttributeError(
+                        f"No override and no default given for `{name}` on {obj.__class__.__name__} instance {obj}."
+                    )
+                else:
+                    raise AttributeError(
+                        f"No override and no default given for unknown Attribute {obj}."
+                    )
             self.__set__(obj, value)
         return self.instances[obj]
 
diff --git a/src/batou/environment.py b/src/batou/environment.py
index 9d18eb1c..2b33b9b3 100644
--- a/src/batou/environment.py
+++ b/src/batou/environment.py
@@ -509,9 +509,15 @@ class Environment(object):
                         )
                     )
 
+                cycle_start = previous_working_sets.index(retry)
+                # consider all components tried during they cycle as unconfigured
+                unconfigured = set().union(*previous_working_sets[cycle_start:])
+
                 # We did not manage to improve on our last working set, so we
                 # give up.
-                exceptions.append(NonConvergingWorkingSet.from_context(retry))
+                exceptions.append(
+                    NonConvergingWorkingSet.from_context(unconfigured)
+                )
                 break
 
             working_set = retry


Test patch:
diff --git a/src/batou/tests/test_component.py b/src/batou/tests/test_component.py
index 13f9f06f..23490586 100644
--- a/src/batou/tests/test_component.py
+++ b/src/batou/tests/test_component.py
@@ -591,7 +591,10 @@ def test_attribute_missing_override_and_default(root):
     with pytest.raises(AttributeError) as e:
         root.component += f
 
-    assert e.value.args[0] == "No override and no default given."
+    assert (
+        e.value.args[0]
+        == 'No override and no default given for `a` on Foo instance <Foo (localhost) "MyComponent > Foo">.'
+    )
 
 
 @pytest.mark.parametrize(
diff --git a/src/batou/tests/test_endtoend.py b/src/batou/tests/test_endtoend.py
index ac9fa43d..16125de2 100644
--- a/src/batou/tests/test_endtoend.py
+++ b/src/batou/tests/test_endtoend.py
@@ -149,7 +149,7 @@ cycle1 depends on
 cycle2 depends on
         cycle1
 
-ERROR: 8 remaining unconfigured component(s): component1, component2, component4, crontab, cycle1, dnsproblem, dnsproblem2, filemode
+ERROR: 9 remaining unconfigured component(s): component1, component2, component4, crontab, cycle1, cycle2, dnsproblem, dnsproblem2, filemode
 ======================= 10 ERRORS - CONFIGURATION FAILED =======================
 ====================== DEPLOYMENT FAILED (during connect) ======================
 """