Hierarchical parsing

Hierarchical parsing allows complex class types to be expaded into individual parameters. For each property of a class type a parameter is generated with name being the path to the parameter. This is illustrated in the following example:

@dataclass
class Model:
    '''
    :param learning_rate: Learning rate
    :param num_features: Number of features
    '''
    learning_rate: float = 1e-4
    num_features: int = 5


@aclick.command
def train(model: Model, num_epochs: int):
    print(f'''lr: {learning_rate},
num_features: {model.num_features},
num_epochs: {num_epochs}''')

The corresponding help page looks as follows:

$ python train.py --help
Usage: train.py [OPTIONS]

Options:
  --help                        Show this message and exit.
  --model-learning-rate FLOAT   Learning rate  [default: 0.0001]
  --model-num-features INTEGER  Number of features  [default: 5]
  --num-epochs INTEGER          [required]

Note, that in some cases hierarchical parsing may not support your type, default value, etc. In particular, positional arguments cannot be expanded in hierarchical parsing. If your type in not supported, you can either inline it by wrapping it with aclick.default_from_str() wrapper or turn of hierarchical parsing by passing hierarchical = False to the command constructor.

With a deep class structure the parameter names can grow long. Parameters can be renamed to prevent long names. For more information see hierarchical parsing and FlattenParameterRenamer.

Custom classes

User defined classes are naturally expand to individual parameters if hierarchical = True (default). We can have a class hierarchy and parameters will be expanded to the lowest level. This can be seen in the following example:

class Schedule:
    def __init__(self, type: str, constant: float = 1e-4):
        self.type = type
        self.constant = constant

@dataclass
class Model:
    '''
    :param learning_rate: Learning rate
    :param num_features: Number of features
    '''
    learning_rate: Schedule
    num_features: int = 5


@aclick.command
def train(model: Model, num_epochs: int):
    pass

The corresponding help page looks as follows:

$ python train.py --help
Usage: train.py [OPTIONS]

Options:
  --help                          Show this message and exit.
  --model-learning-rate-type TEXT
                                  [required]
  --model-learning-rate-constant FLOAT
                                  [default: 0.0001]
  --model-num-features INTEGER    Number of features  [default: 5]
  --num-epochs INTEGER            [required]

Optional values

If a property of a class type is optional, there will be a boolean flag with the property name indicating whether the class is actually present. This is illustrated in the following example:

class Schedule:
    def __init__(self, type: str, constant: float = 1e-4):
        self.type = type
        self.constant = constant

@dataclass
class Model:
    '''
    :param learning_rate: Learning rate
    :param num_features: Number of features
    '''
    learning_rate: t.Optional[Schedule] = None
    num_features: int = 5


@aclick.command
def train(model: Model, num_epochs: int):
    pass

The corresponding help page looks as follows:

$ python train.py --help
Usage: train.py [OPTIONS]

Options:
  --help                        Show this message and exit.
  --model-learning-rate         Set model.learning_rate to a schedule instance
  --model-num-features INTEGER  Number of features  [default: 5]
  --num-epochs INTEGER          [required]

And after specifying that we want to instantiate the learning_rate instance:

$ python train.py --model-learning-rate --help
Usage: train.py [OPTIONS]

Options:
  --help                          Show this message and exit.
  --model-learning-rate           Set model.learning_rate to a schedule instance
  --model-learning-rate-type TEXT
                                  [required]
  --model-learning-rate-constant FLOAT
                                  [default: 0.0001]
  --model-num-features INTEGER    Number of features  [default: 5]
  --num-epochs INTEGER            [required]

Union of classes

We can also specify multiple types for a parameter or property and the concrete type will be specified when invoking the command. This scenario is illustrated in the following example:

@dataclass
class ModelA:
    '''
    :param learning_rate: Learning rate
    :param num_features: Number of features
    '''
    learning_rate: float = 0.1
    num_features: int = 5

@dataclass
class ModelB:
    '''
    :param learning_rate: Learning rate
    :param num_layers: Number of layers
    '''
    learning_rate: float = 0.2
    num_layers: int = 10


@aclick.command
def train(model: t.Union[ModelA, ModelB], num_epochs: int):
    pass

The corresponding help page looks as follows:

$ python train.py --help
Usage: train.py [OPTIONS]

Options:
  --help                     Show this message and exit.
  --model [model-a|model-b]  Set model to a type instance  [required]
  --num-epochs INTEGER       [required]

And after specifying that we want to use ModelB class:

$ python train.py --model model-b --help
Usage: train.py [OPTIONS]

Options:
  --help                       Show this message and exit.
  --model [model-a|model-b]    Set model to a type instance  [required]
  --model-learning-rate FLOAT  Learning rate  [default: 0.2]
  --model-num-layers INTEGER   Number of layers  [default: 10]
  --num-epochs INTEGER         [required]