Skip to content

Commit 647e913

Browse files
authored
Merge pull request #7 from LexianDEV/copilot/fix-b2ef1dad-ec82-42a2-9d48-7a4afb775120
Replace .env file with bundled JSON configuration system
2 parents c98c2fb + 8fd7c6b commit 647e913

8 files changed

Lines changed: 165 additions & 65 deletions

File tree

.env.example

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

CONFIG.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ This Python template includes a Laravel-inspired configuration system that allow
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+
- **JSON Configuration**: Use `config.json` file for non-sensitive settings that get bundled into the application
9+
- **Environment Overrides**: Use environment variables to override config values
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,44 @@ if helpers.has_config('app.name'):
3031
all_app_config = helpers.get_all_config('app')
3132
```
3233

34+
### JSON Configuration Overrides
35+
36+
JSON configuration takes precedence over config files but environment variables still override everything. The `config.json` file is bundled into the application when built and contains non-sensitive settings:
37+
38+
- `app.name``APP_NAME` in JSON
39+
- `app.response_number``APP_RESPONSE_NUMBER` in JSON
40+
- `app.logging.level``APP_LOGGING_LEVEL` in JSON
41+
42+
Example `config.json` file:
43+
```json
44+
{
45+
"APP_NAME": "My Custom App",
46+
"APP_ENV": "development",
47+
"APP_DEBUG": true,
48+
"APP_RESPONSE_NUMBER": 100,
49+
"APP_MESSAGE": "Hello from the environment!",
50+
"APP_LOGGING_LEVEL": "DEBUG"
51+
}
52+
```
53+
3354
### Environment Variable Overrides
3455

35-
Environment variables take precedence over config files. Use uppercase and underscores:
56+
Environment variables take highest precedence over both JSON config and config files. Use uppercase and underscores:
3657

3758
- `app.name``APP_NAME`
3859
- `app.response_number``APP_RESPONSE_NUMBER`
3960
- `app.logging.level``APP_LOGGING_LEVEL`
4061

41-
Example `.env` file:
62+
Example environment variables:
4263
```bash
43-
APP_NAME=My Custom App
44-
APP_DEBUG=true
45-
APP_RESPONSE_NUMBER=100
46-
APP_MESSAGE=Hello from the environment!
64+
export APP_NAME="My Custom App"
65+
export APP_DEBUG=true
66+
export APP_RESPONSE_NUMBER=100
67+
export APP_MESSAGE="Hello from the environment!"
4768
```
4869

70+
**Note:** The bundled `config.json` file provides application defaults and is automatically extracted and cleaned up when the application runs. Environment variables can still be used to override any setting at runtime.
71+
4972
### Runtime Configuration Changes
5073

5174
```python
@@ -94,7 +117,7 @@ cache = {
94117
from config_manager import ConfigManager
95118

96119
# Create a custom config manager
97-
config = ConfigManager(config_dir="custom_config", env_file="custom.env")
120+
config = ConfigManager(config_dir="custom_config", json_file="custom_config.json")
98121

99122
# Use the manager directly
100123
value = config.get('custom.setting', 'default')
@@ -112,10 +135,13 @@ value = helpers.env('app.name') # Falls back to config if no env var
112135
## Best Practices
113136

114137
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
138+
2. **Use JSON for Non-Sensitive Defaults**: Store application defaults in `config.json` that get bundled with the application
139+
3. **Use Environment Variables**: For sensitive data and environment-specific settings
140+
4. **Provide Defaults**: Always provide sensible default values
141+
5. **Document Settings**: Add comments to explain complex configuration options
142+
6. **Validate Config**: Add validation for critical configuration values in your application startup
143+
144+
**Security Note**: The `config.json` file is bundled into the application and can be viewed by anyone with access to the built executable. Only store non-sensitive configuration in this file. Use environment variables for sensitive data like API keys, passwords, and secrets.
119145

120146
## Example: Full Configuration Workflow
121147

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.json', '.')],
99
hiddenimports=[],
1010
hookspath=[],
1111
hooksconfig={},

config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"APP_NAME": "My Custom App",
3+
"APP_VERSION": "1.0.0",
4+
"APP_ENV": "development",
5+
"APP_DEBUG": true,
6+
"APP_RESPONSE_NUMBER": 100,
7+
"APP_MESSAGE": "Hello from the environment!",
8+
"APP_LOGGING_LEVEL": "DEBUG"
9+
}

