<jupyter_start><jupyter_text>Autocfg BasicsThis notebook will demonstrate the basic functionalities of autocfg<jupyter_code>from autocfg import dataclass, field  # drop-in replacement of dataclass decorator out of dataclasses<jupyter_output><empty_output><jupyter_text>dataclass decoratorThe usage of dataclass decorator shouldn't be anything different than the native `dataclasses` introduced in python 3.7. In python 3.6 we use the backported `dataclasses` so it minimum requirement of `autocfg` package is python 3.6Let's create some random configurations you will use in an experiment.<jupyter_code>from typing import Union
# first is the common training config
@dataclass
class TrainConfig:
  batch_size : int = 32
  learning_rate : Union[float, int] = 1e-3
  weight_decay : float = 1e-5<jupyter_output><empty_output><jupyter_text>followed by a nested config<jupyter_code># supports nested config
@dataclass
class MyExp:
  train : TrainConfig = TrainConfig() # or field(default_factory=TrainConfig)
  num_class : int = 1000
  depth : int = 50<jupyter_output><empty_output><jupyter_text>Note that usually a mutable object such as `TrainConfig` is not allowed in dataclass, but we have made it simpler to specify default value for nested config. A former `field(default_factory=TrainConfig)` is recommended.  Initialize, and direct access<jupyter_code># we can initialize the plain configs as-is
train = TrainConfig()
train1 = TrainConfig(batch_size=128)
print('train:', train)
print('train1:', train1)
# the exp config, a nested class
exp = MyExp(depth=18)
print('exp with default train:', exp)
# config can be viewed as normal dict
print('dict:', exp.asdict())
# To modify the values, attributes can be directly accessed
exp.num_class = 10
exp.train.learning_rate = 1.5
tt = TrainConfig()
tt.learning_rate = 1000
print('updated exp:', exp)<jupyter_output>updated exp: MyExp(train=TrainConfig(batch_size=32, learning_rate=1.5, weight_decay=1e-05), num_class=10, depth=18)<jupyter_text>Type checkingThe type validation is a built-in feature for each field, for example, an invalid `learning_rate` will triger `TypeError`<jupyter_code>try:
    invalid_train = TrainConfig(learning_rate='1.0')
except TypeError as e:
    print('raised error:', e)<jupyter_output>raised error: `<class '__main__.TrainConfig'>.learning_rate` requires typing.Union[float, int], given <class 'str'>:1.0<jupyter_text>More importantly, `autocfg` also safe-guard the `setattr` behavior so you won't assign invalid values to fields<jupyter_code>invalid_train = TrainConfig(learning_rate=1.0)
try:
    invalid_train.batch_size = 0.1
except TypeError as e:
    print('raised error:', e)<jupyter_output>raised error: `<class '__main__.TrainConfig'>.batch_size` requires <class 'int'>, given <class 'float'>:0.1<jupyter_text>Serialization`autocfg` prefers `yaml` as the human-readable format for serialization, which can be viewed and modified pretty effortlessly.<jupyter_code># save to 'exp.yaml'
exp.save('exp.yaml')
!cat exp.yaml
# directly load from file is also straight-forward
exp1 = MyExp.load('exp.yaml')
assert exp == exp1
# a python file-like object can be handy in case in-memory operation is preferred
import io
f = io.StringIO('depth: 1000')
exp2 = MyExp.load(f)
print(exp2)
assert exp2.depth == 1000<jupyter_output>MyExp(train=TrainConfig(batch_size=32, learning_rate=1.5, weight_decay=1e-05), num_class=1000, depth=1000)<jupyter_text>Update config in-placeThough configs can be updated by direct access and assignment, we also need a faster `update` method similar to nested dict<jupyter_code>exp2.update(exp1)
print(exp2)
assert exp2 == exp1
# update support files, file-like objects where configs has been dumped
exp2 = MyExp(num_class=200)
exp2.update('exp.yaml')
print(exp2)
assert exp2 == exp
# update with a dict
exp2.update({'num_class': 10, 'train': {'learning_rate': 1.0}})
print(exp2)
assert exp2.num_class == 10 and exp2.train.learning_rate == 1.0
# update a subsection only, specified by `key=xxx` , e.g., `train` only
exp2 = MyExp(num_class=200)
exp2.update(exp, key='train')
print('only update exp2.train', exp2)
assert exp2.num_class == 200 and exp2.train.learning_rate == exp.train.learning_rate
# supports multiple keys
exp2.update(exp, key=('train', 'num_class'))
# update with kwargs with key=value pairs
exp2 = MyExp(num_class=200)
exp2.update(num_class=100, depth=100)
print(exp2)
# useful in a nested config
exp2.update(train={'batch_size': 4})
# or
exp2.train.update(batch_size=4, learning_rate=1.0)
print(exp2)<jupyter_output>MyExp(train=TrainConfig(batch_size=32, learning_rate=1.0, weight_decay=1e-05), num_class=100, depth=100)
MyExp(train=TrainConfig(batch_size=4, learning_rate=1.0, weight_decay=1e-05), num_class=100, depth=100)<jupyter_text>Add new key or allow type changeType checking is a very useful feature to avoid inaccidental modification to sensitive parameters which will very likely cause a crashed program. By default, configs will also reject new attributes to keep it safe.You can, however, modify the configs on-the-fly with new keys or change the types of values inside.<jupyter_code># add new key
exp = MyExp(num_class=10, train=TrainConfig(learning_rate=1.0))
try:
    exp.update({'new_k': 'new_value'}, allow_new_key=False)
