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 yield is 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 yield is 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:

  1. get_temp_file_path: This function now uses yield. It creates a temporary file, yields its path, and then removes the file in a finally block. The contextlib.contextmanager() decorator is used here for clarity and standard practice, although Picodi only requires the single yield structure.

  2. Injection: write_to_temp_file injects the yielded value (the file path string) from get_temp_file_path.

  3. Execution Flow: When write_to_temp_file is called:

    • Picodi calls get_temp_file_path.

    • The code before yield runs (file created).

    • The file path is yielded and injected into write_to_temp_file.

    • The body of write_to_temp_file executes (writing to the file).

    • After write_to_temp_file finishes, Picodi resumes the get_temp_file_path generator.

    • The code after yield (in the finally block) 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.