Basic Usage¶
This guide covers the fundamental usage patterns of apywire.
Creating a Wiring Container¶
The Wiring class is the main entry point for dependency injection:
from apywire import Wiring
spec = {
"datetime.datetime now": {"year": 2025, "month": 1, "day": 1},
}
wired = Wiring(spec)
By default, Wiring creates a non-thread-safe container suitable for single-threaded applications. For thread safety, see the Thread Safety guide.
Defining Specs¶
The spec dictionary maps wiring keys to configuration. There are two types of entries:
Wired Objects¶
Use the format "module.Class name" to define objects that should be lazily instantiated:
spec = {
"datetime.datetime start_time": {"year": 2025, "month": 1, "day": 1},
"pathlib.Path project_root": {0: "/home/user/project"},
"MyClass service": {"param1": "value1", "param2": 42},
}
Constants¶
Simple keys without the module.Class format become constants:
spec = {
"host": "localhost",
"port": 8080,
"debug": True,
"api_key": "secret-key-here",
}
wired = Wiring(spec)
# Constants are primarily used as placeholder references in other wired objects
Note
Constants are most useful as placeholder references for other wired objects using the {name} syntax.
Spec Value Types¶
apywire supports various parameter types:
Keyword Arguments (Dict)¶
Positional Arguments (List)¶
Positional Arguments (Numeric Dict Keys)¶
Mixed Arguments¶
You can combine positional and keyword arguments:
Numeric keys are sorted and passed as positional arguments, while string keys become keyword arguments.
Using Placeholders¶
Reference other wired objects or constants using {name} syntax:
spec = {
"database_url": "postgresql://localhost/mydb",
"max_connections": 10,
"psycopg2.pool.SimpleConnectionPool pool": {
"minconn": 1,
"maxconn": "{max_connections}",
"dsn": "{database_url}",
},
}
wired = Wiring(spec)
pool = wired.pool() # placeholders are resolved to actual values
Nested Placeholders¶
Placeholders work in nested structures:
spec = {
"api_key": "secret-key",
"MyClient client": {
"config": {
"auth": {
"api_key": "{api_key}", # Nested placeholder
},
"timeout": 30,
},
},
}
List Placeholders¶
Placeholders work in lists too:
spec = {
"datetime.datetime start": {"year": 2025, "month": 1, "day": 1},
"datetime.datetime end": {"year": 2025, "month": 12, "day": 31},
"MyReport report": {
"date_range": ["{start}", "{end}"], # List with placeholders
},
}
Accessing Wired Objects¶
The Accessor Pattern¶
When you access an attribute on a Wiring container, you get an Accessor:
from apywire import Wiring, Accessor
wired = Wiring(spec)
accessor = wired.my_object # Returns an Accessor instance
print(type(accessor)) # <class 'apywire.runtime.Accessor'>
Call the accessor to instantiate the object:
Most commonly, you'll do both in one line:
Caching Behavior¶
Objects are instantiated once and cached:
spec = {
"datetime.datetime now": {"year": 2025, "month": 1, "day": 1},
}
wired = Wiring(spec)
dt1 = wired.now()
dt2 = wired.now()
assert dt1 is dt2 # True - same object instance!
Each access returns the same instance, not a new one. This is crucial for:
- Maintaining singleton-like behavior
- Avoiding duplicate resource allocation (e.g., database connections)
- Ensuring consistent state across your application
When Instantiation Happens¶
wired = Wiring(spec)
# No objects instantiated yet!
accessor = wired.my_object
# Still nothing instantiated - just got the accessor
obj = accessor()
# NOW the object is created and cached
Working with Multiple Objects¶
spec = {
"datetime.datetime start": {"year": 2025, "month": 1, "day": 1},
"datetime.timedelta delta": {"days": 7},
"pathlib.Path root": ["/home/user"],
}
wired = Wiring(spec)
# Access multiple objects
start_date = wired.start()
time_delta = wired.delta()
root_path = wired.root()
Dependency Resolution Order¶
When accessing an object with placeholder dependencies, apywire resolves them in order:
spec = {
"MyDatabase db": {},
"MyCache cache": {"db": "{db}"}, # Depends on db
"MyService service": {
"cache": "{cache}", # Depends on cache
"db": "{db}", # Also depends on db
},
}
wired = Wiring(spec)
service = wired.service()
# Resolution order: db → cache → service
apywire automatically handles the dependency graph and instantiates objects in the correct order.
Error Handling¶
Unknown Placeholder¶
from apywire import Wiring, UnknownPlaceholderError
spec = {
"MyClass obj": {"dependency": "{nonexistent}"},
}
wired = Wiring(spec)
try:
obj = wired.obj()
except UnknownPlaceholderError as e:
print(f"Unknown placeholder: {e}")
Circular Dependencies¶
from apywire import Wiring, CircularWiringError
spec = {
"MyClass a": {"dep": "{b}"},
"MyClass b": {"dep": "{a}"},
}
wired = Wiring(spec)
try:
obj = wired.a()
except CircularWiringError as e:
print(f"Circular dependency: {e}")
Import Errors¶
spec = {
"nonexistent.module.Class obj": {},
}
wired = Wiring(spec)
try:
obj = wired.obj()
except ImportError as e:
print(f"Cannot import: {e}")
Best Practices¶
1. Use Descriptive Names¶
# Good
spec = {
"psycopg2.connect db_connection": {"dsn": "{database_url}"},
"MyRepository user_repository": {"db": "{db_connection}"},
}
# Avoid
spec = {
"psycopg2.connect conn": {"dsn": "{url}"},
"MyRepository repo": {"db": "{conn}"},
}
2. Group Related Objects¶
spec = {
# Database layer
"db_url": "postgresql://localhost/mydb",
"psycopg2.connect db": {"dsn": "{db_url}"},
# Cache layer
"redis_url": "redis://localhost",
"redis.Redis cache": {"url": "{redis_url}"},
# Service layer
"MyService service": {"db": "{db}", "cache": "{cache}"},
}
3. Validate Configuration Early¶
def validate_wiring(wired: Wiring) -> None:
"""Validate all wired objects can be instantiated."""
# Access all objects to trigger any configuration errors
_ = wired.database()
_ = wired.cache()
_ = wired.service()
# In your app startup
wired = Wiring(spec)
validate_wiring(wired) # Fail fast if configuration is wrong
4. Use Constants for Configuration¶
spec = {
# Configuration constants
"debug": True,
"log_level": "INFO",
"timeout": 30,
# Wired objects using configuration
"logging.Logger logger": {
"name": "myapp",
"level": "{log_level}",
},
"MyClient client": {
"timeout": "{timeout}",
"debug": "{debug}",
},
}
5. Keep Specs Testable¶
# production_spec.py
def get_production_spec():
return {
"psycopg2.connect db": {"dsn": "postgresql://prod/db"},
}
# test_spec.py
def get_test_spec():
return {
"unittest.mock.Mock db": {}, # Mock database for testing
}
# Usage
if os.getenv("TESTING"):
wired = Wiring(get_test_spec())
else:
wired = Wiring(get_production_spec())
Next Steps¶
- Async Support - Use
await wired.aio.name()for async access - Thread Safety - Enable thread-safe instantiation
- Compilation - Generate standalone code from your spec
- Advanced Features - Factory methods and complex patterns