except KeyError as e:
    print(f'{e}')
exp.update({'new_k': 'new_value'}, allow_new_key=True)
print('new_k value:', exp.new_k)

# change type
try:
    exp.update(new_k=100, allow_type_change=False)
except TypeError as e:
    print(f'{e}')
exp.update(new_k=100, allow_type_change=True)
print('updated new_k:', exp.new_k, type(exp.new_k))<jupyter_output>'new_k is not a valid key in MyExp(train=TrainConfig(batch_size=32, learning_rate=1.0, weight_decay=1e-05), num_class=10, depth=50), as `allow_new_key` is False'
new_k value: new_value
type not matching <class 'str'> vs <class 'int'>, as `allow_type_change` is False
updated new_k: 100 <class 'int'><jupyter_text>Note that the forced type change is temporary, the original type annotation is still valid<jupyter_code>try:
    exp.new_k = 1000
except TypeError as e:
    print(f'It is still enforced to have type check: {e}')<jupyter_output>It is still enforced to have type check: `<class 'types.MyExp'>.new_k` requires <class 'str'>, given <class 'int'>:1000<jupyter_text>Make copy or merge configsMaking copies or merge configs to another object without modifying the original configuration. In such cases, you can use the built-in `copy` module or `merge` method<jupyter_code>import copy
orig_cfg = MyExp()
new_cfg = copy.deepcopy(orig_cfg)
new_cfg.train.learning_rate = 0.5
print(orig_cfg, '\nversus...\n', new_cfg)
# merge a yaml file directly
new_cfg = orig_cfg.merge('exp.yaml')
print(orig_cfg, '\nversus...\n', new_cfg)
# merge with a dict
new_cfg = orig_cfg.merge({'train': {'learning_rate': 1.5}})
print(new_cfg)
# merge from another config object
new_cfg = orig_cfg.merge(exp2)
print(new_cfg)<jupyter_output>MyExp(train=TrainConfig(batch_size=4, learning_rate=1.0, weight_decay=1e-05), num_class=1000, depth=50) 
versus...
 MyExp(train=TrainConfig(batch_size=32, learning_rate=1.5, weight_decay=1e-05), num_class=10, depth=18)
MyExp(train=TrainConfig(batch_size=4, learning_rate=1.5, weight_decay=1e-05), num_class=1000, depth=50)
MyExp(train=TrainConfig(batch_size=4, learning_rate=1.0, weight_decay=1e-05), num_class=100, depth=100)<jupyter_text>Anyway, using `copy` or `merge`, we can make the original config intact while quickly update the desired fields. In paticular, `merge` support all updating method we have shown with `update` function.  Argparse integrationIt's always time consuming if you need to manually add a argparse parser to handle command line inputs, `autocfg` handles the issue out of the box, saving tons of efforts for you.Since jupyter notebook doesnot play with `sys.argv` well, we will use list inputs to simulate command line inputs.<jupyter_code># auto generated helper
try:
    new_exp = MyExp.parse_args(['-h'])
except SystemExit as e:
    pass<jupyter_output>usage: MyExp's auto argument parser [-h] [--train.batch-size TRAIN.BATCH_SIZE]
                                    [--train.learning-rate TRAIN.LEARNING_RATE]
                                    [--train.weight-decay TRAIN.WEIGHT_DECAY]
                                    [--num-class NUM_CLASS] [--depth DEPTH]

