This guide will help you migrate your code from Bevy 3.0 beta to Bevy 3.1 beta.
Bevy 3.1 beta introduces a dependency injection system with the following key improvements:
- ✅ Type-safe: Full IDE autocomplete and type checking with
Inject[T] - ✅ Python 3.12+ features: Uses modern type system with
typekeyword - ✅ Rich hook system: Enhanced extensibility with detailed context
- ✅ Better debugging: Comprehensive debug mode and execution tracking
- ✅ Flexible strategies: Multiple injection strategies for different use cases
- ✅ Optional dependencies: Native support for
T | Nonetypes
Before (3.0 beta):
from bevy import inject, dependency
@inject
def process_data(service: UserService = dependency()):
return service.process()After (3.1 beta):
from bevy import injectable, auto_inject, Inject
# Option 1: Use with container
@injectable
def process_data(service: Inject[UserService]):
return service.process()
# Option 2: Use with global container
@auto_inject
@injectable
def process_data(service: Inject[UserService]):
return service.process()Before (3.0 beta):
# Default parameter approach
def func(service: UserService = dependency()): passAfter (3.1 beta):
# Type annotation approach
def func(service: Inject[UserService]): passBefore (3.0 beta):
def custom_factory(container):
return UserService("custom")
@inject
def func(service: UserService = dependency(custom_factory)):
passAfter (3.1 beta):
from bevy import Options
def custom_factory():
return UserService("custom")
@injectable
def func(service: Inject[UserService, Options(default_factory=custom_factory)]):
passBefore:
from bevy import inject, dependency, get_registry, get_containerAfter:
from bevy import injectable, auto_inject, Inject, Options, get_registry, get_containerReplace all @inject decorators:
Simple functions (global):
# OLD
@inject
def func(service: UserService = dependency()):
pass
# NEW
@auto_inject
@injectable
def func(service: Inject[UserService]):
passFunctions called via container:
# OLD
def func(service: UserService = dependency()):
pass
container.call(func)
# NEW
@injectable
def func(service: Inject[UserService]):
pass
container.call(func)Replace dependency() defaults with Inject[T] annotations:
Basic dependencies:
# OLD
def func(
service: UserService = dependency(),
db: Database = dependency()
):
pass
# NEW
def func(
service: Inject[UserService],
db: Inject[Database]
):
passCustom factories:
# OLD
def func(
service: UserService = dependency(my_factory)
):
pass
# NEW
def func(
service: Inject[UserService, Options(default_factory=my_factory)]
):
passBefore (3.0 beta):
from bevy import get_registry
from bevy.factories import create_type_factory
registry = get_registry()
registry.add_factory(create_type_factory(UserService))After (3.1 beta):
from bevy import get_registry
from bevy.bundled.type_factory_hook import type_factory
# Option 1: Use type_factory hook for automatic creation
registry = get_registry()
type_factory.register_hook(registry)
# Option 2: Still use explicit factories
from bevy.factories import create_type_factory
registry.add_factory(create_type_factory(UserService))Before (3.0 beta):
class UserService:
@inject
def __init__(self, db: Database = dependency()):
self.db = dbAfter (3.1 beta):
class UserService:
@injectable
def __init__(self, db: Inject[Database]):
self.db = db
# Or use constructor injection automatically
class UserService:
def __init__(self, db: Database): # Will be injected if using ANY_NOT_PASSED
self.db = dbBefore:
@inject
def get_user_data(user_id: str, service: UserService = dependency()):
return service.get_user(user_id)
registry = get_registry()
registry.add_factory(create_type_factory(UserService))
result = get_user_data("123")After:
@auto_inject
@injectable
def get_user_data(user_id: str, service: Inject[UserService]):
return service.get_user(user_id)
registry = get_registry()
type_factory.register_hook(registry) # Enables automatic creation
result = get_user_data("123")Before:
# Test setup
registry = Registry()
registry.add_factory(create_type_factory(Database, "test"))
container = registry.create_container()
def process_data(db: Database = dependency()):
return db.process()
result = container.call(process_data)After:
# Test setup
registry = Registry()
type_factory.register_hook(registry)
container = Container(registry)
container.add(Database("test")) # Override default
@injectable
def process_data(db: Inject[Database]):
return db.process()
result = container.call(process_data)Before:
# Not directly supported - had to use try/catch or manual checks
@inject
def func(required: UserService = dependency()):
try:
optional = get_container().get(CacheService)
except:
optional = NoneAfter:
# Native support for optional dependencies
@injectable
def func(
required: Inject[UserService],
optional: Inject[CacheService | None]
):
if optional:
# Use optional service
passBefore:
def config_factory(container):
return AppConfig(env="production")
@inject
def app_startup(config: AppConfig = dependency(config_factory)):
passAfter:
@injectable
def app_startup(
config: Inject[AppConfig, Options(default_factory=lambda: AppConfig(env="production"))]
):
passControl which parameters get injected:
# Only inject Inject[T] parameters (default)
@injectable
def explicit(service: Inject[UserService], manual: str): pass
# Inject any typed parameter not provided
@injectable(strategy=InjectionStrategy.ANY_NOT_PASSED)
def auto(service: UserService, manual: str): pass
# Only inject specific parameters
@injectable(strategy=InjectionStrategy.ONLY, params=["service"])
def selective(service: UserService, manual: str): passGet detailed injection logging:
@injectable(debug=True)
def debug_function(service: Inject[UserService]):
pass
# Output:
# [BEVY DEBUG] Resolving <class 'UserService'> with options None
# [BEVY DEBUG] Injected service: <class 'UserService'> = <UserService object at 0x...>Hooks with detailed context:
from bevy.hooks import hooks
@hooks.INJECTION_REQUEST
def log_injection_request(container, context):
print(f"Injecting {context.requested_type.__name__} for {context.function_name}")
@hooks.POST_INJECTION_CALL
def log_execution_time(container, context):
print(f"Function {context.function_name} took {context.execution_time_ms:.2f}ms")Choose between strict and lenient error handling:
# Strict mode (default) - raises errors for missing dependencies
@injectable(strict=True)
def strict_func(service: Inject[UserService]): pass
# Non-strict mode - injects None for missing dependencies
@injectable(strict=False)
def lenient_func(service: Inject[UserService]):
if service is None:
# Handle gracefully
pass- Update imports to use new decorators and types
- Replace
@injectwith@injectableor@auto_inject+@injectable - Replace
dependency()defaults withInject[T]annotations - Update factory usage to use
Options(default_factory=...) - Register
type_factoryhook for automatic type creation - Update class constructors to use injection system
- Test container setup and dependency resolution
- Update any custom hooks to use hook types
- Consider using optional dependency features
- Enable debug mode during migration for troubleshooting
1. "Missing dependencies" errors
- Ensure
type_factoryhook is registered for automatic creation - Or add explicit factories/instances to containers
2. "Type errors" with IDE
- Make sure you're using
Inject[T]annotations correctly - Check that type imports are available in the function's namespace
3. "@auto_inject requires @injectable" errors
- Ensure decorator order:
@auto_injectcomes before@injectable
4. "Circular import" issues
- Use factory functions or lazy initialization
- Consider restructuring module dependencies
- Enable debug mode: Add
debug=Trueto@injectabledecorators - Check container state: Use
container.instancesto see what's registered - Test with minimal setup: Start with
type_factoryhook for simplicity - Use container branching: Isolate test scenarios with
container.branch()
The system generally performs better due to:
- Cached analysis: Function signatures are analyzed once and cached
- Optimized type checking: Faster type resolution with modern algorithms
- Reduced overhead: Less dynamic inspection at runtime
However, be aware that:
- First call overhead: Initial function analysis has some cost
- Memory usage: Cached analysis uses slightly more memory
- Debug mode cost: Debug logging has performance impact
While the migration requires updating your decorators and type annotations, the system provides:
- Better type safety with full IDE support
- More powerful hooks for extensibility
- Flexible injection strategies for different scenarios
- Native optional dependency support
- Improved debugging capabilities
The effort to migrate pays off with a more robust, type-safe, and feature-rich dependency injection system.