From 1b767b2202d8aa1db71fd909fbf2b82e66172ed1 Mon Sep 17 00:00:00 2001 From: Mudit Agarwal Date: Wed, 29 Apr 2026 09:25:13 -0700 Subject: [PATCH 1/4] refactor: audit config error messages The idea here is to go through our config error messages and --- src/config.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/config.py diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..4081b1a --- /dev/null +++ b/src/config.py @@ -0,0 +1,93 @@ +import sys +import tomllib +from pathlib import Path +from typing import Optional + +from pydantic import BaseModel, Field, ValidationError + + +class ConfigError(Exception): + """Custom exception for configuration-related errors.""" + pass + + +class Settings(BaseModel): + """ + Defines the application's configuration structure using Pydantic. + This ensures that all configuration values are of the correct type. + """ + api_key: str = Field(..., description="The API key for the primary service.") + timeout: int = Field( + default=30, gt=0, description="Default timeout for API requests in seconds." + ) + log_level: str = Field( + default="INFO", + pattern=r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$", + description="Logging level (e.g., DEBUG, INFO, WARNING).", + ) + database_url: Optional[str] = Field( + default=None, description="Optional database connection URL." + ) + + +def load_config(config_path: Path = Path("config.toml")) -> Settings: + """ + Loads and validates configuration from a TOML file. + + This function is designed to provide clear, actionable error messages + to help users fix configuration issues quickly. + + Args: + config_path: The path to the configuration file. + + Returns: + A validated Settings object. + + Raises: + ConfigError: If the file is not found, cannot be parsed, or fails validation. + """ + # Pain Point 1: The configuration file doesn't exist. + # The error should be explicit about what's missing and where we looked. + if not config_path.is_file(): + raise ConfigError( + f"Configuration file not found at '{config_path}'.\n" + "Hint: Make sure the file exists. You might need to create one from an " + "example template like 'config.example.toml'." + ) + + # Pain Point 2: The file exists but is malformed (e.g., invalid TOML). + # We'll catch the specific parsing error and provide context. + try: + with open(config_path, "rb") as f: + config_data = tomllib.load(f) + except tomllib.TOMLDecodeError as e: + raise ConfigError( + f"Error parsing '{config_path}':\n" + f" {e}\n" + "Hint: Check the file for syntax errors like missing quotes, " + "incorrect formatting, or invalid characters." + ) + except OSError as e: + # Also handle cases where the file can't be read due to permissions. + raise ConfigError(f"Could not read configuration file '{config_path}': {e}") + + # Pain Points 3 & 4: Missing required keys or values of the wrong type. + # Pydantic handles this validation, but we can format its error for clarity. + try: + return Settings(**config_data) + except ValidationError as e: + # Pydantic's default error is good, but we can make it more user-friendly + # by formatting it as a clear, readable list. + error_messages = [] + for error in e.errors(): + field = " -> ".join(map(str, error["loc"])) + message = error["msg"] + error_messages.append(f" - Field '{field}': {message}") + + formatted_errors = "\n".join(error_messages) + raise ConfigError( + f"Configuration in '{config_path}' is invalid:\n" + f"{formatted_errors}\n" + "Hint: Please check the values in your configuration file and ensure they " + "match the expected types and requirements." + ) \ No newline at end of file From cbb90512a5f8806bc7104e4785128eb37b8081c2 Mon Sep 17 00:00:00 2001 From: Mudit Agarwal Date: Wed, 29 Apr 2026 09:51:43 -0700 Subject: [PATCH 2/4] refactor: make config errors actionable The idea here is that good error messages are --- src/config.py | 26 ++++++---- src/main.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 src/main.py diff --git a/src/config.py b/src/config.py index 4081b1a..5326ea4 100644 --- a/src/config.py +++ b/src/config.py @@ -51,8 +51,9 @@ def load_config(config_path: Path = Path("config.toml")) -> Settings: if not config_path.is_file(): raise ConfigError( f"Configuration file not found at '{config_path}'.\n" - "Hint: Make sure the file exists. You might need to create one from an " - "example template like 'config.example.toml'." + "Hint: Check that the file exists at this path. You may need to create it " + "from a template (e.g., 'config.example.toml') or specify the correct " + "path via a command-line argument or environment variable." ) # Pain Point 2: The file exists but is malformed (e.g., invalid TOML). @@ -63,13 +64,18 @@ def load_config(config_path: Path = Path("config.toml")) -> Settings: except tomllib.TOMLDecodeError as e: raise ConfigError( f"Error parsing '{config_path}':\n" - f" {e}\n" - "Hint: Check the file for syntax errors like missing quotes, " - "incorrect formatting, or invalid characters." + f" {e}\n\n" + "Hint: This looks like a TOML syntax error. Common issues include " + "unclosed quotes for strings or misplaced brackets. Please double-check " + "the line mentioned in the error above." ) except OSError as e: # Also handle cases where the file can't be read due to permissions. - raise ConfigError(f"Could not read configuration file '{config_path}': {e}") + raise ConfigError( + f"Could not read configuration file '{config_path}': {e}\n" + "Hint: Please check the file's permissions and ensure the application " + "has read access." + ) # Pain Points 3 & 4: Missing required keys or values of the wrong type. # Pydantic handles this validation, but we can format its error for clarity. @@ -87,7 +93,9 @@ def load_config(config_path: Path = Path("config.toml")) -> Settings: formatted_errors = "\n".join(error_messages) raise ConfigError( f"Configuration in '{config_path}' is invalid:\n" - f"{formatted_errors}\n" - "Hint: Please check the values in your configuration file and ensure they " - "match the expected types and requirements." + f"{formatted_errors}\n\n" + "Hint: Please review the errors above and correct your configuration file. For example:\n" + " - 'api_key' must be a non-empty string.\n" + " - 'timeout' must be a whole number greater than 0.\n" + " - 'log_level' must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL." ) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..cf46d77 --- /dev/null +++ b/src/main.py @@ -0,0 +1,136 @@ +import sys +from typing import Any, Dict, List + + +class ConfigError(Exception): + """A custom exception for user-facing configuration errors.""" + pass + + +def validate_config(config: Dict[str, Any]) -> Dict[str, Any]: + """ + Validates a configuration dictionary, raising ConfigError with actionable + advice if any checks fail. + """ + _check_for_required_keys(config) + _check_api_key(config) + _check_timeout(config) + _check_mode(config) + + return config + + +def _check_for_required_keys(config: Dict[str, Any]): + """Ensure all mandatory keys are present.""" + required_keys = ['api_key', 'timeout', 'mode'] + missing_keys = [key for key in required_keys if key not in config] + + if missing_keys: + key_str = ", ".join(f"'{key}'" for key in missing_keys) + raise ConfigError( + f"Configuration error: Missing required key(s): {key_str}.\n" + f" > Please add the missing key(s) to your configuration file." + ) + + +def _check_api_key(config: Dict[str, Any]): + """Validate the 'api_key' is a non-empty string.""" + api_key = config.get('api_key') + if not isinstance(api_key, str) or not api_key: + # A common mistake is an empty string or forgetting quotes in YAML. + # This message helps diagnose both. + raise ConfigError( + "Configuration error: The 'api_key' must be a non-empty string.\n" + f" > We found a value of type '{type(api_key).__name__}'.\n" + " > Please ensure your config looks like: api_key: \"your_secret_key\"" + ) + + +def _check_timeout(config: Dict[str, Any]): + """Validate the 'timeout' is a positive integer.""" + timeout = config.get('timeout') + if not isinstance(timeout, int) or timeout <= 0: + # Guide the user on the expected type and a valid range. + raise ConfigError( + "Configuration error: The 'timeout' must be a positive integer.\n" + f" > We found '{timeout}', which is a '{type(timeout).__name__}'.\n" + " > Please use a whole number greater than zero, like: timeout: 30" + ) + + +def _check_mode(config: Dict[str, Any]): + """Validate the 'mode' is one of the allowed values.""" + mode = config.get('mode') + valid_modes = ['fast', 'balanced', 'high_quality'] + if mode not in valid_modes: + # Show the user the exact value they provided and list the valid options. + # This prevents typos and guesswork. + raise ConfigError( + f"Configuration error: Invalid value for 'mode'.\n" + f" > We received '{mode}', but the only allowed values are: {valid_modes}.\n" + f" > Please update 'mode' in your configuration file." + ) + + +def run_with_config(config_name: str, config_data: Dict[str, Any]): + """A helper to simulate running the app with a given configuration.""" + print(f"--- Attempting to load '{config_name}' ---") + try: + validate_config(config_data) + print("✅ Configuration is valid and loaded successfully.\n") + except ConfigError as e: + # This is where the user sees our improved, actionable error message. + # Printing to stderr is standard practice for errors. + print(f"❌ {e}\n", file=sys.stderr) + + +def main(): + """ + Demonstrates configuration validation with user-friendly error messages + by running through several common invalid configuration scenarios. + """ + # A valid configuration to show the success case. + valid_config = { + "api_key": "sk-12345abcde", + "timeout": 30, + "mode": "balanced" + } + + # --- Example Error Cases --- + + # 1. Missing a required key ('api_key') + config_missing_key = { + "timeout": 60, + "mode": "fast" + } + + # 2. Incorrect data type for 'timeout' (string instead of int) + config_wrong_type = { + "api_key": "sk-12345abcde", + "timeout": "30", # Should be an integer + "mode": "fast" + } + + # 3. Invalid value for 'mode' + config_invalid_value = { + "api_key": "sk-12345abcde", + "timeout": 15, + "mode": "quick" # Not a valid mode + } + + # 4. Empty string for 'api_key' + config_empty_key = { + "api_key": "", + "timeout": 15, + "mode": "high_quality" + } + + run_with_config("Valid Config", valid_config) + run_with_config("Config Missing Key", config_missing_key) + run_with_config("Config Wrong Type", config_wrong_type) + run_with_config("Config Invalid Value", config_invalid_value) + run_with_config("Config Empty Key", config_empty_key) + + +if __name__ == "__main__": + main() \ No newline at end of file From 0dbd8b77e3e0a5958e9dfee7c4a79a5a41765fbc Mon Sep 17 00:00:00 2001 From: Mudit Agarwal Date: Wed, 29 Apr 2026 09:58:07 -0700 Subject: [PATCH 3/4] docs: add fork differences to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 3987e8b..d4d7429 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Pallets Projects Website +## What's different in this fork + + + + This is the code and content for https://palletsprojects.com. Static content from the `content` folder is loaded when the server is started then served quickly from memory or cache. This allows editing content as plain files like a From 29fbbb4f46cf2773a809a0f97981de1123d41f95 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 00:38:45 +0000 Subject: [PATCH 4/4] [pre-commit.ci lite] apply automatic fixes --- src/config.py | 12 +++++++----- src/main.py | 50 ++++++++++++++++++++------------------------------ 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/config.py b/src/config.py index 5326ea4..b806141 100644 --- a/src/config.py +++ b/src/config.py @@ -1,13 +1,14 @@ -import sys import tomllib from pathlib import Path -from typing import Optional -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel +from pydantic import Field +from pydantic import ValidationError class ConfigError(Exception): """Custom exception for configuration-related errors.""" + pass @@ -16,6 +17,7 @@ class Settings(BaseModel): Defines the application's configuration structure using Pydantic. This ensures that all configuration values are of the correct type. """ + api_key: str = Field(..., description="The API key for the primary service.") timeout: int = Field( default=30, gt=0, description="Default timeout for API requests in seconds." @@ -25,7 +27,7 @@ class Settings(BaseModel): pattern=r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$", description="Logging level (e.g., DEBUG, INFO, WARNING).", ) - database_url: Optional[str] = Field( + database_url: str | None = Field( default=None, description="Optional database connection URL." ) @@ -98,4 +100,4 @@ def load_config(config_path: Path = Path("config.toml")) -> Settings: " - 'api_key' must be a non-empty string.\n" " - 'timeout' must be a whole number greater than 0.\n" " - 'log_level' must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL." - ) \ No newline at end of file + ) diff --git a/src/main.py b/src/main.py index cf46d77..82383fe 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,14 @@ import sys -from typing import Any, Dict, List +from typing import Any class ConfigError(Exception): """A custom exception for user-facing configuration errors.""" + pass -def validate_config(config: Dict[str, Any]) -> Dict[str, Any]: +def validate_config(config: dict[str, Any]) -> dict[str, Any]: """ Validates a configuration dictionary, raising ConfigError with actionable advice if any checks fail. @@ -20,9 +21,9 @@ def validate_config(config: Dict[str, Any]) -> Dict[str, Any]: return config -def _check_for_required_keys(config: Dict[str, Any]): +def _check_for_required_keys(config: dict[str, Any]): """Ensure all mandatory keys are present.""" - required_keys = ['api_key', 'timeout', 'mode'] + required_keys = ["api_key", "timeout", "mode"] missing_keys = [key for key in required_keys if key not in config] if missing_keys: @@ -33,22 +34,22 @@ def _check_for_required_keys(config: Dict[str, Any]): ) -def _check_api_key(config: Dict[str, Any]): +def _check_api_key(config: dict[str, Any]): """Validate the 'api_key' is a non-empty string.""" - api_key = config.get('api_key') + api_key = config.get("api_key") if not isinstance(api_key, str) or not api_key: # A common mistake is an empty string or forgetting quotes in YAML. # This message helps diagnose both. raise ConfigError( "Configuration error: The 'api_key' must be a non-empty string.\n" f" > We found a value of type '{type(api_key).__name__}'.\n" - " > Please ensure your config looks like: api_key: \"your_secret_key\"" + ' > Please ensure your config looks like: api_key: "your_secret_key"' ) -def _check_timeout(config: Dict[str, Any]): +def _check_timeout(config: dict[str, Any]): """Validate the 'timeout' is a positive integer.""" - timeout = config.get('timeout') + timeout = config.get("timeout") if not isinstance(timeout, int) or timeout <= 0: # Guide the user on the expected type and a valid range. raise ConfigError( @@ -58,10 +59,10 @@ def _check_timeout(config: Dict[str, Any]): ) -def _check_mode(config: Dict[str, Any]): +def _check_mode(config: dict[str, Any]): """Validate the 'mode' is one of the allowed values.""" - mode = config.get('mode') - valid_modes = ['fast', 'balanced', 'high_quality'] + mode = config.get("mode") + valid_modes = ["fast", "balanced", "high_quality"] if mode not in valid_modes: # Show the user the exact value they provided and list the valid options. # This prevents typos and guesswork. @@ -72,7 +73,7 @@ def _check_mode(config: Dict[str, Any]): ) -def run_with_config(config_name: str, config_data: Dict[str, Any]): +def run_with_config(config_name: str, config_data: dict[str, Any]): """A helper to simulate running the app with a given configuration.""" print(f"--- Attempting to load '{config_name}' ---") try: @@ -90,40 +91,29 @@ def main(): by running through several common invalid configuration scenarios. """ # A valid configuration to show the success case. - valid_config = { - "api_key": "sk-12345abcde", - "timeout": 30, - "mode": "balanced" - } + valid_config = {"api_key": "sk-12345abcde", "timeout": 30, "mode": "balanced"} # --- Example Error Cases --- # 1. Missing a required key ('api_key') - config_missing_key = { - "timeout": 60, - "mode": "fast" - } + config_missing_key = {"timeout": 60, "mode": "fast"} # 2. Incorrect data type for 'timeout' (string instead of int) config_wrong_type = { "api_key": "sk-12345abcde", "timeout": "30", # Should be an integer - "mode": "fast" + "mode": "fast", } # 3. Invalid value for 'mode' config_invalid_value = { "api_key": "sk-12345abcde", "timeout": 15, - "mode": "quick" # Not a valid mode + "mode": "quick", # Not a valid mode } # 4. Empty string for 'api_key' - config_empty_key = { - "api_key": "", - "timeout": 15, - "mode": "high_quality" - } + config_empty_key = {"api_key": "", "timeout": 15, "mode": "high_quality"} run_with_config("Valid Config", valid_config) run_with_config("Config Missing Key", config_missing_key) @@ -133,4 +123,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()