Skip to content

Exceptions

gpconfig defines a set of custom exception classes for handling various error scenarios during configuration management.

Import

from gpconfig import (
    GPConfigError,
    ConfigFolderError,
    ConfigNotFoundError,
    IllegalPathError,
    ConfigReadonlyError,
    RegistrationError,
    ConfigurableConstructionError,
    ConfigValidationError,
)

Exception Hierarchy

GPConfigError (base class)
├── ConfigFolderError
├── ConfigNotFoundError
├── IllegalPathError
├── ConfigReadonlyError
├── RegistrationError
├── ConfigurableConstructionError
└── ConfigValidationError

GPConfigError

Base exception for all gpconfig errors.

class GPConfigError(Exception):
    """Base exception for all gpconfig errors."""
    pass

Usage

Catch all gpconfig-related exceptions:

from gpconfig import GPConfigError, GPConfigManager

try:
    manager = GPConfigManager("myapp")
    config = manager.get_config("database")
except GPConfigError as e:
    print(f"Config error: {e}")

ConfigFolderError

Raised when the config folder cannot be found or is invalid.

class ConfigFolderError(GPConfigError):
    """Raised when the config folder cannot be found or is invalid."""
    pass

Trigger Conditions

  • Config folder does not exist
  • Config path is not a directory
  • Config folder is missing global_env.yaml

Examples

from gpconfig import GPConfigManager, ConfigFolderError
from pathlib import Path

try:
    # Folder doesn't exist
    manager = GPConfigManager("myapp", cfg_folder=Path("/nonexistent"))
except ConfigFolderError as e:
    print(f"Config folder error: {e}")
    # Output: Config folder does not exist: /nonexistent (from explicit parameter)

try:
    # Missing global_env.yaml
    manager = GPConfigManager("myapp", cfg_folder=Path("/tmp"))
except ConfigFolderError as e:
    print(f"Config folder error: {e}")
    # Output: Config folder must contain global_env.yaml: /tmp (from explicit parameter)

ConfigNotFoundError

Raised when a requested config path doesn't exist.

class ConfigNotFoundError(GPConfigError):
    """Raised when a requested config path doesn't exist."""

    def __init__(self, path: str, message: str = ""):
        self.path = path
        super().__init__(message or f"Config not found: {path}")

Attributes

Attribute Type Description
path str The config path that was not found

Examples

from gpconfig import GPConfigManager, ConfigNotFoundError

manager = GPConfigManager("myapp")

try:
    config = manager.get_config("nonexistent")
except ConfigNotFoundError as e:
    print(f"Config not found: {e.path}")
    print(f"Error message: {e}")

try:
    value = manager.get_config("database.nonexistent_key")
except ConfigNotFoundError as e:
    print(f"Key not found: {e.path}")

IllegalPathError

Raised when a config path is malformed or escapes the cfg_folder (defence-in-depth containment).

class IllegalPathError(GPConfigError):
    """Raised when a config path is malformed or escapes the cfg_folder."""

    def __init__(self, path: str, message: str = ""):
        self.path = path
        super().__init__(message or f"Illegal config path: {path}")

Trigger Conditions

A path is considered malformed and raises IllegalPathError when it is:

  • Empty — e.g. "" (was previously a tolerated way to refer to the root folder).
  • Dots-only — e.g. ".", "..".
  • Contains consecutive dots — e.g. "a..b".
  • Has a leading or trailing dot — e.g. ".x", "x." (a trailing dot previously returned the entire global_env dict by accident).
  • Contains a literal / or \ in cfg_path style — cfg_path uses dot-notation; a raw separator signals a malformed path.

Additionally, even a syntactically valid path raises IllegalPathError if it resolves outside the cfg_folder. This containment check is a defence-in-depth guarantee that no path can escape the managed folder.

Attributes

Attribute Type Description
path str The offending path string that was rejected

Raised By

IllegalPathError is raised by GPConfigManager.get_config(), get_object(), list_configs(), and save() whenever they encounter a malformed or escaping path.

Examples

from gpconfig import GPConfigManager, IllegalPathError

manager = GPConfigManager("myapp")

