Exception Handling

koji-habitude provides a comprehensive exception hierarchy that correlates errors with their source in YAML files, templates, and koji objects. All exceptions include rich context information to help diagnose and fix issues quickly.

Exception Hierarchy

All koji-habitude exceptions inherit from HabitudeError, which provides:

  • filename - The YAML file where the error originated

  • lineno - The line number in the YAML file

  • trace - Template expansion chain (when applicable)

  • original_exception - The underlying exception (pydantic, jinja2, koji, etc.)

Exception Types

YAML Parsing Errors

YAMLError

Wraps yaml.YAMLError when YAML files cannot be parsed.

Example:

YAML Error:
YAML parsing error: while parsing a block mapping
  Location: /data/tags/broken.yaml:4

Validation Errors

ValidationError

Wraps pydantic validation errors when object data fails schema validation.

Shows which fields failed validation and why.

Example:

Validation Error:
Validation error for tag 'fedora-42-build' (2 validation errors):
  - arches: field required
  - extra_arches: Extra inputs are not permitted
  Location: /data/tags/fedora.yaml:42
  Template trace:
    - build-tag-template in /templates/common.yaml:15

Template Errors

TemplateError

Base class for all template-related errors. Wraps Jinja2 errors with template context.

TemplateSyntaxError

Wraps Jinja2 syntax errors when template content has invalid syntax.

Example:

Template Error:
Error in template 'build-tag-template': unexpected '}'
  Location: /templates/common.yaml:15
  Template file: common-content.j2
  Template line: 23
TemplateRenderError

Wraps Jinja2 rendering errors such as undefined variables.

Example:

Template Error:
Error in template 'build-tag-template': 'arch' is undefined
  Location: /templates/common.yaml:15
  Template trace:
    - build-tag-template in /templates/common.yaml:15
TemplateOutputError

Raised when a template renders successfully but produces invalid output (invalid YAML or objects that fail validation).

Example:

Template Error:
Invalid output from template 'build-tag-template': Template rendered invalid YAML
  Location: /templates/common.yaml:15

Namespace Errors

ExpansionError

Raised when template expansion fails, such as when a referenced template cannot be found.

Shows which templates are available to help identify typos.

Example:

Template Error:
Could not resolve template: missing-template
  Available templates: build-tag, release-tag, test-template
  Location: /data/calls.yaml:10
RedefineError

Raised when attempting to redefine an object in the namespace (when redefine=Redefine.ERROR).

Shows both the original and new object locations.

Example:

Redefinition Error:
Redefinition of tag 'fedora-42-build'
  Original: /data/tags.yaml:42
  New: /data/duplicate.yaml:5
  Location: /data/duplicate.yaml:5

Koji API Errors

KojiError

Base class for koji API errors. All koji errors include object context.

ChangeReadError

Wraps koji.GenericError exceptions during the query/read phase when fetching current koji state.

Example:

Koji Error:
Koji error during query for tag 'fedora-42-build'
  Error: ServerOffline: database outage in progress
  Location: /data/tags/fedora.yaml:42
ChangeApplyError

Wraps koji.GenericError exceptions during the apply phase when writing changes to koji.

Includes the specific change that failed and the koji method that was called.

Example:

Koji Error:
Koji error during apply changes for tag 'fedora-42-build'
  Koji method: createTag
  Parameters: {'args': ('fedora-42-build',), 'kwargs': {'arches': 'x86_64'}}
  Error: TagAlreadyExists: tag fedora-42-build already exists
  Change: Create tag 'fedora-42-build'
  Location: /data/tags/fedora.yaml:42
  Template trace:
    - build-tag-template in /templates/common.yaml:15

Template Trace

When errors occur in objects created from templates, the template trace shows the full expansion chain:

Template trace:
  - build-tag-template in /templates/common.yaml:15
  - release-tags in /templates/release.yaml:42
  - platform-release in /templates/platforms.yaml:10

