Skip to content

Commit b1b282b

Browse files
authored
Merge pull request #3 from LexianDEV/copilot/fix-5f6c4c3f-8535-42d2-b6d6-1a33f38519ad
Add generalized configuration system with environment overrides for Python templates
2 parents 26e10ab + eed542e commit b1b282b

File tree

8 files changed

+621
-2
lines changed

8 files changed

+621
-2
lines changed

.env

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Environment Configuration
2+
# This file contains environment-specific settings that override config files
3+
4+
# Application Settings
5+
APP_NAME="My Custom App"
6+
APP_ENV=development
7+
APP_DEBUG=true
8+
APP_RESPONSE_NUMBER=100
9+
APP_MESSAGE="Hello from the environment!"
10+
11+
# Logging
12+
APP_LOGGING_LEVEL=DEBUG

CONFIG.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Configuration System
2+
3+
This Python template includes a Laravel-inspired configuration system that allows you to manage application settings through Python files and environment variable overrides.
4+
5+
## Features
6+
7+
- **Multiple Config Files**: Organize your configuration into separate Python files (e.g., `app.py`)
8+
- **Environment Overrides**: Use `.env` file or environment variables to override config values
9+
- **Dot Notation Access**: Access nested configuration using dot notation (e.g., `app.name`, `app.logging.level`)
10+
- **Runtime Changes**: Modify configuration values at runtime for testing or dynamic behavior
11+
- **Helper Functions**: Easy-to-use helper functions for common config operations
12+
13+
## Usage
14+
15+
### Basic Configuration Access
16+
17+
```python
18+
import helpers
19+
20+
# Get configuration values using dot notation
21+
app_name = helpers.get_config('app.name', 'Default App')
22+
response_number = helpers.get_config('app.response_number', 42)
23+
debug_mode = helpers.get_config('app.debug', False)
24+
25+
# Check if a configuration exists
26+
if helpers.has_config('app.name'):
27+
print("App name is configured")
28+
29+
# Get all configuration for a file
30+
all_app_config = helpers.get_all_config('app')
31+
```
32+
33+
### Environment Variable Overrides
34+
35+
Environment variables take precedence over config files. Use uppercase and underscores:
36+
37+
- `app.name``APP_NAME`
38+
- `app.response_number``APP_RESPONSE_NUMBER`
39+
- `app.logging.level``APP_LOGGING_LEVEL`
40+
41+
Example `.env` file:
42+
```bash
43+
APP_NAME=My Custom App
44+
APP_DEBUG=true
45+
APP_RESPONSE_NUMBER=100
46+
APP_MESSAGE=Hello from the environment!
47+
```
48+
49+
### Runtime Configuration Changes
50+
51+
```python
52+
# Modify configuration at runtime (doesn't persist)
53+
helpers.set_config('app.name', 'Runtime Modified Name')
54+
55+
# Reload configuration from files
56+
helpers.reload_config() # Reload all
57+
helpers.reload_config('app') # Reload specific file
58+
```
59+
60+
## Configuration Files
61+
62+
### Creating New Config Files
63+
64+
1. Create a new Python file in the `config/` directory
65+
2. Define configuration variables as module-level variables
66+
3. Use dictionaries for nested configuration
67+
68+
Example `config/services.py`:
69+
```python
70+
# External service configurations
71+
api_key = "your-api-key-here"
72+
73+
external_api = {
74+
"base_url": "https://api.example.com",
75+
"timeout": 30,
76+
"retry_attempts": 3
77+
}
78+
79+
cache = {
80+
"enabled": True,
81+
"ttl": 3600
82+
}
83+
```
84+
85+
### Existing Config Files
86+
87+
- **`app.py`**: Application-wide settings (name, version, debug mode, response number, etc.)
88+
89+
## Advanced Usage
90+
91+
### Direct Config Manager Access
92+
93+
```python
94+
from config_manager import ConfigManager
95+
96+
# Create a custom config manager
97+
config = ConfigManager(config_dir="custom_config", env_file="custom.env")
98+
99+
# Use the manager directly
100+
value = config.get('custom.setting', 'default')
101+
config.set('custom.setting', 'new_value')
102+
```
103+
104+
### Environment Helper
105+
106+
```python
107+
# Get environment variable with fallback to config
108+
value = helpers.env('API_KEY', 'default-key')
109+
value = helpers.env('app.name') # Falls back to config if no env var
110+
```
111+
112+
## Best Practices
113+
114+
1. **Organize by Feature**: Create separate config files for different aspects (database, mail, cache, etc.)
115+
2. **Use Environment Variables**: For sensitive data and environment-specific settings
116+
3. **Provide Defaults**: Always provide sensible default values
117+
4. **Document Settings**: Add comments to explain complex configuration options
118+
5. **Validate Config**: Add validation for critical configuration values in your application startup
119+
120+
## Example: Full Configuration Workflow
121+
122+
```python
123+
import helpers
124+
125+
# Application startup - validate critical config
126+
if not helpers.has_config('app.name'):
127+
raise ValueError("App name must be configured")
128+
129+
# Get database connection details
130+
db_config = helpers.get_all_config('database')
131+
connection = db_config['connections'][db_config['default']]
132+
133+
# Use environment override for sensitive data
134+
api_key = helpers.env('API_KEY')
135+
if not api_key:
136+
raise ValueError("API_KEY environment variable required")
137+
138+
# Runtime configuration for testing
139+
if helpers.get_config('app.env') == 'testing':
140+
helpers.set_config('app.debug', True)
141+
helpers.set_config('database.default', 'sqlite')
142+
```
143+
144+
This configuration system provides the flexibility of Laravel's config system while maintaining Python's simplicity and power.

