Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion argclass/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
UnexpectedConfigValue,
ValueKind,
)
from .exceptions import (
ArgclassError,
ArgumentDefinitionError,
ComplexTypeError,
ConfigurationError,
EnumValueError,
TypeConversionError,
)
from .factory import (
Argument,
ArgumentSequence,
Expand All @@ -44,7 +52,14 @@
StoreMeta,
TypedArgument,
)
from .types import Actions, ConverterType, LogLevelEnum, Nargs, NargsType
from .types import (
Actions,
ConverterType,
LogLevelEnum,
MetavarType,
Nargs,
NargsType,
)
from .utils import parse_bool, read_ini_configs

# Alias for backward compatibility
Expand All @@ -54,11 +69,20 @@
EnumType = EnumMeta

__all__ = [
# Exceptions
"ArgclassError",
"ArgumentDefinitionError",
"ComplexTypeError",
"ConfigurationError",
"EnumValueError",
"TypeConversionError",
"UnexpectedConfigValue",
# Types and enums
"Actions",
"Nargs",
"LogLevelEnum",
"ConverterType",
"MetavarType",
"NargsType",
# Classes
"SecretString",
Expand Down
12 changes: 8 additions & 4 deletions argclass/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from pathlib import Path
from typing import Any, Dict, Iterable, Mapping, Optional, Tuple, Union

from .exceptions import ConfigurationError

try:
import tomllib

Expand All @@ -30,16 +32,18 @@ class ValueKind(IntEnum):
BOOL = 2 # boolean value


class UnexpectedConfigValue(ValueError):
class UnexpectedConfigValue(ConfigurationError):
"""Config value doesn't match expected type."""

def __init__(self, key: str, expected: ValueKind, value: Any):
self.value = repr(value)
self.expected = expected
self.key = key
self.key = key # Backward compatibility alias
super().__init__(
f"Config key '{key}' expected {expected.name!r}, "
f"got {type(value)!r}: {self.value}"
f"expected {expected.name}, "
f"got {type(value).__name__}: {self.value}",
field_name=key,
hint=f"Provide a value of type {expected.name}",
)


Expand Down
300 changes: 300 additions & 0 deletions argclass/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
"""Exception classes for argclass with rich context for debugging."""

from typing import Any, Optional, Tuple


class ArgclassError(Exception):
"""Base exception for all argclass errors.

Provides structured context for debugging configuration issues.
All argclass exceptions inherit from this class.

Attributes:
message: The error message describing what went wrong.
field_name: The name of the field that caused the error, if applicable.
hint: A suggestion for how to fix the error, if available.

Example::

try:
# ... argclass operation that may fail
pass
except ArgclassError as e:
print(f"Error: {e}")
if e.field_name:
print(f"Field: {e.field_name}")
if e.hint:
print(f"Hint: {e.hint}")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
hint: Optional[str] = None,
):
self.message = message
self.field_name = field_name
self.hint = hint
super().__init__(self._format_message())

def _format_message(self) -> str:
parts = []
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)


class ArgumentDefinitionError(ArgclassError):
"""Error in argument definition or registration with argparse.

Raised when an argument cannot be added to the parser due to
invalid configuration (conflicting options, invalid types, etc.).

Attributes:
aliases: The conflicting aliases, e.g., ``("-v", "--verbose")``.
kwargs: The kwargs passed to argparse when the error occurred.

Example::

# This exception is typically raised during parser construction
# when argument definitions conflict:
try:
class Parser(argclass.Parser):
verbose: bool = argclass.Argument("-h") # conflicts with --help
except ArgumentDefinitionError as e:
print(f"Conflict: {e.aliases}")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
aliases: Optional[Tuple[str, ...]] = None,
kwargs: Optional[dict] = None,
hint: Optional[str] = None,
):
self.aliases = aliases
self.kwargs = kwargs
super().__init__(message, field_name=field_name, hint=hint)

def _format_message(self) -> str:
parts = []
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.aliases:
parts.append(f"(aliases: {', '.join(self.aliases)})")
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)