This shows that:

  1. platform-release template (line 10 of platforms.yaml) was called

  2. Which expanded to release-tags template (line 42 of release.yaml)

  3. Which expanded to build-tag-template (line 15 of common.yaml)

  4. Which finally produced the object that caused the error

The trace makes it easy to track down where configuration problems originate, even through multiple levels of template expansion.

Exception Chaining

All wrapped exceptions preserve the original exception via Python’s exception chaining (raise ... from e):

  • Access the original exception via exception.__cause__

  • Stack traces show both the wrapper and original exception

  • You can catch either the wrapper (e.g., TemplateError) or the original (e.g., jinja2.TemplateSyntaxError)

Example:

from koji_habitude.exceptions import TemplateSyntaxError
from jinja2.exceptions import TemplateSyntaxError as Jinja2Error

try:
    # ... code that might fail ...
except TemplateSyntaxError as e:
    # Catches our wrapped exception with rich context
    print(e)  # Shows full context
    print(e.__cause__)  # Shows original jinja2 error
except Jinja2Error as e:
    # This will also catch it (exception chaining)
    print("Caught original jinja2 error")

CLI Error Display

The CLI catchall decorator in koji_habitude.cli.util provides user-friendly error display:

  • Each exception type is clearly labeled

  • Full context is displayed (file location, template trace)

  • Original error details are included for debugging

  • Users see exactly what to fix and where

State Machine Errors

The following exceptions are not part of the HabitudeError hierarchy, as they represent programming errors rather than user data errors:

  • ProcessorStateError - Invalid processor state transitions

  • ChangeError - Invalid change state transitions

  • ChangeReportError - Invalid change report state transitions

  • WorkflowStateError - Invalid workflow state transitions

These should not occur during normal operation and indicate bugs in the code.

Best Practices

When catching exceptions:

  1. Catch specific types when you know what to expect:

    try:
        namespace.expand()
    except ExpansionError as e:
        logger.error(f"Template expansion failed: {e}")
    
  2. Catch HabitudeError to handle all user data errors:

    try:
        workflow.run()
    except HabitudeError as e:
        # All user-facing errors
        print(f"Error: {e}")
        return 1
    
  3. Access original exceptions for detailed debugging:

    try:
        namespace.to_object(data)
    except ValidationError as e:
        # Show our formatted error
        print(e)
        # Access pydantic's detailed error
        if e.original_exception:
            for error in e.original_exception.errors():
                print(f"  Field: {error['loc']}")
                print(f"  Type: {error['type']}")
    
  4. Use exception attributes to programmatically handle errors:

    try:
        processor.run()
    except ChangeApplyError as e:
        logger.error(f"Failed to apply changes to {e.typename} '{e.name}'")
        logger.error(f"  File: {e.filename}:{e.lineno}")
        logger.error(f"  Change: {e.change_description}")
        logger.error(f"  Koji method: {e.method_name}")
    

API Reference

koji_habitude.exceptions

Exception hierarchy for wrapping third-party exceptions with context about the YAML file, object, template, or change that caused the error.

author:

Christopher O’Brien <obriencj@gmail.com>

license:

GNU General Public License v3

ai-assistant:

Claude 3.5 Sonnet via Cursor

exception koji_habitude.exceptions.ChangeApplyError(original_error: Exception, obj: Any, change_description: str | None = None, method_name: str | None = None, parameters: Dict[str, Any] | None = None)

Bases: KojiError

Wraps koji exceptions that occur during the apply/write phase.

Parameters:
  • original_error – The original koji error

  • obj – The object being modified

  • change_description – Optional description of the change

  • method_name – Optional koji method name

  • parameters – Optional parameters passed to the method

exception koji_habitude.exceptions.ChangeReadError(original_error: Exception, obj: Any)

Bases: KojiError

Wraps koji exceptions that occur during the query/read phase.

Parameters:
  • original_error – The original koji error

  • obj – The object being queried

exception koji_habitude.exceptions.ExpansionError(call: Any, available_templates: List[str] | None = None)

