Repository: flyingcircusio/batou

Issue:
failures loading components are missing position information
In situations like this we are missing the positioning information. Full tracebacks might not be necessary but the last entry would be helpful.

```
ERROR: Failed loading component file
      File: /Users/ctheune/Code/fc.directory.deployment/components/nginx/component.py
 Exception: __init__() takes from 1 to 2 positional arguments but 3 were given
```

Discussion:

Patch:
diff --git a/src/batou/__init__.py b/src/batou/__init__.py
index a10cec90..4d8d16eb 100644
--- a/src/batou/__init__.py
+++ b/src/batou/__init__.py
@@ -18,6 +18,27 @@ if not os.environ.get("REMOTE_PDB_PORT", None):
     os.environ["REMOTE_PDB_PORT"] = "4444"
 
 
+def prepare_traceback(tb):
+    from batou import component, environment
+
+    stack = traceback.extract_tb(tb)
+    while stack:
+        # Delete remoting-internal stack frames.'
+        line = stack.pop(0)
+        if line[0] in [
+            "<string>",
+            "<remote exec>",
+            environment.__file__.rstrip("c"),
+            component.__file__.rstrip("c"),
+        ]:
+            continue
+        stack.insert(0, line)
+        break
+    if not stack:
+        return "<no-non-remote-internal-traceback-lines-found>"
+    return "".join(traceback.format_list(stack))
+
+
 class ReportingException(Exception):
     """Exceptions that support user-readable reporting."""
 
@@ -390,22 +411,8 @@ class UnknownComponentConfigurationError(ConfigurationError):
         self.root_name = root.name
         self.root_host_name = root.host.name
         self.exception_repr = repr(exception)
-        stack = traceback.extract_tb(tb)
-        from batou import component, environment
-
-        while True:
-            # Delete remoting-internal stack frames.'
-            line = stack.pop(0)
-            if line[0] in [
-                "<string>",
-                "<remote exec>",
-                environment.__file__.rstrip("c"),
-                component.__file__.rstrip("c"),
-            ]:
-                continue
-            stack.insert(0, line)
-            break
-        self.traceback = "".join(traceback.format_list(stack))
+
+        self.traceback = prepare_traceback(tb)
         return self
 
     def __str__(self):
@@ -507,10 +514,12 @@ class ComponentLoadingError(ConfigurationError):
     sort_key = (0,)
 
     @classmethod
-    def from_context(cls, filename, exception):
+    def from_context(cls, filename, exception, tb):
         self = cls()
         self.filename = filename
         self.exception_str = str(exception)
+
+        self.traceback = prepare_traceback(tb)
         return self
 
     def __str__(self):
@@ -520,6 +529,11 @@ class ComponentLoadingError(ConfigurationError):
         output.error("Failed loading component file")
         output.tabular("File", self.filename, red=True)
         output.tabular("Exception", self.exception_str, red=True)
+        output.annotate(
+            "Traceback (simplified, most recent call last):", red=True
+        )
+        output.annotate(self.traceback, red=True)
+
         # TODO provide traceback in debug output
         # see: https://github.com/flyingcircusio/batou/issues/316
 
diff --git a/src/batou/environment.py b/src/batou/environment.py
index 960c01cc..ec54ac30 100644
--- a/src/batou/environment.py
+++ b/src/batou/environment.py
@@ -215,8 +215,9 @@ class Environment(object):
             try:
                 self.components.update(load_components_from_file(filename))
             except Exception as e:
+                exc_type, ex, tb = sys.exc_info()
                 self.exceptions.append(
-                    ComponentLoadingError.from_context(filename, e)
+                    ComponentLoadingError.from_context(filename, e, tb)
                 )
 
         config = Config(config_file)


Test patch:
diff --git a/src/batou/tests/test_deploy.py b/src/batou/tests/test_deploy.py
index de28fa86..fd567109 100644
--- a/src/batou/tests/test_deploy.py
+++ b/src/batou/tests/test_deploy.py
@@ -37,10 +37,16 @@ main: Loading secrets ...
 ERROR: Failed loading component file
            File: .../examples/errors/components/component5/component.py
       Exception: invalid syntax (component.py, line 1)
+Traceback (simplified, most recent call last):
+<no-non-remote-internal-traceback-lines-found>
 
 ERROR: Failed loading component file
            File: .../examples/errors/components/component6/component.py
       Exception: No module named 'asdf'
+Traceback (simplified, most recent call last):
+  File ".../examples/errors/components/component6/component.py", line 1, in <module>
+    import asdf  # noqa: F401 import unused
+
 
 ERROR: Missing component
       Component: missingcomponent
diff --git a/src/batou/tests/test_endtoend.py b/src/batou/tests/test_endtoend.py
index bb25057c..c9ad9866 100644
--- a/src/batou/tests/test_endtoend.py
+++ b/src/batou/tests/test_endtoend.py
@@ -31,10 +31,16 @@ main: Loading secrets ...
 ERROR: Failed loading component file
            File: .../examples/errors/components/component5/component.py
       Exception: invalid syntax (component.py, line 1)
+Traceback (simplified, most recent call last):
+<no-non-remote-internal-traceback-lines-found>
 
 ERROR: Failed loading component file
            File: .../examples/errors/components/component6/component.py
       Exception: No module named 'asdf'
+Traceback (simplified, most recent call last):
+  File ".../errors/components/component6/component.py", line 1, in <module>
+    import asdf  # noqa: F401 import unused
+
 
 ERROR: Missing component
       Component: missingcomponent
@@ -77,10 +83,16 @@ gpg: ...
 ERROR: Failed loading component file
            File: .../examples/errors/components/component5/component.py
       Exception: invalid syntax (component.py, line 1)
+Traceback (simplified, most recent call last):
+<no-non-remote-internal-traceback-lines-found>
 
 ERROR: Failed loading component file
            File: .../examples/errors/components/component6/component.py
       Exception: No module named 'asdf'
+Traceback (simplified, most recent call last):
+  File ".../examples/errors/components/component6/component.py", line 1, in <module>
+    import asdf  # noqa: F401 import unused
+
 
 ERROR: Missing component
       Component: missingcomponent
diff --git a/src/batou/tests/test_exceptions.py b/src/batou/tests/test_exceptions.py
index b9455cce..f5be7c4c 100644
--- a/src/batou/tests/test_exceptions.py
+++ b/src/batou/tests/test_exceptions.py
@@ -61,7 +61,7 @@ def test_configurationerrors_can_be_sorted(root):
     errors.append(MissingEnvironment.from_context(root.environment))
 
     errors.append(
-        ComponentLoadingError.from_context("asdf.py", ValueError("asdf"))
+        ComponentLoadingError.from_context("asdf.py", ValueError("asdf"), None)
     )
 
     errors.append(MissingComponent.from_context("component", "hostname"))