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:

  1. Check package distribution: Uses importlib_metadata.packages_distributions() to see if your package is installed as a proper distribution

  2. If not found: Assumes development mode (editable = True)

  3. Locate project root: Searches upward from the current file to find pyproject.toml

  4. Respect boundaries: Honors GIT_CEILING_DIRECTORIES environment variable to limit search scope

Production mode detection:

  1. Package found in distribution: Package is properly installed (pip install, not pip install -e)

  2. Set production mode: editable = False

  3. 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):

  1. Configuration directory: Files specified in configuration includes

  2. Local directory: .env file in current working directory

  3. Development 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