# Malformed paths
for bad in ["", ".", "a..b", ".hidden", "global_env.", "a/b"]:
    try:
        manager.get_config(bad)
    except IllegalPathError as e:
        print(f"Rejected {bad!r}: {e.path} -> {e}")

# save() rejects path containing '.' (cfg_path style, '.yaml' suffix, '..' traversal)
try:
    manager.save(config, "backups/database.yaml")  # contains '.' (.yaml suffix)
except IllegalPathError as e:
    print(f"Save rejected: {e}")

ConfigReadonlyError

Raised when trying to save a readonly config.

class ConfigReadonlyError(GPConfigError):
    """Raised when trying to save a readonly config."""

    def __init__(self, config_name: str):
        super().__init__(f"Config '{config_name}' is readonly and cannot be saved")

Examples

from gpconfig import GPConfig, GPConfigManager, ConfigReadonlyError
from typing import ClassVar

class SecureConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "SecureConfig"
    api_key: str

# Get config
manager = GPConfigManager("myapp")
config = manager.get_config("secure", SecureConfig)

# Set as readonly
config.readonly = True

try:
    config.api_key = "new_key"
    config.save()
except ConfigReadonlyError as e:
    print(f"Cannot save readonly config: {e}")
    # Output: Config 'secure' is readonly and cannot be saved

Use Cases

Readonly configs are suitable for: - Production environment sensitive configs - System configs that should not be modified - Preventing accidental overwrites

# Auto-set readonly in production environment
import os

config = manager.get_config("production_db", DatabaseConfig)
if os.environ.get("ENV") == "production":
    config.readonly = True

RegistrationError

Raised when there's an issue with class registration.

class RegistrationError(GPConfigError):
    """Raised when there's an issue with class registration."""
    pass

Trigger Conditions

  • Registering a different class under an already-registered cfg_class_name (re-registering the same class is idempotent and succeeds silently)
  • Config class not registered but get_object() was called
  • Config class has no associated configured_class

Examples

from gpconfig import GPConfig, GPConfigManager, RegistrationError
from typing import ClassVar

class MyConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "MyConfig"
    value: str

class ConflictConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "MyConfig"  # same cfg_class_name, different class
    other: str

# First registration - success
GPConfigManager.register_config_class(MyConfig)

# Second registration with a DIFFERENT class under the same name - failure
try:
    GPConfigManager.register_config_class(ConflictConfig)
except RegistrationError as e:
    print(f"Registration error: {e}")
    # Output: Config class name 'MyConfig' is already registered with a different class

ConfigurableConstructionError

Raised when a configurable object's construction hook returns an invalid object type.

class ConfigurableConstructionError(GPConfigError):
    """Raised when a construction hook returns an invalid object type.

    Attributes:
        path: Canonical dotted path used for object construction.
        expected_type: Registered configurable class that should be returned.
        actual_type: Type actually returned by the construction hook.
    """

    def __init__(
        self,
        path: str,
        expected_type: type,
        actual_type: type,
    ):
        self.path = path
        self.expected_type = expected_type
        self.actual_type = actual_type

Trigger Conditions

ConfigurableConstructionError is raised by GPConfigManager.get_object() (and GPConfigFolder.get_object(), which delegates to it) when a configurable class's from_config() hook returns a value that is not an instance of the registered configurable class:

  • The hook returned an unrelated type (e.g. a dict, None, or a plain object).
  • The hook returned an instance of a class that is neither the registered class nor a subclass of it.

Returning an instance of a subclass of the registered class is allowed and does not raise. Only a return value that fails isinstance(result, configurable_cls) triggers this error.

Attributes

Attribute Type Description
path str Canonical dotted path of the source YAML file used for construction (no .yaml suffix, no project-name prefix)
expected_type type The registered configurable class that should have been returned
actual_type type The type actually returned by the construction hook

Raised By

GPConfigManager.get_object() and GPConfigFolder.get_object(), after invoking configurable_cls.from_config(config, context=context). Exceptions raised inside the hook (business errors, or TypeError from an incompatible hook signature) propagate unchanged and do not become ConfigurableConstructionError — this error is specifically for a wrong return type.

Examples

