diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d42dd5 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +Can I simply decorate config class? Or also want/need to inherit? + +Do I want to assert so that class name isnt same as MasterConfig? + +Do I want to inherit dataclasses? Whats the benefit? Do I automatically get freeze, hash, eq etc? + + +Need to be able to load configs. How does it work with objects? + + +How do we print objects? +How do we print nested configs? +If we have a function, which then calls another function. Can we show source code for both? + + +Would like to be able to use dataclasses structure. Its possible now but the type hinting is needed, and its not enforced so just seems bad to use. + + +make it so __repr__ is ambigious + + +Should have a function to register the config object in the anyfig module which can later simply be imported anywhere. No more passing around configs + +Name suggestions: +Anyfig + + + +Do we want to let users be able to create several configs? The fire/argparse doesn't work then. Unless we hook into that and divides it... But not for v1 \ No newline at end of file diff --git a/anyfig/__init__.py b/anyfig/__init__.py new file mode 100644 index 0000000..63062cc --- /dev/null +++ b/anyfig/__init__.py @@ -0,0 +1,2 @@ +from .masterconfig import MasterConfig, save_config, load_config +from .setup import setup_config, parse_args, choose_config, get_available_configs, overwrite, config_class, print_source \ No newline at end of file diff --git a/anyfig/decorators.py b/anyfig/decorators.py new file mode 100644 index 0000000..e69de29 diff --git a/anyfig/masterconfig.py b/anyfig/masterconfig.py new file mode 100644 index 0000000..2f5a8f3 --- /dev/null +++ b/anyfig/masterconfig.py @@ -0,0 +1,70 @@ +from abc import ABC +import pprint +import inspect +from dataclasses import FrozenInstanceError + + +def load_config(path): + # TODO: Add checks so that object is config + print("Loading config") + with open(path, 'rb') as f: + return pickle.load(f) + + +def save_config(config_obj, path): + # TODO: Add checks so that object is config + print(f"Saving config @ {path}") + with open(path, 'wb') as f: + pickle.dump(config_obj, f, pickle.HIGHEST_PROTOCOL) + + with open(path + '.txt', 'w') as f: + f.write(str(config_obj)) + + +class MasterConfig(ABC): + def __init__(self): + self._frozen: bool = False + self.config_class = type(self).__name__ + + def frozen(self, freeze=True): + self._frozen = freeze + + def get_parameters(self): + return self.__dict__ + + def __str__(self): + str_ = "" + params = vars(self) + params.pop('_frozen') # Dont print frozen + for key, val in params.items(): + if hasattr(val, '__anyfig_print_source__'): + cls_str = val.__anyfig_print_source__() + s = f"'{key}':\n{cls_str}" + else: + s = pprint.pformat({key: val}) + + # Prettyprint adds some extra wings that I dont like + s = s.lstrip('{').rstrip('}').replace('\n ', '\n') + str_ += s + '\n' + + return str_ + + def __setattr__(self, name, value): + # Raise error if frozen unless we're trying to unfreeze the config + if hasattr(self, '_frozen'): + if name == '_frozen': + pass + elif self._frozen: + err_msg = (f"Cannot set attribute '{name}'. Config object is frozen. " + "Unfreeze the config for a mutable config object") + raise FrozenInstanceError(err_msg) + + # Check for reserved names + name_taken_msg = f"The attribute '{name}' can't be assigned to config '{type(self).__name__}' since it already has a method by that name" + + def assert_name(name, method_name): + assert name != method_name, name_taken_msg + + methods = inspect.getmembers(self, predicate=inspect.ismethod) + [assert_name(name, m[0]) for m in methods] + object.__setattr__(self, name, value) \ No newline at end of file diff --git a/anyfig/run.py b/anyfig/run.py new file mode 100644 index 0000000..38148bf --- /dev/null +++ b/anyfig/run.py @@ -0,0 +1,68 @@ +import anyfig + + +@print_source +class Noise(): + def __init__(self, x, y): + self.x = x + self.y = y + + +@print_source +class Flip(): + def __init__(self, x, y): + self.x = x + self.y = y + self.noise3 = Noise(1, 2) + + +@print_source +class Transformer(): + def __init__(self, x): + # self.noise1 = Noise(x, x) + # self.flip = Flip(4, 0) + # self.noise2 = Noise(4, 0) + self.primitive = x + + +# @config_class +# class MainConfig(MasterConfig): +# def __init__(self): +# print("MAIN CONFIG SUPER") +# self.start_time = time.time() +# self.img_size = 100 +# self.classes = ['car', 'dog'] +# self.freeze_config = False + + +@config_class +class Train(MasterConfig): + def __init__(self): + super().__init__() + self.name = 'oldname' + self.transforms111 = Transformer(100) + # self.freeze_config = False + # self.frozen = 123 + + +def main(): + # config = setup_config() + config = setup_config(default_config='Train') + parameters = config.get_parameters() + print(parameters) + # params = str(config) + params = config.__dict__ + print(params) + qwe + + # config.frozen() + # config.frozen(freeze=False) + config.name = '123123' + + print(config) + # print(config.name) + + +if __name__ == '__main__': + print("BEFORE MAIN") + main() diff --git a/anyfig/setup.py b/anyfig/setup.py new file mode 100644 index 0000000..56555b7 --- /dev/null +++ b/anyfig/setup.py @@ -0,0 +1,116 @@ +from io import StringIO +import fire +import inspect +import argparse +import sys +from .masterconfig import MasterConfig + + +def setup_config(default_config=None): # TODO: Handle None + config_str = parse_args(default_config) + config = choose_config(config_str) + return config + + +def parse_args(default_config): + p = argparse.ArgumentParser() + + p.add_argument('--config', + type=str, + default=default_config, + help='What config class to choose') + + args, _ = p.parse_known_args() + return args.config + + +def choose_config(config_str): + # Create config object + available_configs = get_available_configs() + try: + config_class_ = available_configs[config_str] + config_obj = config_class_() + except KeyError as e: + err_msg = f"Config class '{config_str}' wasn't found. Feel free to create it as a new config class or use one of the existing ones marked as '@config_class' -> {set(available_configs)}" + raise KeyError(err_msg) from e + + # Overwrite parameters via optional input flags + config_obj = overwrite(config_obj) + + # Freezes config + config_obj.frozen(freeze=True) + return config_obj + + +def get_available_configs(): + available_configs = {} + for name, obj in inspect.getmembers(sys.modules[__name__]): + if inspect.isclass(obj) and issubclass(obj, MasterConfig): + available_configs[name] = obj + available_configs.pop('MasterConfig') + return available_configs + + +def overwrite(config_obj): + ''' Overwrites parameters with input flags. Function is needed for the + convenience of specifying parameters via a combination of the config classes + and input flags. ''' + class NullIO(StringIO): + def write(self, txt): + pass + + def parse_unknown_flags(**kwargs): + return kwargs + + sys.stdout = NullIO() + extra_arguments = fire.Fire(parse_unknown_flags) + sys.stdout = sys.__stdout__ + + for key, val in extra_arguments.items(): + if key not in vars(config_obj): + err_str = f"The input parameter '{key}' isn't allowed. It's only possible to overwrite attributes that exist in the DefaultConfig class. Add your input parameter to the default class or catch it before this message" + raise NotImplementedError(err_str) + setattr(config_obj, key, val) + + return config_obj + + +def config_class(func): + class_name = func.__name__ + err_msg = (f"Can't decorate '{class_name}' of type {type(func)}. " + "Can only be used for classes") + assert inspect.isclass(func), err_msg + err_msg = (f"Can't decorate '{class_name}' since it's not a sublass of " + "'chilliconfig.MasterConfig'") + assert issubclass(func, MasterConfig), err_msg + setattr(sys.modules[__name__], class_name, func) + + return func + + +def print_source(func): + class_name = func.__name__ + err_msg = (f"Can't decorate '{class_name}' of type {type(func)}. " + "Can only be used for classes") + + assert inspect.isclass(func), err_msg + + def __print_source__(self): + ''' Get my source. Get my childrens sources ''' + + # Newline makes indention better + src = '\n' + inspect.getsource(self.__class__) + + unique_classes = {v.__class__: v for k, v in vars(self).items()} + for key, val in unique_classes.items(): + if hasattr(val, '__anyfig_print_source__'): + src += __print_source__(val) + + # TODO: Source code can have different indention than \t + # Make it a config to anyfig? + # Adds one indention + src = src.replace('\n', '\n ') + return src + + setattr(func, '__anyfig_print_source__', __print_source__) + return func \ No newline at end of file diff --git a/run_example.py b/run_example.py new file mode 100644 index 0000000..bd12f21 --- /dev/null +++ b/run_example.py @@ -0,0 +1,55 @@ +import anyfig + + +@anyfig.print_source +class Noise(): + def __init__(self, x, y): + self.x = x + self.y = y + + +@anyfig.print_source +class Flip(): + def __init__(self, x, y): + self.x = x + self.y = y + self.noise3 = Noise(1, 2) + + +@anyfig.print_source +class Transformer(): + def __init__(self, x): + # self.noise1 = Noise(x, x) + # self.flip = Flip(4, 0) + # self.noise2 = Noise(4, 0) + self.primitive = x + + +# @config_class +# class MainConfig(MasterConfig): +# def __init__(self): +# print("MAIN CONFIG SUPER") +# self.start_time = time.time() +# self.img_size = 100 +# self.classes = ['car', 'dog'] +# self.freeze_config = False + + +@anyfig.config_class +class Train(anyfig.MasterConfig): + def __init__(self): + super().__init__() + self.name = 'oldname' + self.transforms111 = Transformer(100) + # self.freeze_config = False + # self.frozen = 123 + + +def main(): + config = anyfig.setup_config(default_config='Train') + print(config) + + +if __name__ == '__main__': + print("BEFORE MAIN") + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e7c4931 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from distutils.core import setup +setup( + name='anyfig', + packages=['anyfig'], + version='0.0.1', + license='MIT', + description='Config parameters in Python code. Anything is possible ;)', + author='Olof Harrysson', + author_email='harrysson.olof@gmail.com', + url= + 'https://github.com/user/reponame', # Provide either the link to your github or to your website + download_url= + 'https://github.com/user/reponame/archive/v_01.tar.gz', # I explain this later on + keywords=['SOME', 'MEANINGFULL', + 'KEYWORDS'], # Keywords that define your package best + install_requires=[ # I get to this in a second + 'validators', + 'beautifulsoup4', + ], + classifiers=[ + 'Development Status :: 3 - Alpha', # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package + 'Intended Audience :: Developers', # Define that your audience are developers + 'Topic :: Software Development :: Build Tools', + 'License :: OSI Approved :: MIT License', # Again, pick a license + 'Programming Language :: Python :: 3', #Specify which pyhton versions that you want to support + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], +) \ No newline at end of file