Environment Handling¶
Introduction¶
The appcore
package provides sophisticated environment detection and
management. It automatically distinguishes between development and production
environments, loads environment variables from files, and supports custom
environment injection for testing.
>>> import appcore
>>> import platformdirs
>>> import io
Development vs Production Detection¶
Appcore automatically detects whether your application is running in development or production mode by examining how the package is installed:
Development mode detection:
Check package distribution: Uses
importlib_metadata.packages_distributions()
to see if your package is installed as a proper distributionIf not found: Assumes development mode (
editable = True
)Locate project root: Searches upward from the current file to find
pyproject.toml
Respect boundaries: Honors
GIT_CEILING_DIRECTORIES
environment variable to limit search scope
Production mode detection:
Package found in distribution: Package is properly installed (
pip install
, notpip install -e
)Set production mode:
editable = False
Use installed location: Points to the installed package location
Detection process happens automatically during appcore.prepare()
import asyncio
import contextlib
import appcore
async def main( ):
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare( exits )
# Check the automatically detected mode
if globals_dto.distribution.editable:
print( 'Running in DEVELOPMENT mode' )
print( f"Project root: {globals_dto.distribution.location}" )
print( f"Package name: {globals_dto.distribution.name}" )
# Development-specific behavior
debug_mode = True
use_local_configs = True
else:
print( 'Running in PRODUCTION mode' )
print( f"Installed at: {globals_dto.distribution.location}" )
print( f"Package name: {globals_dto.distribution.name}" )
# Production-specific behavior
debug_mode = False
use_local_configs = False
if __name__ == '__main__':
asyncio.run( main( ) )
Example output in development:
- Running in DEVELOPMENT mode
- Project root: /home/user/projects/myapp
- Package name: myapp
Example output in production:
- Running in PRODUCTION mode
- Installed at: /usr/local/lib/python3.10/site-packages/myapp
- Package name: myapp
Environment Variable Injection¶
For testing or custom deployment scenarios, you can inject environment variables directly:
>>> import asyncio
>>> import contextlib
>>>
>>> async def test_with_custom_env( ):
... # Define custom environment variables
... custom_env = {
... 'DEBUG': 'true',
... 'LOG_LEVEL': 'debug',
... 'API_KEY': 'test-key-12345'
... }
... # Create custom directories to avoid filesystem operations
... custom_dirs = platformdirs.PlatformDirs( 'test-app', ensure_exists = False )
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... directories = custom_dirs,
... environment = custom_env # Inject environment variables
... )
... print( f"Environment injected successfully" )
... return globals_dto
>>>
>>> # This would normally be run with asyncio.run()
>>> # globals_dto = asyncio.run( test_with_custom_env( ) )
Custom Platform Directories¶
You can override the default platform directory logic for testing or specialized deployments:
>>> import platformdirs
>>> # Create custom directory configuration
>>> custom_dirs = platformdirs.PlatformDirs(
... appname = 'test-app',
... appauthor = 'TestCorp',
... version = '1.0.0',
... ensure_exists = False # Don't create directories during testing
... )
>>>
>>> async def test_with_custom_directories( ):
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... directories = custom_dirs
... )
... # Use injected directories instead of auto-generated ones
... cache_dir = globals_dto.provide_cache_location( )
... print( f"Custom cache directory: {cache_dir}" )
... return globals_dto
Distribution Information Override¶
For advanced testing scenarios, you can provide custom distribution information:
>>> import appcore
>>> from pathlib import Path
>>> # Create mock distribution for testing
>>> test_distribution = appcore.DistributionInformation(
... name = 'my-test-app',
... location = Path( '/tmp/test-project' ),
... editable = True # Simulate development mode
... )
>>>
>>> async def test_development_behavior( ):
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... distribution = test_distribution
... )
... # Test development-specific code paths
... assert globals_dto.distribution.editable
... assert globals_dto.distribution.name == 'my-test-app'
... print( 'Development mode simulation successful' )
... return globals_dto
Configuration Stream with Environment¶
Combine stream-based configuration with environment variable injection:
>>> config_content = '''
... [application]
... name = "stream-app"
... debug = false
...
... [logging]
... level = "info"
... '''
>>>
>>> env_overrides = {
... 'DEBUG': 'true',
... 'LOG_LEVEL': 'debug'
... }
>>>
>>> def apply_env_overrides( config ):
... ''' Apply environment variable overrides to configuration. '''
... import os
... # Use DEBUG environment variable if present
... if 'DEBUG' in os.environ:
... if 'application' not in config:
... config[ 'application' ] = { }
... config[ 'application' ][ 'debug' ] = (
... os.environ[ 'DEBUG' ].lower( ) in ( 'true', '1', 'yes' ) )
>>>
>>> async def test_config_with_env( ):
... config_stream = io.StringIO( config_content )
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... configfile = config_stream,
... environment = env_overrides,
... configedits = ( apply_env_overrides, )
... )
... config = globals_dto.configuration
... debug_enabled = config.get( 'application', { } ).get( 'debug', False )
... print( f"Debug mode: {debug_enabled}" )
... return globals_dto
Environment File Loading¶
Appcore can load environment variables from .env
files with precedence
rules:
import asyncio
import contextlib
import appcore
async def main( ):
async with contextlib.AsyncExitStack( ) as exits:
# Enable environment file loading
globals_dto = await appcore.prepare(
exits,
environment = True # Load from .env files
)
# Environment variables are now available in os.environ
import os
debug_mode = os.environ.get( 'DEBUG', 'false' ).lower( ) == 'true'
api_key = os.environ.get( 'API_KEY', 'default-key' )
print( f"Debug mode: {debug_mode}" )
print( f"API key configured: {'Yes' if api_key != 'default-key' else 'No'}" )
if __name__ == '__main__':
asyncio.run( main( ) )
Environment file precedence (later files override earlier ones):
Configuration directory: Files specified in configuration includes
Local directory:
.env
file in current working directoryDevelopment mode: Project root
.env
file (takes precedence)
Complete Testing Setup¶
Here’s a comprehensive example showing how to set up a controlled environment for testing:
>>> import appcore
>>> import platformdirs
>>> import io
>>> import contextlib
>>> from pathlib import Path
>>> async def create_test_environment( ):
... ''' Create a completely controlled test environment. '''
... # Custom application info
... app_info = appcore.ApplicationInformation(
... name = 'test-suite',
... publisher = 'TestCorp',
... version = '0.1.0'
... )
... # Custom directories (no filesystem access)
... test_dirs = platformdirs.PlatformDirs(
... appname = 'test-suite',
... ensure_exists = False
... )
... # Mock distribution info
... test_dist = appcore.DistributionInformation(
... name = 'test-suite',
... location = Path( '/tmp/test' ),
... editable = True
... )
... # Custom environment variables
... test_env = {
... 'DEBUG': 'true',
... 'TEST_MODE': 'true',
... 'LOG_LEVEL': 'debug'
... }
... # Custom configuration
... test_config = '''
... [application]
... name = "test-suite"
... timeout = 10
...
... [testing]
... enabled = true
... '''
... config_stream = io.StringIO( test_config )
... # Initialize with all custom components
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... application = app_info,
... directories = test_dirs,
... distribution = test_dist,
... environment = test_env,
... configfile = config_stream
... )
... # Verify everything is set up correctly
... assert globals_dto.application.name == 'test-suite'
... assert globals_dto.distribution.editable == True
... config = globals_dto.configuration
... assert config[ 'testing' ][ 'enabled' ] == True
... print( 'Complete test environment setup successful' )
... return globals_dto
Complete test environment setup successful
Error Handling for Environment Issues¶
Environment setup can encounter various error conditions:
>>> import appcore
>>> import contextlib
>>> from pathlib import Path
>>> async def test_error_scenarios( ):
... # Test with invalid distribution location
... bad_dist = appcore.DistributionInformation(
... name = 'bad-app',
... location = Path( '/nonexistent/path' ),
... editable = True
... )
... try:
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... distribution = bad_dist
... )
... except Exception as e:
... print( f"Handled distribution error: {type( e ).__name__}" )
... # Test with invalid environment values
... bad_env = { 'INVALID_KEY': None } # None values not allowed
... try:
... async with contextlib.AsyncExitStack( ) as exits:
... globals_dto = await appcore.prepare(
... exits,
... environment = bad_env
... )
... except Exception as e:
... print( f"Handled environment error: {type( e ).__name__}" )
>>>
>>> # This would normally be run with asyncio.run()
>>> # asyncio.run( test_error_scenarios( ) )
Next Steps¶
This covers environment handling in appcore. For more topics, see:
Advanced Usage - Testing patterns and dependency injection strategies
Configuration Management - TOML loading and hierarchical includes
Basic Usage - Application setup and platform directories