config_manager.py

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,67 @@
44
"""
55

66
import os
7+
import sys
8+
import json
79
import importlib.util
810
from typing import Any, Dict, Optional
911
from pathlib import Path
12+
import tempfile
13+
import shutil
1014

1115

1216
class ConfigManager:
1317
"""Manages application configuration with support for multiple config files and environment overrides."""
1418

15-
def __init__(self, config_dir: str = "config", env_file: str = ".env"):
19+
def __init__(self, config_dir: str = "config", json_file: str = "config.json"):
1620
self.config_dir = Path(config_dir)
17-
self.env_file = Path(env_file)
21+
self.json_file = Path(json_file)
1822
self._config_cache: Dict[str, Dict[str, Any]] = {}
19-
self._env_vars: Dict[str, str] = {}
23+
self._json_vars: Dict[str, Any] = {}
24+
self._extracted_json_path: Optional[Path] = None
2025

21-
# Load environment variables from .env file
22-
self._load_env_file()
26+
# Load configuration from JSON file
27+
self._load_json_file()
2328

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("'")
29+
def _load_json_file(self) -> None:
30+
"""Load configuration from JSON file. If bundled, extract it temporarily."""
31+
json_path = self.json_file
32+
33+
# Check if this is a bundled application (PyInstaller)
34+
if hasattr(sys, '_MEIPASS'):
35+
# Extract JSON from bundled resources to a temporary location
36+
bundled_json = Path(sys._MEIPASS) / self.json_file.name
37+
if bundled_json.exists():
38+
# Create a temporary file
39+
temp_fd, temp_path = tempfile.mkstemp(suffix='.json')
40+
os.close(temp_fd)
41+
42+
# Copy the bundled JSON to temp location
43+
shutil.copy2(bundled_json, temp_path)
44+
json_path = Path(temp_path)
45+
self._extracted_json_path = json_path
46+
47+
# Load the JSON file if it exists
48+
if json_path.exists():
49+
try:
50+
with open(json_path, 'r', encoding='utf-8') as f:
51+
self._json_vars = json.load(f)
52+
except (json.JSONDecodeError, IOError) as e:
53+
print(f"Warning: Could not load JSON config file {json_path}: {e}")
54+
self._json_vars = {}
55+
56+
def __del__(self):
57+
"""Clean up extracted JSON file when the ConfigManager is destroyed."""
58+
self._cleanup_extracted_json()
59+
60+
def _cleanup_extracted_json(self) -> None:
61+
"""Delete the extracted JSON file if it exists."""
62+
if hasattr(self, '_extracted_json_path') and self._extracted_json_path and self._extracted_json_path.exists():
63+
try:
64+
self._extracted_json_path.unlink()
65+
self._extracted_json_path = None
66+
except OSError:
67+
pass # Ignore cleanup errors
3368

3469
def _load_config_file(self, config_name: str) -> Dict[str, Any]:
3570
"""Load a specific config file."""
@@ -56,24 +91,23 @@ def _load_config_file(self, config_name: str) -> Dict[str, Any]:
5691
def get(self, key: str, default: Any = None) -> Any:
5792
"""
5893
Get a configuration value using dot notation (e.g., 'app.name' or 'database.host').
59-
Environment variables take precedence over config files.
94+
JSON configuration takes precedence over config files.
95+
Environment variables take precedence over everything.
6096
"""
61-
# Check for environment variable override first
97+
# Check for environment variable override first (highest priority)
6298
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
6999
if env_key in os.environ:
70100
value = os.environ[env_key]
71101
# Convert string boolean values
72102
if value.lower() in ('true', 'false'):
73103
return value.lower() == 'true'
74104
return value
105+
106+
# Check for JSON config override (second priority)
107+
if env_key in self._json_vars:
108+
return self._json_vars[env_key]
75109

76-
# Parse the key to get config file and setting
110+
# Parse the key to get config file and setting (lowest priority)
77111
if '.' not in key:
78112
return default
79113

@@ -124,7 +158,9 @@ def reload(self, config_name: Optional[str] = None) -> None:
124158
del self._config_cache[config_name]
125159
else:
126160
self._config_cache.clear()
127-
self._load_env_file()
161+
# Clean up old extracted JSON and reload
162+
self._cleanup_extracted_json()
163+
self._load_json_file()
128164

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

modules/display.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def show_app_info(self):
2424
"""Display application information."""
2525
# Get basic app configuration
2626
app_name = helpers.get_config('app.name', 'Unknown App')
27-
app_version = helpers.get_config('app.version', '0.0.0')
27+
app_version = helpers.get_config('app.version', '1.0.0')
2828
app_env = helpers.get_config('app.env', 'unknown')
2929
app_debug = helpers.get_config('app.debug', False)
3030

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ It was developed by LexianDEV
1515
This template includes a powerful configuration system inspired by Laravel. You can:
1616

1717
- Define configuration in Python files (`config/app.py`, `config/database.py`, etc.)
18-
- Override settings with environment variables or `.env` file
18+
- Override settings with JSON configuration or environment variables
1919
- Access configuration using dot notation: `helpers.get_config('app.name')`
2020
- Modify configuration at runtime for testing
2121

tests/test_config.py

Lines changed: 55 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_json_overrides():
51+
"""Test JSON configuration overrides."""
52+
print("Testing JSON configuration overrides...")
5353

5454
with tempfile.TemporaryDirectory() as temp_dir:
5555
config_dir = Path(temp_dir) / "config"
@@ -62,21 +62,23 @@ 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 JSON config file
66+
json_file = Path(temp_dir) / "config.json"
67+
json_file.write_text("""
68+
{
69+
"APP_NAME": "Override App",
70+
"APP_DEBUG": true
71+
}
7072
""")
7173

7274
# Test config manager
73-
manager = ConfigManager(config_dir=str(config_dir), env_file=str(env_file))
75+
manager = ConfigManager(config_dir=str(config_dir), json_file=str(json_file))
7476

75-
# Test environment overrides
77+
# Test JSON overrides
7678
assert manager.get('app.name') == "Override App"
7779
assert manager.get('app.debug') == True
7880

79-
print("✓ Environment overrides work")
81+
print("✓ JSON configuration overrides work")
8082

8183

8284
def test_runtime_config_changes():
@@ -93,8 +95,8 @@ def test_runtime_config_changes():
9395
name = "Original App"
9496
""")
9597

