Skip to content

Commit efb650d

Browse files
authored
Merge pull request #5 from LexianDEV/copilot/fix-30608041-4694-46cc-8bd1-2ce8b482dc47
Replace .env file with bundled settings.py for application distribution
2 parents 9b95018 + 26c717a commit efb650d

File tree

6 files changed

+102
-65
lines changed

6 files changed

+102
-65
lines changed

.env.example

Lines changed: 0 additions & 12 deletions
This file was deleted.

CONFIG.md

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Configuration System
22

3-
This Python template includes a Laravel-inspired configuration system that allows you to manage application settings through Python files and environment variable overrides.
3+
This Python template includes a Laravel-inspired configuration system that allows you to manage application settings through Python files and settings overrides.
44

55
## Features
66

77
- **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
8+
- **Settings Overrides**: Use `config/settings.py` file or environment variables to override config values
9+
- **Bundled Settings**: Settings are bundled with the application binary (no external .env files needed)
910
- **Dot Notation Access**: Access nested configuration using dot notation (e.g., `app.name`, `app.logging.level`)
1011
- **Runtime Changes**: Modify configuration values at runtime for testing or dynamic behavior
1112
- **Helper Functions**: Easy-to-use helper functions for common config operations
@@ -30,22 +31,37 @@ if helpers.has_config('app.name'):
3031
all_app_config = helpers.get_all_config('app')
3132
```
3233

33-
### Environment Variable Overrides
34+
### Settings File Overrides
3435

35-
Environment variables take precedence over config files. Use uppercase and underscores:
36+
Settings take precedence over config files but can be overridden by environment variables. Use uppercase and underscores:
3637

3738
- `app.name``APP_NAME`
3839
- `app.response_number``APP_RESPONSE_NUMBER`
3940
- `app.logging.level``APP_LOGGING_LEVEL`
4041

41-
Example `.env` file:
42+
Example `config/settings.py` file:
43+
```python
44+
# Application Settings - these override config/app.py values
45+
APP_NAME = "My Custom App"
46+
APP_DEBUG = True
47+
APP_RESPONSE_NUMBER = 100
48+
APP_MESSAGE = "Hello from the bundled settings!"
49+
```
50+
51+
### Environment Variable Overrides
52+
53+
Environment variables have the highest precedence and will override both config files and settings:
54+
4255
```bash
43-
APP_NAME=My Custom App
44-
APP_DEBUG=true
45-
APP_RESPONSE_NUMBER=100
46-
APP_MESSAGE=Hello from the environment!
56+
export APP_NAME="Production App"
57+
export APP_DEBUG=false
4758
```
4859

60+
**Precedence Order (highest to lowest):**
61+
1. Environment variables
62+
2. Settings file (`config/settings.py`)
63+
3. Config files (`config/*.py`)
64+
4965
### Runtime Configuration Changes
5066

5167
```python
@@ -85,6 +101,7 @@ cache = {
85101
### Existing Config Files
86102

87103
- **`app.py`**: Application-wide settings (name, version, debug mode, response number, etc.)
104+
- **`settings.py`**: Override settings bundled with the application
88105

89106
## Advanced Usage
90107

@@ -94,7 +111,7 @@ cache = {
94111
from config_manager import ConfigManager
95112

96113
# Create a custom config manager
97-
config = ConfigManager(config_dir="custom_config", env_file="custom.env")
114+
config = ConfigManager(config_dir="custom_config", settings_file="custom_settings")
98115

99116
# Use the manager directly
100117
value = config.get('custom.setting', 'default')
@@ -112,10 +129,11 @@ value = helpers.env('app.name') # Falls back to config if no env var
112129
## Best Practices
113130

114131
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
132+
2. **Use Settings File**: For non-sensitive application-specific overrides that should be bundled
133+
3. **Use Environment Variables**: For sensitive data and deployment-specific settings
134+
4. **Provide Defaults**: Always provide sensible default values
135+
5. **Document Settings**: Add comments to explain complex configuration options
136+
6. **Validate Config**: Add validation for critical configuration values in your application startup
119137

120138
## Example: Full Configuration Workflow
121139

@@ -141,4 +159,4 @@ if helpers.get_config('app.env') == 'testing':
141159
helpers.set_config('database.default', 'sqlite')
142160
```
143161

144-
This configuration system provides the flexibility of Laravel's config system while maintaining Python's simplicity and power.
162+
This configuration system provides the flexibility of Laravel's config system while maintaining Python's simplicity and power. Settings are now bundled with the application for easier distribution.

build.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ a = Analysis(
55
['main.py'],
66
pathex=[],
77
binaries=[],
8-
datas=[],
8+
datas=[('config/settings.py', 'config')],
99
hiddenimports=[],
1010
hookspath=[],
1111
hooksconfig={},

config/settings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Application-wide settings and overrides.
3+
This file contains environment-specific settings that can override config values.
4+
Unlike traditional .env files, this is bundled with the application.
5+
"""
6+
7+
# Application Settings - these override config/app.py values
8+
APP_NAME = "My Custom App"
9+
APP_ENV = "development"
10+
APP_DEBUG = True
11+
APP_RESPONSE_NUMBER = 100
12+
APP_MESSAGE = "Hello from the bundled settings!"
13+
14+
# Logging Settings
15+
APP_LOGGING_LEVEL = "DEBUG"

config_manager.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
Laravel-like configuration manager for Python applications.
3-
Supports loading config files from the config directory and environment overrides.
3+
Supports loading config files from the config directory and settings overrides.
44
"""
55

66
import os
@@ -10,26 +10,35 @@
1010

1111

1212
class ConfigManager:
13-
"""Manages application configuration with support for multiple config files and environment overrides."""
13+
"""Manages application configuration with support for multiple config files and settings overrides."""
1414

15-
def __init__(self, config_dir: str = "config", env_file: str = ".env"):
15+
def __init__(self, config_dir: str = "config", settings_file: str = "settings"):
1616
self.config_dir = Path(config_dir)
17-
self.env_file = Path(env_file)
17+
self.settings_file = settings_file
1818
self._config_cache: Dict[str, Dict[str, Any]] = {}
19-
self._env_vars: Dict[str, str] = {}
19+
self._settings_vars: Dict[str, str] = {}
2020

21-
# Load environment variables from .env file
22-
self._load_env_file()
21+
# Load settings variables from settings file
22+
self._load_settings_file()
2323

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("'")
24+
def _load_settings_file(self) -> None:
25+
"""Load settings variables from settings.py file if it exists."""
26+
settings_file_path = self.config_dir / f"{self.settings_file}.py"
27+
if settings_file_path.exists():
28+
# Load the settings file as a module
29+
spec = importlib.util.spec_from_file_location(self.settings_file, settings_file_path)
30+
if spec is None or spec.loader is None:
31+
return
32+
33+
module = importlib.util.module_from_spec(spec)
34+
spec.loader.exec_module(module)
35+
36+
# Extract all non-private attributes as settings values
37+
for attr_name in dir(module):
38+
if not attr_name.startswith('_') and not callable(getattr(module, attr_name)):
39+
value = getattr(module, attr_name)
40+
# Store as string for consistency with old env behavior
41+
self._settings_vars[attr_name] = str(value)
3342

3443
def _load_config_file(self, config_name: str) -> Dict[str, Any]:
3544
"""Load a specific config file."""
@@ -56,21 +65,28 @@ def _load_config_file(self, config_name: str) -> Dict[str, Any]:
5665
def get(self, key: str, default: Any = None) -> Any:
5766
"""
5867
Get a configuration value using dot notation (e.g., 'app.name' or 'database.host').
59-
Environment variables take precedence over config files.
68+
Environment variables have highest precedence, then settings variables, then config files.
6069
"""
61-
# Check for environment variable override first
70+
# Check for environment variable override first (highest precedence)
6271
env_key = key.upper().replace('.', '_')
63-
if env_key in self._env_vars:
64-
value = self._env_vars[env_key]
72+
if env_key in os.environ:
73+
value = os.environ[env_key]
6574
# Convert string boolean values
6675
if value.lower() in ('true', 'false'):
6776
return value.lower() == 'true'
6877
return value
69-
if env_key in os.environ:
70-
value = os.environ[env_key]
78+
79+
# Check for settings variable override second
80+
if env_key in self._settings_vars:
81+
value = self._settings_vars[env_key]
7182
# Convert string boolean values
7283
if value.lower() in ('true', 'false'):
7384
return value.lower() == 'true'
85+
# Try to convert to int if it looks like a number
86+
try:
87+
return int(value)
88+
except ValueError:
89+
pass
7490
return value
7591

7692
# Parse the key to get config file and setting
@@ -124,7 +140,7 @@ def reload(self, config_name: Optional[str] = None) -> None:
124140
del self._config_cache[config_name]
125141
else:
126142
self._config_cache.clear()
127-
self._load_env_file()
143+
self._load_settings_file()
128144

129145
def all(self, config_name: str) -> Dict[str, Any]:
130146
"""Get all configuration values for a specific config file."""

tests/test_config.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def test_config_loading():
4747
print("✓ Config file loading works")
4848

4949

50-
def test_env_overrides():
51-
"""Test environment variable overrides."""
52-
print("Testing environment overrides...")
50+
def test_settings_overrides():
51+
"""Test settings file overrides."""
52+
print("Testing settings overrides...")
5353

5454
with tempfile.TemporaryDirectory() as temp_dir:
5555
config_dir = Path(temp_dir) / "config"
@@ -62,21 +62,21 @@ def test_env_overrides():
6262
debug = False
6363
""")
6464

65-
# Create .env file
66-
env_file = Path(temp_dir) / ".env"
67-
env_file.write_text("""
68-
APP_NAME=Override App
69-
APP_DEBUG=true
65+
# Create settings file
66+
settings_file = config_dir / "settings.py"
67+
settings_file.write_text("""
68+
APP_NAME = "Override App"
69+
APP_DEBUG = True
7070
""")
7171

7272
# Test config manager
73-
manager = ConfigManager(config_dir=str(config_dir), env_file=str(env_file))
73+
manager = ConfigManager(config_dir=str(config_dir), settings_file="settings")
7474

75-
# Test environment overrides
75+
# Test settings overrides
7676
assert manager.get('app.name') == "Override App"
7777
assert manager.get('app.debug') == True
7878

79-
print("✓ Environment overrides work")
79+
print("✓ Settings overrides work")
8080

8181

8282
def test_runtime_config_changes():
@@ -93,8 +93,8 @@ def test_runtime_config_changes():
9393
name = "Original App"
9494
""")
9595

96-
# Test config manager with no .env file
97-
manager = ConfigManager(config_dir=str(config_dir), env_file="/nonexistent/.env")
96+
# Test config manager with no settings file
97+
manager = ConfigManager(config_dir=str(config_dir), settings_file="nonexistent")
9898

9999
# Test runtime changes
100100
original_name = manager.get('app.name')
@@ -133,7 +133,7 @@ def run_tests():
133133

134134
try:
135135
test_config_loading()
136-
test_env_overrides()
136+
test_settings_overrides()
137137
test_runtime_config_changes()
138138
test_helpers_integration()
139139

0 commit comments

Comments
 (0)