from typing import ClassVar

from gpconfig import (
    GPConfig,
    GPConfigurable,
    GPConfigManager,
    ConfigurableConstructionError,
)
from gpconfig.configurable import GPConfigurableContext


class WorkerConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "WorkerConfig"
    worker_name: str


class Worker(GPConfigurable):
    @classmethod
    def from_config(
        cls,
        config: WorkerConfig,
        *,
        context: GPConfigurableContext,
    ) -> "Worker":
        # BUG: returns a dict instead of a Worker instance
        return {"worker_name": config.worker_name}


# Assume worker.yaml has configured_class_name: "Worker"
GPConfigManager.register_config_class(WorkerConfig)
GPConfigManager.register_configurable_class(Worker)

manager = GPConfigManager("myapp")

try:
    worker = manager.get_object("worker")
except ConfigurableConstructionError as e:
    print(f"Wrong type from {e.path}")        # e.path == "worker"
    print(f"Expected: {e.expected_type.__name__}")  # "Worker"
    print(f"Got: {e.actual_type.__name__}")         # "dict"
    print(e)
    # Configurable 'Worker' from path 'worker' returned dict; expected an instance of Worker

ConfigValidationError

Raised when a config file fails validation.

class ConfigValidationError(GPConfigError):
    """Raised when a config file fails validation."""

    def __init__(self, path: str, original_error: Exception):
        self.path = path
        self.original_error = original_error
        super().__init__(f"Validation failed for '{path}': {original_error}")

Trigger Conditions

ConfigValidationError is raised by GPConfigManager.get_config() (and the YAML loading path it uses) whenever a config file cannot be turned into a valid config object:

  • YAML syntax/parse error — malformed YAML (bad indentation, unclosed quotes/brackets, tabs). The original error is a yaml.YAMLError, whose message carries the file path, line, and column (e.g. in ".../server.yaml", line 3, column 5).
  • Duplicate explicit YAML key — two keys in the same mapping resolve to the same Python dictionary key, including differently written keys such as 1/true or null/~. The original error is a yaml.constructor.ConstructorError; its message identifies the key, file path, first definition line/column, and repeated line/column. The same key may still appear in separate mappings, and explicit keys may override values supplied through a legal merge key (<<).
  • Non-dict top level — the YAML parses but its root is a list or scalar instead of a mapping. The message embeds the on-disk file path.
  • Pydantic validation failure — the YAML parses to a dict but a field fails schema validation (wrong type, missing required field, or an extra key under extra="forbid"). The original error is a Pydantic ValidationError, whose message names the offending field.

In all cases the exception message carries the dotted config path (on .path), the on-disk file path, and the underlying error's detail. Duplicate-key errors additionally carry the readable key plus the first and repeated line/column locations. YAML errors are preserved on both .original_error and .__cause__, so callers can inspect the original yaml.YAMLError without parsing the wrapper's complete message.

Attributes

Attribute Type Description
path str The dotted config path that failed validation
original_error Exception The underlying error: a Pydantic ValidationError, a yaml.YAMLError, or a TypeError (non-dict top level)

Examples

from gpconfig import GPConfig, GPConfigManager, ConfigValidationError
from typing import ClassVar

class ServerConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "ServerConfig"
    host: str      # Required
    port: int      # Must be integer

# Assume server.yaml contains:
# host: localhost
# port: "not_a_number"  # Type error

manager = GPConfigManager("myapp")

try:
    config = manager.get_config("server", ServerConfig)
except ConfigValidationError as e:
    print(f"Config validation failed: {e.path}")
    # e.path is the dotted config path, e.g. "server".
    # str(e) also embeds the on-disk file path and the offending field name.
    print(f"Original error: {e.original_error}")

Best Practices

  1. Use specific exception types: Catch specific exceptions based on the scenario
  2. Provide helpful error messages: Give user-friendly error messages in exception handlers
  3. Log structured attributes: Several exceptions carry context worth logging alongside the message — ConfigValidationError.original_error, ConfigurableConstructionError (path, expected_type, actual_type), and IllegalPathError.path.
  4. Use base class as fallback: Use GPConfigError to catch all unhandled config exceptions