optional arguments:
  -h, --help            show this help message and exit
  --train.batch-size TRAIN.BATCH_SIZE
                        batch_size (default: 32)
  --train.learning-rate TRAIN.LEARNING_RATE
                        learning_rate (default: 0.001)
  --train.weight-decay TRAIN.WEIGHT_DECAY
                        weight_decay (default: 1e-05)
  --num-class NUM_CLASS
                        num_class (default: 1000)
  --depth DEPTH         depth (default: 50)<jupyter_text>The default values are also available in the console output<jupyter_code># normal overriding
new_exp = MyExp.parse_args(['--depth', '100'])
print(new_exp)
# nested overriding
new_exp = MyExp.parse_args(['--train.weight-decay', '100.0'])
print(new_exp)<jupyter_output>MyExp(train=TrainConfig(batch_size=32, learning_rate=0.001, weight_decay=100.0), num_class=1000, depth=50)<jupyter_text>Diff configurationsKnowing what's been modified is an important feature of configuration systems, `autocfg` provide a `diff` function to evaluate the changes<jupyter_code>from pprint import pprint
pprint(new_exp.diff(exp2))<jupyter_output>['root.depth           50 != 100',
 'root.num_class       1000 != 100',
 'root.train.batch_size 32 != 4',
 'root.train.learning_rate 0.001 != 1.0',
 'root.train.weight_decay 100.0 != 1e-05']<jupyter_text>Frozen config`autocfg` provides `freeze()` method to emulates read-only frozen configurations.<jupyter_code>exp = MyExp(num_class=10, train=TrainConfig(learning_rate=1.0))
# change value is just fine
exp.num_class = 20
# call freeze to emulate immutable object
exp.freeze()
from autocfg import FrozenInstanceError
try:
    exp.num_class = 30
except FrozenInstanceError as e:
    print(e)
# unfreeze
exp.unfreeze()
exp.num_class = 30<jupyter_output><empty_output><jupyter_text>Work with fields when type check is not requiredBy default, python dataclasses ignores all fields with no type annotation, for example<jupyter_code>from dataclasses import dataclass as _dataclass  # built-in

@_dataclass
class Plain:
    a = 1
    b = '2'
    c = (1, 2, 3)
p0 = Plain()
print(p0)

from dataclasses import asdict
# You can access the attributes, but won't be able to use some features out of dataclasses
print('You can access attribute Plain.a: ', p0.a, '\nHowever, asdict() is empty:', asdict(p0))<jupyter_output>Plain()
You can access attribute Plain.a:  1 
However, asdict() is empty: {}<jupyter_text>Since there's no field with type annotation, the default dataclass won't even catch the attributes.It is not that convenient to specify all fields with type annotation, especially sometimes we don't need type checking.With `autocfg`, the plain attributes without type annotation will be automatically catched, and treated as `typing.Any` type<jupyter_code>@dataclass
class Plain:
    a = 1
    b = '2'
    c = (1, 2, 3)
p1 = Plain()
print(p1)
print('asdict:', p1.asdict())<jupyter_output>Plain(a=1, b=2, c=(1, 2, 3))
asdict: {'a': 1, 'b': '2', 'c': (1, 2, 3)}<jupyter_text>Now the `Plain` object is a short version of:<jupyter_code>from typing import Any
@dataclass
class PlainAnnotated:
    a : Any = 1
    b : Any = '2'
    c : Any = (1, 2, 3)
p1 = PlainAnnotated()
print(p1)
print('asdict:', p1.asdict())<jupyter_output>PlainAnnotated(a=1, b=2, c=(1, 2, 3))
asdict: {'a': 1, 'b': '2', 'c': (1, 2, 3)}
{'fraction_non_alphanumeric': 0.11242705115001717, 'fraction_numerical': 0.031840027463096464, 'mean_word_length': 5.278556034482759, 'pattern_counts': {'":': 0, '<': 70, '<?xml version=': 0, '>': 70, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '245713', 'license': 'Apache-2.0', 'n_tokens_mistral': 4146, 'n_tokens_neox': 3855, 'n_words': 1164, 'path': 'examples/basics.ipynb', 'repo_name': 'sxjscience/autocfg', 'size': '11652'}