Bases: HabitudeError

Indicates an error during the template expansion process.

Parameters:
  • call – Either a :class:TemplateCall object or a plain string message

  • available_templates – Optional list of available template names

exception koji_habitude.exceptions.HabitudeError(message: str, filename: str | None = None, lineno: int | None = None, trace: List[Dict[str, Any]] | None = None, original_exception: Exception | None = None)

Bases: Exception

Base exception for all koji-habitude exceptions.

All custom exceptions include context about where the error originated.

Parameters:
  • message – The error message

  • filename – Optional filename where the error occurred

  • lineno – Optional line number where the error occurred

  • trace – Optional template trace information

  • original_exception – Optional original exception that caused this error

exception koji_habitude.exceptions.KojiError(original_error: Exception, typename: str | None = None, name: str | None = None, filename: str | None = None, lineno: int | None = None, trace: List[Dict[str, Any]] | None = None, operation: str | None = None, method_name: str | None = None, parameters: Dict[str, Any] | None = None)

Bases: HabitudeError

Wraps generic koji exceptions with object context.

Used when koji API calls fail.

Parameters:
  • original_error – The original koji error

  • typename – Optional type name of the object

  • name – Optional name of the object

  • filename – Optional filename

  • lineno – Optional line number

  • trace – Optional template trace

  • operation – Optional operation description

  • method_name – Optional koji method name

  • parameters – Optional parameters passed to the method

exception koji_habitude.exceptions.RedefineError(key: Any, original_obj: Any, new_obj: Any)

Bases: HabitudeError

Indicates a redefinition of an object in the namespace.

Parameters:
  • key – Either a BaseKey (tuple) or string (for templates)

  • original_obj – The original object

  • new_obj – The new object attempting to redefine

exception koji_habitude.exceptions.TemplateError(original_error: Exception, template: Any | None = None, data: Dict[str, Any] | None = None, template_file: str | None = None)

Bases: HabitudeError

Wraps Jinja2 template errors with template context.

Base class for all template-related errors.

Parameters:
  • original_error – The original exception

  • template – Optional template object

  • data – Optional call data

  • template_file – Optional template filename

exception koji_habitude.exceptions.TemplateOutputError(message: str, template: Any, data: Dict[str, Any], rendered_content: str | None = None, original_exception: Exception | None = None)

Bases: HabitudeError

Used when template renders successfully but produces invalid output.

This can be either invalid YAML or valid YAML that fails validation.

Parameters:
  • message – Error message describing the problem

  • template – The template object

  • data – The call data

  • rendered_content – Optional rendered content

  • original_exception – Optional original exception

exception koji_habitude.exceptions.TemplateRenderError(original_error: Exception, template: Any, data: Dict[str, Any])

Bases: TemplateError

Wraps Jinja2 rendering errors (undefined variables, etc.).

Used when template rendering fails due to missing variables or other runtime issues.

Parameters:
  • original_error – The original rendering error

  • template – The template object

  • data – The call data

exception koji_habitude.exceptions.TemplateSyntaxError(original_error: Exception, template: Any, template_file: str | None = None)

Bases: TemplateError

Wraps Jinja2 syntax errors.

Used when template content has invalid Jinja2 syntax.

Parameters:
  • original_error – The original Jinja2 syntax error

  • template – The template object

  • template_file – Optional template filename

exception koji_habitude.exceptions.ValidationError(original_error: ValidationError, objdict: Dict[str, Any])

Bases: HabitudeError

Wraps pydantic validation errors with object and file context.

Used when object data fails pydantic schema validation.

Parameters:
  • original_error – The original pydantic validation error

  • objdict – The object data that failed validation

exception koji_habitude.exceptions.YAMLError(original_error: YAMLError, filename: str | None = None)

Bases: HabitudeError

Wraps YAML parsing errors with file context.

Used when YAML files cannot be parsed due to syntax errors.

Parameters:
  • original_error – The original YAML error

  • filename – Optional filename where the error occurred