class TypeConversionError(ArgclassError):
"""Error during type conversion of argument values.

Raised when a value cannot be converted to the expected type,
either by the type function or a custom converter.

Attributes:
value: The original value that failed conversion.
target_type: The type that the value was being converted to.

Example::

try:
# When a custom converter fails:
def strict_port(value: str) -> int:
port = int(value)
if not (1 <= port <= 65535):
raise TypeConversionError(
"Port must be between 1 and 65535",
field_name="port",
value=value,
target_type=int,
hint="Use a valid port number (1-65535)",
)
return port
except TypeConversionError as e:
print(f"Invalid value: {e.value!r} for type {e.target_type}")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
value: Any = None,
target_type: Optional[type] = None,
hint: Optional[str] = None,
):
self.value = value
self.target_type = target_type
super().__init__(message, field_name=field_name, hint=hint)

def _format_message(self) -> str:
parts = []
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.value is not None:
parts.append(f"(got {self.value!r})")
if self.target_type is not None:
type_name = getattr(
self.target_type, "__name__", str(self.target_type)
)
parts.append(f"expected type: {type_name}")
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)


class ConfigurationError(ArgclassError):
"""Error loading or parsing configuration files.

Raised when a configuration file cannot be parsed or contains
invalid values for the expected argument types.

Attributes:
file_path: Path to the configuration file that caused the error.
section: The config section (e.g., INI section) where error occurred.

Example::

try:
parser = Parser(
config_files=["config.ini"],
config_parser_class=argclass.INIDefaultsParser,
)
except ConfigurationError as e:
print(f"Config error in {e.file_path}")
if e.section:
print(f"Section: [{e.section}]")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
file_path: Optional[str] = None,
section: Optional[str] = None,
hint: Optional[str] = None,
):
self.file_path = file_path
self.section = section
super().__init__(message, field_name=field_name, hint=hint)

def _format_message(self) -> str:
parts = []
if self.file_path:
location = self.file_path
if self.section:
location = f"{self.file_path}:[{self.section}]"
parts.append(f"({location})")
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)


class EnumValueError(ArgclassError):
"""Error with enum argument value or default.

Raised when an enum default or value is not a valid member
of the specified enum class.

Attributes:
enum_class: The enum class that was expected.
valid_values: Tuple of valid enum member names.

Example::

from enum import Enum

class Color(Enum):
RED = "red"
GREEN = "green"

try:
class Parser(argclass.Parser):
# "YELLOW" is not a valid Color member
color: Color = argclass.EnumArgument(Color, default="YELLOW")
except EnumValueError as e:
print(f"Invalid value for {e.enum_class.__name__}")
print(f"Valid options: {', '.join(e.valid_values)}")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
enum_class: Optional[type] = None,
valid_values: Optional[Tuple[str, ...]] = None,
hint: Optional[str] = None,
):
self.enum_class = enum_class
self.valid_values = valid_values
super().__init__(message, field_name=field_name, hint=hint)

def _format_message(self) -> str:
parts = []
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.valid_values:
parts.append(f"Valid values: {', '.join(self.valid_values)}")
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)


class ComplexTypeError(ArgclassError):
"""Error with complex type annotations.

Raised when a type annotation is too complex to be automatically
handled and requires explicit converter specification.

Attributes:
typespec: The type annotation that could not be handled.

Example::

try:
class Parser(argclass.Parser):
# Union types (other than T | None) are not supported
value: str | int # Raises ComplexTypeError
except ComplexTypeError as e:
print(f"Cannot handle type: {e.typespec}")
print("Provide an explicit converter with type=...")
"""

def __init__(
self,
message: str,
*,
field_name: Optional[str] = None,
typespec: Any = None,
hint: Optional[str] = None,
):
self.typespec = typespec
super().__init__(message, field_name=field_name, hint=hint)

def _format_message(self) -> str:
parts = []
if self.field_name:
parts.append(f"[{self.field_name}]")
parts.append(self.message)
if self.typespec is not None:
parts.append(f"(type: {self.typespec!r})")
if self.hint:
parts.append(f"Hint: {self.hint}")
return " ".join(parts)
Loading