Tutorial: 02 - Yield Dependencies¶
Some resources, like files or network connections, need to be properly closed or released after use. Picodi allows you to manage the setup and teardown of such dependencies using generator functions with a single yield.
The Need for Setup and Teardown¶
Imagine a dependency that provides a temporary file for writing data. This file needs to be created before use and deleted afterward.
A simple return statement isn’t enough because we need to
execute cleanup code after the dependency has been used by the function that injected it.
Using yield for Lifecycle Management¶
Picodi leverages Python’s generators for this.
If a dependency function is a generator that yields exactly once,
Picodi treats it like a context manager (similar to those created with contextlib.contextmanager()).
The code before the
yieldis executed during the setup phase (when the dependency is first needed).The value yielded is the actual dependency instance provided to the injecting function.
The code after the
yieldis executed during the teardown phase (after the function that injected the dependency finishes).
Let’s modify our example to use a temporary file managed by a yield dependency.
# dependencies.py
import tempfile
import os
# We don't need @inject or Provide here for get_temp_file_path
# because it doesn't depend on any other dependencies.
def get_temp_file_path():
"""Provides a path to a temporary file and cleans it up afterwards."""
tf = tempfile.NamedTemporaryFile(delete=False, mode="w+", suffix=".txt")
file_path = tf.name
print("Setup: Created temp file")
tf.close() # Close the file handle, but the file remains
try:
yield file_path # Provide the path
finally:
# Teardown: This code runs after the injecting function finishes
if os.path.exists(file_path):
os.remove(file_path)
print("Teardown: Removed temp file")
else:
print(
"Teardown: Temp file already removed"
) # Should not happen in normal flow
# services.py
from picodi import Provide, inject
# from dependencies import get_temp_file_path
@inject
def write_to_temp_file(
content: str,
temp_file: str = Provide(get_temp_file_path), # Inject the yielded path
) -> None:
"""Writes content to a temporary file provided by a dependency."""
print("Service: Writing to temp_file")
with open(temp_file, "a") as f:
f.write(content + "\n")
print("Service: Finished writing to temp_file")
# main.py
# from services import write_to_temp_file
print("Main: Calling service the first time.")
write_to_temp_file("Hello from Picodi!")
print("Main: Service call finished.")
print("\nMain: Calling service the second time.")
write_to_temp_file("Another message.")
print("Main: Service call finished.")
Explanation:
get_temp_file_path: This function now uses
yield. It creates a temporary file, yields its path, and then removes the file in afinallyblock. Thecontextlib.contextmanager()decorator is used here for clarity and standard practice, although Picodi only requires the singleyieldstructure.Injection:
write_to_temp_fileinjects the yielded value (the file path string) fromget_temp_file_path.Execution Flow: When
write_to_temp_fileis called:Picodi calls
get_temp_file_path.The code before
yieldruns (file created).The file path is yielded and injected into
write_to_temp_file.The body of
write_to_temp_fileexecutes (writing to the file).After
write_to_temp_filefinishes, Picodi resumes theget_temp_file_pathgenerator.The code after
yield(in thefinallyblock) runs (file removed).
Output:
Main: Calling service the first time.
Setup: Created temp file
Service: Writing to temp_file
Service: Finished writing to temp_file
Teardown: Removed temp file
Main: Service call finished.
Main: Calling service the second time.
Setup: Created temp file
Service: Writing to temp_file
Service: Finished writing to temp_file
Teardown: Removed temp file
Main: Service call finished.
As you can see, the setup code runs before the service function, and the teardown code runs after it finishes,
ensuring the resource is managed correctly.
A new temporary file is created and destroyed for each call because we are still using the default NullScope.
Next Steps¶
Now that you know how to manage dependency lifecycles with yield,
let’s explore how to control how often dependencies are created using Scopes.