Skip to main content

Evolve

spock provides evolve functionality similar to the underlying attrs library (attrs.evolve. evolve() creates a new Spockspace instance based on differences between the underlying declared state and any passed in instantiated @spock decorated classes.

Using Evolve#

The evolve() method is available form the SpockBuilder object. evolve() takes as input a variable number of instantiated @spock decorated classes, evolves the underlying attrs objects to incorporate the changes between the instantiated classes and the underlying classes, and returns the new Spockspace object.

For instance:


from enum import Enumfrom spock import spockfrom spock import SpockBuilder

class Choices(Enum):    choice1 = 1    choice2 = 2

@spockclass Configs4OneThing:    the_choice: Choices = Choices.choice1    param: int = 10

def main():    evolve_class = Configs4OneThing(param=20)    evolved_configs = SpockBuilder(Configs4OneThing, desc='Evolve Example').evolve(evolve_class)    print(evolved_configs)    if __name__ == '__main__':    main()

This would evolve the value of param to be 20 instead of the default value of 10. The print output would be:

Configs4OneThing: !!python/object:spock.backend.config.Configs4OneThing  param: 20  the_choice: 1

Maintaining CLI and Python API Configuration Parity#

evolve is quite useful when writing python code/libraries/packages that maintain both a CLI and a Python API. With spock it is simple to maintain parity between the CLI and the Python API by leveraging the evolve functionality.

For instance, let's say we have two different @spock decorated configs we want to use for both the CLI and the Python API:

# config.py
from enum import Enumfrom spock import spockfrom typing import List

class Choices(Enum):    choice1 = 1    choice2 = 2

@spockclass Configs4OneThing:    the_choice: Choices = Choices.choice1    param: int = 10

@spockclass Configs4AnotherThing:    some_list: List[float] = [10.0, 20.0]    flag: bool = False
    # List of all configsALL_CONFIGS = [    Configs4OneThing,    Configs4AnotherThing]

With these @spock decorated classes it's easy to write a parent class that contains shared functionality (i.e. run a model, do some work, etc.) and two child classes that handle the slightly different syntax needed for the underlying SpockBuilder for the CLI and for the Python API.

For the CLI, we use the common spock syntax that has been shown in previous examples/tutorial. Call the builder object and pass in all @spock decorated classes. Keep the no_cmd_line flag set to False which will automatically generate a command line argument for each defined parameter and provide support for the --config argument to pass in values via a markdown file(s). We then call generate on the builder to return the Spockspace.

For the Python API, we modify the spock syntax slightly. We still pass in all @spock decorated classes but set the no_cmd_line flag to True to prevent command line arguments (and markdown configuration). We then call evolve and pass in any user instantiated @spock decorated classes to evolve the underlying object and return a new Spockspace object that has been evolved based on the differences between the values within instantiated classes and the values in the underlying object.

Example code is given below:

# code.pyfrom abc import ABCfrom spock import SpockBuilderfrom config import ALL_CONFIGS

class Base(ABC):    def run(self):        # do something with self.configs        ...
    class OurAPI(Base):    def __init__(self,                 config_4_one_thing: Configs4OneThing = Configs4OneThing(),                  config_4_another_thing: Configs4AnotherThing = Configs4AnotherThing()                 ):        # Call the SpockBuilder with the no_cmd_line flag set to True         # This will prevent command-line arguments from being generated        # Additionally call evolve on the builder with the custom/default Configs4OneThing & Configs4AnotherThing        # objects        self.configs = SpockBuilder(*ALL_CONFIGS, no_cmd_line=True, configs=[]).evolve(            config_4_one_thing, config_4_another_thing        )

class OurCLI(Base):    def __init__(self):        # Call the SpockBuilder with the no_cmd_line flag set to False (default value)        # This will automatically provide command-line arguments for all of the @spock decorated        # config classes        self.configs = SpockBuilder(*ALL_CONFIGS).generate()    
def cli_shim():    """Shim function for setup.py entry_points
    Returns:        None    """    cli_runner = OurCLI().run()