Prerequisites

This tutorial is a direct follow-on to: Provide Swappable Configuration Groups.

Inject Novel Functionality via the Application’s Configurable Interface#

In this tutorial we will add new functionality into our application without modifying either our library’s code or our task function.

Let’s suppose that the Halloween holiday is approaching, and we want to surprise our players with a treat: all of their costumes will display as “spooky” renditions during this season 🎃. We can use the “zen-wrappers” feature of builds to “inject” these updated costumes into the game, simply by modifying our application’s config.

Modifying Our Application#

We will need to make two modifications to our code in my_app.py:

  1. Define a wrapper that will modify the player’s costume so that it is a “spooky” version of itself.

  2. Include the wrapper in the player’s config.

Creating the Functionality-Injecting Wrapper#

Let’s create a wrapper that will take in our game_library.Character object, instantiate it, and then update its costume.

We will incorporate the following code in my_app.py.

Defining the wrapper#
def halloween_update(CharClass):
    def wrapper(*args, **kwargs):
        # instantiate the Character
        char = CharClass(*args, **kwargs)

        costume = char.inventory["costume"]
        if costume:
            # update the costume
            char.inventory["costume"] = f"spooky {costume}"
        return char
    return wrapper

To see this in action, let’s open a Python console (or Jupyter notebook) in the same directory as game_library.py and my_app.py, and run the following.

Attention

The game_library code used below comes from a script that we, the tutorial-readers, created ourselves. See Creating (Fake) Library Code for details.

Testing out the wrapper#
>>> from game_library import Character, inventory

>>> def halloween_update(CharClass):
...     def wrapper(*args, **kwargs):
...         # instantiate the Character
...         char = CharClass(*args, **kwargs)
...
...         costume = char.inventory["costume"]
...         if costume:
...             # update the costume
...             char.inventory["costume"] = f"spooky {costume}"
...         return char
...     return wrapper

>>> WrappedChar = halloween_update(Character)  # character's costume will be made spooky

We see that the wrapped version of game_library.Character automatically has its costume updated for the holiday. Verify that you see the following behaviors.

>>> WrappedChar(name="ness", inventory=inventory(gold=1, weapon="none", costume="shirt"))
ness, lvl: 1, has: {'gold': 1, 'weapon': 'none', 'costume': 'spooky shirt'}

>>> Character(name="ness", inventory=inventory(gold=1, weapon="none", costume="shirt"))
ness, lvl: 1, has: {'gold': 1, 'weapon': 'none', 'costume': 'shirt'}

Including the Wrapper in Our Config#

Incorporating this wrapper into our application simply involves specifying it as a “zen-wrapper” in our config for game_library.Character. I.e. we will update:

CharConf = builds(Character, ...)

to be

CharConf = builds(Character, ..., zen_wrappers=halloween_update)

Putting It All Together#

Let’s update the contents of my_app.py to reflect the changes that we just went over. Modify your my_app.py script to match the following code.

Contents of my_app.py#
from hydra_zen import store, make_custom_builds_fn, zen

from game_library import inventory, Character

builds = make_custom_builds_fn(populate_full_signature=True)


# 1. Added our wrapper
def halloween_update(CharClass):
    def wrapper(*args, **kwargs):
        # instantiate the Character
        char = CharClass(*args, **kwargs)

        costume = char.inventory["costume"]
        if costume:
            # update the costume
            char.inventory["costume"] = f"spooky {costume}"
        return char

    return wrapper


# Create inventory configs
InventoryConf = builds(inventory)
starter_gear = InventoryConf(gold=10, weapon="stick", costume="tunic")
advanced_gear = InventoryConf(gold=500, weapon="wand", costume="magic robe")
hard_mode_gear = InventoryConf(gold=0, weapon="inner thoughts", costume="rags")

# Register inventory configs under group: player/inventory
inv_store = store(group="player/inventory")

inv_store(starter_gear, name="starter")
inv_store(advanced_gear, name="advanced")
inv_store(hard_mode_gear, name="hard_mode")

