Dependencies Explained¶
In Picodi, the concept of a “dependency” is intentionally simple: a dependency provider is typically a Python function (or any callable) that returns a value or yields it. This function usually takes no required arguments, allowing Picodi to call it automatically when needed.
This topic delves into the different ways you can define these dependency providers.
Simple Dependency Functions¶
The most basic form of a dependency provider is a function that directly returns a value.
Synchronous Example:
def get_database_url() -> str:
"""Returns the connection string for the database."""
return "postgresql://user:password@host:port/dbname"
def get_settings() -> dict:
"""Loads and returns application settings."""
# In a real app, this might load from a file or environment variables
return {"timeout": 30, "retries": 3}
Asynchronous Example:
Picodi also supports asynchronous dependency providers.
These are defined using the async def syntax.
import asyncio
async def get_external_api_key() -> str:
"""Fetches an API key from a secure vault (simulated)."""
print("Fetching API key...")
await asyncio.sleep(0.1) # Simulate I/O
return "secret-api-key-12345"
These functions are ready to be used with Provide()
within an inject()-decorated function.
Yield Dependencies (Resource Management)¶
Often, dependencies represent resources that need setup before use and cleanup afterward
(e.g., database connections, file handles, network clients).
Picodi handles this elegantly using generator functions with a single yield.
Picodi treats such generators like context managers:
Setup: Code before
yieldruns when the dependency is first requested.Value: The value yielded is injected into the dependent function.
Teardown: Code after
yield(ideally in afinallyblock) runs after the dependent function finishes execution (or when the dependency’s scope dictates cleanup).
Synchronous Example:
import sqlite3
def get_db_cursor():
"""Provides a database cursor and ensures the connection is closed."""
connection = sqlite3.connect(":memory:")
print("DB Connection Opened")
cursor = connection.cursor()
try:
yield cursor # Provide the cursor
finally:
connection.close()
print("DB Connection Closed")
Asynchronous Example:
import asyncio
class AsyncResource: # Example async resource
async def setup(self):
print("Async Resource Setup")
await asyncio.sleep(0.05)
return self
async def close(self):
print("Async Resource Closed")
await asyncio.sleep(0.05)
async def do_work(self):
print("Async Resource Working")
async def get_async_resource():
"""Provides an async resource with setup and teardown."""
resource = AsyncResource()
await resource.setup()
try:
yield resource
finally:
await resource.close()
These yield dependencies ensure resources are managed correctly within the scope of their usage. The exact timing of the teardown depends on the scope assigned to the dependency.
Dependencies Using Other Dependencies¶
Dependency provider functions can themselves use inject() and Provide()
to depend on other dependencies. Picodi automatically resolves the entire dependency graph.
from picodi import Provide, inject
def get_base_url() -> str:
return "https://config-service.com"
@inject # get_api_config depends on get_base_url
def get_api_config(url: str = Provide(get_base_url)) -> dict:
print(f"Fetching config from {url}")
# Simulate fetching config based on the URL
return {"key": "config-key", "timeout": 5}
# Another function can now depend on get_api_config
@inject
def use_config(config: dict = Provide(get_api_config)):
api_key = config["key"]
print(f"Using API key: {api_key}")
return api_key
use_config()
Output:
Fetching config from https://config-service.com
Using API key: config-key
Picodi ensures get_base_url is resolved first, its result is passed to get_api_config,
and then the result of get_api_config is available for injection elsewhere.
Injecting the Registry into a Dependency¶
In some advanced scenarios, a dependency provider might need access to the Picodi registry
object itself, for example, to dynamically resolve other dependencies or interact with scopes.
Picodi supports this by automatically injecting the registry object if a dependency provider
function declares a parameter named exactly registry without a default value.
from picodi import Provide, inject, registry as picodi_registry, Registry
def get_another_dependency() -> str:
return "another_value"
# This dependency provider needs the registry
def get_dynamic_dependency(registry: Registry) -> str:
# The 'registry' parameter will be automatically injected.
# Note: Type hint 'Registry' is for clarity; injection relies on the name.
print(f"Dynamic dependency received registry: {type(registry)}")
# Example: use the registry to resolve another dependency
# This is a simplified example; direct resolution like this inside a
# provider is rare but demonstrates access.
with registry.resolve(get_another_dependency) as resolved_value:
return f"dynamic_value_based_on_{resolved_value}"
@inject
def use_dynamic_dependency(dynamic_dep: str = Provide(get_dynamic_dependency)):
print(f"Service using: {dynamic_dep}")
use_dynamic_dependency()
Output:
Dynamic dependency received registry: <class 'picodi._registry.Registry'>
Service using: dynamic_value_based_on_another_value
Key points for injecting the registry:
The parameter must be named
registry.The parameter must not have a default value.
Type hints are ignored for this specific injection; only the name and lack of a default matter.
This feature provides flexibility for complex dependency creation logic but should be used judiciously.
Key Takeaways¶
A Picodi dependency provider is typically a zero-argument callable (often a function), unless it’s designed to receive the
registryobject.Use regular functions for simple value dependencies (sync or async).
Use generator functions with a single
yieldfor dependencies requiring setup/teardown (sync or async).Dependencies can depend on other dependencies using
@injectandProvide.
Next, let’s look at how these dependencies are actually provided to your code using Injection.