config/app.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Application configuration settings.
3+
"""
4+
5+
# Application name
6+
name = "General Python Template"
7+
8+
# Application version
9+
version = "1.0.0"
10+
11+
# Environment (development, testing, production)
12+
env = "development"
13+
14+
# Debug mode
15+
debug = True
16+
17+
# Default response number that the app returns
18+
response_number = 42
19+
20+
# Message to display
21+
message = "Hello from the General Python Template!"
22+
23+
# Logging configuration
24+
logging = {
25+
"level": "INFO",
26+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
27+
"file": "logs/app.log"
28+
}

config_manager.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
Laravel-like configuration manager for Python applications.
3+
Supports loading config files from the config directory and environment overrides.
4+
"""
5+
6+
import os
7+
import importlib.util
8+
from typing import Any, Dict, Optional
9+
from pathlib import Path
10+
11+
12+
class ConfigManager:
13+
"""Manages application configuration with support for multiple config files and environment overrides."""
14+
15+
def __init__(self, config_dir: str = "config", env_file: str = ".env"):
16+
self.config_dir = Path(config_dir)
17+
self.env_file = Path(env_file)
18+
self._config_cache: Dict[str, Dict[str, Any]] = {}
19+
self._env_vars: Dict[str, str] = {}
20+
21+
# Load environment variables from .env file
22+
self._load_env_file()
23+
24+
def _load_env_file(self) -> None:
25+
"""Load environment variables from .env file if it exists."""
26+
if self.env_file.exists():
27+
with open(self.env_file, 'r') as f:
28+
for line in f:
29+
line = line.strip()
30+
if line and not line.startswith('#') and '=' in line:
31+
key, value = line.split('=', 1)
32+
self._env_vars[key.strip()] = value.strip().strip('"').strip("'")
33+
34+
def _load_config_file(self, config_name: str) -> Dict[str, Any]:
35+
"""Load a specific config file."""
36+
config_file = self.config_dir / f"{config_name}.py"
37+
38+
if not config_file.exists():
39+
return {}
40+
41+
spec = importlib.util.spec_from_file_location(config_name, config_file)
42+
if spec is None or spec.loader is None:
43+
return {}
44+
45+
module = importlib.util.module_from_spec(spec)
46+
spec.loader.exec_module(module)
47+
48+
# Extract all non-private attributes as config values
49+
config_data = {}
50+
for attr_name in dir(module):
51+
if not attr_name.startswith('_'):
52+
config_data[attr_name] = getattr(module, attr_name)
53+
54+
return config_data
55+
56+
def get(self, key: str, default: Any = None) -> Any:
57+
"""
58+
Get a configuration value using dot notation (e.g., 'app.name' or 'database.host').
59+
Environment variables take precedence over config files.
60+
"""
61+
# Check for environment variable override first
62+
env_key = key.upper().replace('.', '_')
63+
if env_key in self._env_vars:
64+
value = self._env_vars[env_key]
65+
# Convert string boolean values
66+
if value.lower() in ('true', 'false'):
67+
return value.lower() == 'true'
68+
return value
69+
if env_key in os.environ:
70+
value = os.environ[env_key]
71+
# Convert string boolean values
72+
if value.lower() in ('true', 'false'):
73+
return value.lower() == 'true'
74+
return value
75+
76+
# Parse the key to get config file and setting
77+
if '.' not in key:
78+
return default
79+
80+
config_name, setting_path = key.split('.', 1)
81+
82+
# Load config file if not cached
83+
if config_name not in self._config_cache:
84+
self._config_cache[config_name] = self._load_config_file(config_name)
85+
86+
config_data = self._config_cache[config_name]
87+
88+
# Navigate through nested settings using dot notation
89+
current = config_data
90+
for part in setting_path.split('.'):
91+
if isinstance(current, dict) and part in current:
92+
current = current[part]
93+
else:
94+
return default
95+
96+
return current
97+
98+
def set(self, key: str, value: Any) -> None:
99+
"""Set a configuration value in the cache (runtime only)."""
100+
if '.' not in key:
101+
return
102+
103+
config_name, setting_path = key.split('.', 1)
104+
105+
# Ensure config is loaded
106+
if config_name not in self._config_cache:
107+
self._config_cache[config_name] = self._load_config_file(config_name)
108+
109+
# Navigate and set the value
110+
current = self._config_cache[config_name]
111+
parts = setting_path.split('.')
112+
113+
for part in parts[:-1]:
114+
if part not in current or not isinstance(current[part], dict):
115+
current[part] = {}
116+
current = current[part]
117+
118+
current[parts[-1]] = value
119+
120+
def reload(self, config_name: Optional[str] = None) -> None:
121+
"""Reload configuration files. If config_name is None, reload all."""
122+
if config_name:
123+
if config_name in self._config_cache:
124+
del self._config_cache[config_name]
125+
else:
126+
self._config_cache.clear()
127+
self._load_env_file()
128+
129+
def all(self, config_name: str) -> Dict[str, Any]:
130+
"""Get all configuration values for a specific config file."""
131+
if config_name not in self._config_cache:
132+
self._config_cache[config_name] = self._load_config_file(config_name)
133+
return self._config_cache[config_name].copy()
134+
135+
def has(self, key: str) -> bool:
136+
"""Check if a configuration key exists."""
137+
sentinel = object()
138+
try:
139+
result = self.get(key, sentinel)
140+
return result is not sentinel
141+
except:
142+
return False
143+
144+
145+
# Global config manager instance
146+
_config_manager = ConfigManager()
147+
148+
149+
def config(key: str, default: Any = None) -> Any:
150+
"""Get a configuration value."""
151+
return _config_manager.get(key, default)
152+
153+
154+
def config_set(key: str, value: Any) -> None:
155+
"""Set a configuration value."""
156+
_config_manager.set(key, value)
157+
158+
159+
def config_reload(config_name: Optional[str] = None) -> None:
160+
"""Reload configuration."""
161+
_config_manager.reload(config_name)
162+
163+
164+
def config_all(config_name: str) -> Dict[str, Any]:
165+
"""Get all configuration for a config file."""
166+
return _config_manager.all(config_name)
167+
168+
169+
def config_has(key: str) -> bool:
170+
"""Check if a configuration key exists."""
171+
return _config_manager.has(key)

0 commit comments

Comments
 (0)