# 2. Included the wrapper in our config for `Character`
CharConf = builds(Character, inventory=starter_gear, zen_wrappers=halloween_update)

brinda_conf = CharConf(
    name="brinda",
    level=47,
    inventory=InventoryConf(costume="cape", weapon="flute", gold=52),
)

rakesh_conf = CharConf(
    name="rakesh",
    level=300,
    inventory=InventoryConf(costume="PJs", weapon="pillow", gold=41),
)

# Register player-profile configs under group: player
player_store = store(group="player")

player_store(CharConf, name="base")
player_store(brinda_conf, name="brinda")
player_store(rakesh_conf, name="rakesh")


# The `hydra_defaults` field is specified in our task function's config.
# It instructs Hydra to use the player config that named 'base' in our
# config store as the default config for our app.
@store(name="my_app",  hydra_defaults=["_self_", {"player": "base"}])
def task_function(player: Character):

    print(player)

    with open("player_log.txt", "a") as f:
        f.write("Game session log:\n")
        f.write(f"Player: {player}\n")

    return player


if __name__ == "__main__":
    # We need to add the configs from our local store to Hydra's
    # global config store
    store.add_to_hydra_store()

    # Our zen-wrapped task function is used to generate
    # the CLI, and to specify which config we want to use
    # to configure the app by default
    zen(task_function).hydra_main(config_name="my_app",
                                  version_base="1.1",
                                  config_path=".",
                                  )

Running Our Application#

We can configure and launch our application exactly as we had before, but now all of the player’s costumes will automatically become 👻 spooky 👻.

Open your terminal in the directory shared by both my_app.py and game_library.py and run the following commands. Verify that you can reproduce the behavior shown below.

Base character.#
$ python my_app.py player.name=ivy
ivy, lvl: 1, has: {'gold': 10, 'weapon': 'stick', 'costume': 'spooky tunic'}
Manually-specified costume.#
$ python my_app.py player.name=ivy player.inventory.costume=crown
ivy, lvl: 1, has: {'gold': 10, 'weapon': 'stick', 'costume': 'spooky crown'}
Load Rakesh’s player-profile#
$ python my_app.py player=rakesh
rakesh, lvl: 300, has: {'gold': 41, 'weapon': 'pillow', 'costume': 'spooky PJs'}

Inspecting the Results#

Hydra will document our use of halloween_update() in the config.yaml for our job. To inspect the config for our most-recent job, let’s open a Python terminal in the same directory as my_app.py and run the following code

>>> from pathlib import Path
>>> def print_file(x: Path):
...     with x.open("r") as f:
...         print(f.read())

>>> *_, latest_job = sorted((Path.cwd() / "outputs").glob("*/*"))

>>> print_file(latest_job / ".hydra" / "config.yaml")
player:
  _target_: hydra_zen.funcs.zen_processing
  _zen_target: game_library.Character
  _zen_wrappers: __main__.halloween_update
  name: rakesh
  level: 300
  inventory:
    _target_: game_library.inventory
    gold: 41
    weapon: pillow
    costume: PJs

From this YAML config file, we can see explicitly that our application launched using the Halloween update; the update will also take effect if we were to re-launch our application using this particular YAML file to reproduce the job.

Outstanding! We successfully leveraged the zen-wrappers feature of builds() to modify the behavior of our application, without touching our library’s source code. And we did so in a self-documenting, and reproducible manner.

Although this achievement might not seem all that impressive in the context of this toy example, it should be emphasized that zen-wrappers can be used to inject arbitrary pre-processing, post-processing, and transformations into the config-instantiation process. For example, hydra-zen provides enhanced data-validation capabilities via zen-wrappers. Based on this tutorial, we hope that you feel emboldened to design and use zen-wrappers in your workflow!

Reference Documentation#

Want a deeper understanding of how hydra-zen and Hydra work? The following reference materials are especially relevant to this tutorial section.

Attention

Cleaning Up: To clean up after this tutorial, delete the outputs directory that Hydra created upon launching our application.