From 3c4f5b6b6ad3f7ca3c19863d74e36aa108ec610f Mon Sep 17 00:00:00 2001 From: Peter Stan Date: Tue, 12 Aug 2025 17:21:53 -0300 Subject: [PATCH] feat: add ValidationError base exception and optional graceful error handling --- pyproject.toml | 4 ++++ src/venvalid/core.py | 29 ++++++++++++++++++++--------- src/venvalid/errors.py | 6 +++++- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da27727..3263176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,10 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent" ] +dependencies = [ + "pytest>=8.3.5", + "rich>=14.1.0", +] [project.optional-dependencies] dev = [ diff --git a/src/venvalid/core.py b/src/venvalid/core.py index fd153cb..8fdc125 100644 --- a/src/venvalid/core.py +++ b/src/venvalid/core.py @@ -1,4 +1,7 @@ import os +from typing import Optional + +from rich.console import Console from .dotenv import load_env_file from .errors import EnvSafeError @@ -8,9 +11,11 @@ def venvalid( specs: dict[str, object], *, - source: dict[str, str] | None = None, + source: Optional[dict[str, str]] = None, dotenv_path: str = ".env", dotenv_override: bool = False, + pretty: bool = False, + exit_on_error: bool = True, ) -> dict[str, object]: """ Validates and loads environment variables based on declarative specifications. @@ -20,9 +25,14 @@ def venvalid( source (dict, optional): Alternative source for the variables (default: os.environ). dotenv_path (str, optional): Path to the .env file to be loaded. dotenv_override (bool): If True, overwrites existing variables when loading .env. + pretty (bool): If True, uses rich to pretty-print errors. + exit_on_error (bool): If True, calls SystemExit(1) on validation errors (default=True for backwards compat). Returns: dict: Validated and converted environment variables. + + Raises: + ValidationError: If exit_on_error=False and validation fails. """ if source is None: load_env_file(dotenv_path, override=dotenv_override) @@ -32,13 +42,19 @@ def venvalid( for key, spec in specs.items(): raw_value = env_source.get(key) - try: value = _resolve_variable(key, raw_value, spec) result[key] = value except EnvSafeError as e: - print(f"\n{e}\n") - raise SystemExit(1) + if pretty: + try: + console = Console() + console.print(f"[bold red]Error:[/bold red] {e}") + except ImportError: + pass + if exit_on_error: + raise SystemExit(1) + raise return result @@ -47,7 +63,6 @@ def _resolve_variable(key: str, raw: str | None, spec: object) -> object: """ Resolves and validates an environment variable based on its specification. """ - # Case enum-style: ["dev", "prod"] if isinstance(spec, list): if raw is None: raise EnvSafeError(f"{key} is required and must be one of {spec}") @@ -62,18 +77,14 @@ def _resolve_variable(key: str, raw: str | None, spec: object) -> object: if isinstance(spec, tuple): t_candidate, options = spec - if not isinstance(t_candidate, type): raise TypeError(f"{key}: expected a type, got {t_candidate}") - expected_type = t_candidate default = options.get("default") allowed = options.get("allowed") validate = options.get("validate") - elif isinstance(spec, type): expected_type = spec - else: raise TypeError(f"{key}: invalid spec type {type(spec)}") diff --git a/src/venvalid/errors.py b/src/venvalid/errors.py index 8252ede..3bd813b 100644 --- a/src/venvalid/errors.py +++ b/src/venvalid/errors.py @@ -1,3 +1,7 @@ -class EnvSafeError(Exception): +class ValidationError(Exception): + pass + + +class EnvSafeError(ValidationError): def __init__(self, message: str): super().__init__(f"[venvalid] {message}")