96-
# Test config manager with no .env file
97-
manager = ConfigManager(config_dir=str(config_dir), env_file="/nonexistent/.env")
98+
# Test config manager with no JSON file
99+
manager = ConfigManager(config_dir=str(config_dir), json_file="/nonexistent/config.json")
98100

99101
# Test runtime changes
100102
original_name = manager.get('app.name')
@@ -110,6 +112,44 @@ def test_runtime_config_changes():
110112
print("✓ Runtime config changes work")
111113

112114

115+
def test_bundled_json_extraction():
116+
"""Test JSON extraction and cleanup for bundled applications."""
117+
print("Testing bundled JSON extraction and cleanup...")
118+
119+
with tempfile.TemporaryDirectory() as temp_dir:
120+
config_dir = Path(temp_dir) / "config"
121+
config_dir.mkdir()
122+
123+
# Create a test config file
124+
test_config = config_dir / "app.py"
125+
test_config.write_text("""
126+
name = "Original App"
127+
debug = False
128+
""")
129+
130+
# Create JSON config file
131+
json_file = Path(temp_dir) / "config.json"
132+
json_file.write_text("""
133+
{
134+
"APP_NAME": "Bundled App",
135+
"APP_DEBUG": true
136+
}
137+
""")
138+
139+
# Test config manager
140+
manager = ConfigManager(config_dir=str(config_dir), json_file=str(json_file))
141+
142+
# Test JSON loading
143+
assert manager.get('app.name') == "Bundled App"
144+
assert manager.get('app.debug') == True
145+
146+
# Test cleanup - the manager should properly initialize without extracted path
147+
# since this is not a bundled environment
148+
assert manager._extracted_json_path is None
149+
150+
print("✓ Bundled JSON extraction and cleanup work")
151+
152+
113153
def test_helpers_integration():
114154
"""Test helper functions."""
115155
print("Testing helper functions...")
@@ -133,8 +173,9 @@ def run_tests():
133173

134174
try:
135175
test_config_loading()
136-
test_env_overrides()
176+
test_json_overrides()
137177
test_runtime_config_changes()
178+
test_bundled_json_extraction()
138179
test_helpers_integration()
139180

140181
print("\n✅ All tests passed!")

0 commit comments

Comments
 (0)