├── .gitignore ├── LICENSE ├── README.md ├── doc └── README.md ├── nip ├── __init__.py ├── constructor.py ├── convertor.py ├── directives.py ├── dumper.py ├── elements.py ├── iter_parser.py ├── main.py ├── non_seq_constructor.py ├── parser.py ├── stream.py ├── tokens.py └── utils.py ├── pyproject.toml ├── setup.cfg └── tests ├── base_tests ├── comments │ ├── configs │ │ ├── comment.nip │ │ └── empty_comment.nip │ └── test_comments.py ├── document │ ├── configs │ │ └── document.nip │ └── test_document.py ├── numbers │ ├── configs │ │ └── numbers.nip │ └── test_numbers.py ├── raises │ ├── configs │ │ ├── wrong_dict_indent.nip │ │ └── wrong_item.nip │ └── test_wrong_dicts.py ├── strings │ ├── configs │ │ └── long_strings_config.nip │ └── test_strings.py └── tags │ ├── configs │ ├── empty_args.nip │ ├── multi_tag.nip │ ├── no_args_func_error.nip │ ├── numpy.nip │ └── simple_tag_config.nip │ └── test_tag.py ├── complex ├── config_dumps │ ├── ababra.yaml │ ├── avocado.yaml │ └── cadabra.yaml ├── configs │ └── config.nip └── test_complex_example.py ├── features ├── __init__.py ├── args │ ├── configs │ │ └── args_config.nip │ └── test_args.py ├── class_construction │ ├── configs │ │ └── class_const_config.nip │ └── test_class_const.py ├── directives │ ├── configs │ │ ├── directive_config.nip │ │ ├── expected_config.nip │ │ └── loaded_config.nip │ └── test_directives.py ├── flatten │ ├── configs │ │ ├── complex.nip │ │ └── simple.nip │ └── test_flatten.py ├── fstrings │ ├── configs │ │ ├── fstrings.nip │ │ └── iter_fstrings.nip │ └── test_fstrings.py ├── inline │ ├── configs │ │ └── inline_config.nip │ └── test_inline.py ├── modification │ ├── configs │ │ ├── config.nip │ │ └── object_config.nip │ ├── dumps │ │ ├── config.nip │ │ ├── object_config.nip │ │ └── object_config_2.nip │ └── test_modification.py ├── module_wrap │ ├── configs │ │ └── auto_wrap_config.yaml │ └── test_module.py ├── non_seq │ ├── configs │ │ ├── harder_non_seq.nip │ │ ├── inline_non_seq.nip │ │ ├── recursive_non_seq.nip │ │ └── simple_non_seq.nip │ └── test_non_seq.py ├── nothing │ ├── configs │ │ └── nothing.nip │ └── test_nothing.py ├── object_dump │ ├── dumps │ │ ├── complex.nip │ │ ├── no_default.nip │ │ └── obj.nip │ ├── some_classes.py │ └── test_dump.py ├── replacement │ ├── configs │ │ ├── config.nip │ │ └── sub_config.nip │ └── test_replacement.py ├── run │ ├── class_example.py │ ├── configs │ │ ├── run.nip │ │ └── run_config_param.nip │ └── test_run.py └── strict │ ├── configs │ ├── double_names.nip │ ├── strict_func_types_1.nip │ ├── strict_func_types_2.nip │ ├── strict_func_types_3.nip │ └── strict_func_types_correct.nip │ └── test_strict.py └── utils ├── __init__.py ├── builders.py └── test_utils.py /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spairet/nip/a019ab56284f072d79fd74812b48d370e133e464/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ilya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NIP 2 | 3 | User guide can be found in [doc directory](https://github.com/spairet/nip/tree/main/doc) 4 | 5 | 6 | Installation 7 | -- 8 | 9 | ``` sh 10 | $ pip install nip-config 11 | ``` 12 | 13 | Contribution 14 | -- 15 | Everything currently presented in nip and future plans are discussable. Feel free to suggest any ideas for future updates or PRs. 16 | 17 | If you find any bugs or unexpected behaviour please report it with an attached config file. 18 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | User Guide 2 | ---- 3 | This brief guide will walk you through main functionality and some extended features of **nip**. 4 | 5 | ### Usual yaml behaviour 6 | **NIP** inherits syntax and most of base functionality of yaml config files with a small differences. 7 | Lets take a look at this config example. 8 | > config.nip 9 | >```yaml 10 | >--- 11 | > 12 | >main: &main 13 | > yaml_style_dict: 14 | > string: "11" 15 | > also_string: I am a string! 16 | > float: 12.5 17 | > yaml_style_list: 18 | > - first item # inline comment 19 | > - 2 20 | > - 21 | > - nested list value 22 | > inline_list: [1, 2, 3] 23 | > incline_dict: {'a': "123", 'b': 321} 24 | > 25 | >also_main: *main 26 | >``` 27 | 28 | _Note:_ **nip** is not extension sensitive, so feel free to change extension to `.yml` for syntax highlighting. **Nip** doesn't have its own highlighting yet :cry: 29 | 30 | ### Main functions 31 | 32 | - `parse("config.nip")` - parses config into `Element` that can be used easily in your code 33 | - `construct(element)` - constructs element into python object 34 | - `load("config.nip")` - parses config and constructs python object 35 | - `dump("save_config.nip", element)` - dumps element to a config file 36 | - `dumps(element)` - return dumped element as a string 37 | 38 | ### Constructing custom objects 39 | Just like PyYAML, **nip** allows you to construct your own python objects from config. But in **nip** this is a way more easier. :yum: 40 | 41 | First of all, you need to wrap you function or class with `@nip` decorator. For example: 42 | ```python 43 | from nip import nip 44 | 45 | 46 | @nip 47 | class MyCoolClass: 48 | def __init__(self, question, answer): 49 | self.question = question 50 | self.answer = answer 51 | 52 | 53 | @nip('just_func') 54 | def MyCoolFunc(a, b=2, c=3): 55 | return a + b * 2 + c * 3 56 | ``` 57 | 58 | Now you are able to construct them with a config file using tag operator `!`. List or dict that lies under tagged node will be passed as args and kwargs to the fucntion or class. 59 | ```yaml 60 | class_object: !MyCoolClass 61 | question: Ultimate Question of Life, The Universe, and Everything 62 | answer: 42 63 | 64 | func_value: !just_func 65 | - 1 66 | c: 4 67 | ``` 68 | So `load("config.nip")` will return the following dict: 69 | ```yaml 70 | { 71 | 'class_object': <__main__.MyCoolClass object at 0x000001F6E13F1988>, 72 | 'func_value': 17 73 | } 74 | ``` 75 | _Note:_ if you specify your wrapped objects in other `.py` file, you have to import it before calling `load()`. 76 | 77 | 78 | There is a number of features for this functionality: 79 | 1. `@nip` decorator allows you to specify name for the object to be used in config file (`just_func` in the example) 80 | 2. You can combine `args` and `kwargs` in the config. (`just_func` creation). 81 | 3. You can automatically wrap everything under module. 82 | Here are two variants of wrapping `source` module: 83 | ```python 84 | from nip import nip 85 | 86 | import source 87 | nip(source) 88 | ``` 89 | ```python 90 | from nip import wrap_module 91 | 92 | wrap_module("source") 93 | ``` 94 | 95 | ### Iterable configs 96 | It is a common case in experimentation, when you want to run a number of experiments with different parameters. 97 | For this case iterable configs will help you a lot! :yum: 98 | 99 | Using **nip** you are able to create elements that don't have a constant values but have an iterables instead. Let's take a look at this example: 100 | ```yaml 101 | a: &a 102 | const: 1 103 | iter: @ [1, 2, 3] 104 | 105 | b: 106 | a_copy: *a 107 | another_iter: @ [4, 5, 6] 108 | ``` 109 | `@` operator allows you to specify values that should be iterated. 110 | Now `parse` and `load` functions will return an iterator over this configs and constructed objects respectively. 111 | In this example there are two iterable objects, and so Cartesian product of all this iterables will be created. 112 | This small code: 113 | ```python 114 | from nip import load 115 | 116 | for obj in load('config.nip'): 117 | print(obj) 118 | ``` 119 | will result in: 120 | ```python 121 | {'a': {'const': 1, 'iter': 1}, 'b': {'a_copy': {'const': 1, 'iter': 1}, 'another_iter': 4}} 122 | {'a': {'const': 1, 'iter': 1}, 'b': {'a_copy': {'const': 1, 'iter': 1}, 'another_iter': 5}} 123 | {'a': {'const': 1, 'iter': 1}, 'b': {'a_copy': {'const': 1, 'iter': 1}, 'another_iter': 6}} 124 | {'a': {'const': 1, 'iter': 2}, 'b': {'a_copy': {'const': 1, 'iter': 2}, 'another_iter': 4}} 125 | {'a': {'const': 1, 'iter': 2}, 'b': {'a_copy': {'const': 1, 'iter': 2}, 'another_iter': 5}} 126 | {'a': {'const': 1, 'iter': 2}, 'b': {'a_copy': {'const': 1, 'iter': 2}, 'another_iter': 6}} 127 | {'a': {'const': 1, 'iter': 3}, 'b': {'a_copy': {'const': 1, 'iter': 3}, 'another_iter': 4}} 128 | {'a': {'const': 1, 'iter': 3}, 'b': {'a_copy': {'const': 1, 'iter': 3}, 'another_iter': 5}} 129 | {'a': {'const': 1, 'iter': 3}, 'b': {'a_copy': {'const': 1, 'iter': 3}, 'another_iter': 6}} 130 | ``` 131 | If you need to iterate some lists synchronously you can specify iter names: `@a [1, 2, 3]`. All the iterators with the same name will be iterated together. The order of iterators is determind by the alphabetic order of thier names. 132 | 133 | _Note:_ All the iterators works with construction of your custom objects only once and using links in the config doesn't recreates the objects. See above example to clarify. 134 | 135 | ### Additional features 136 | There are some small but useful features also presented in **nip**: 137 | 1. Accessing parts of your config without full construction. Once you parsed your config 138 | ```python 139 | from nip import parse, construct 140 | config = parse("config.nip") 141 | ``` 142 | you are able to access its parts using `[]` and construct only them: 143 | ```python 144 | part = config['main']['yaml_style_list'][2] 145 | inner_list = construct(part) # ['nested list value'] 146 | ``` 147 | This might be useful, for example, you want to load only your machine learning model using config of full training pipeline. 148 | 2. `to_python()` method. Every element in **nip** has `to_python` method. So, you can access any parameter of the config without constructing the objects: 149 | ```python 150 | from nip import parse 151 | config = parse('construct_exampe.nip') 152 | print(config['func_value']['c'].to_python()) # 4 153 | ``` 154 | 3. `run()` function. This function will iterate over the configs and run your function multiple times. You should specify function you want to run at the top of the document. 155 | ```yaml 156 | --- !run_experiment 157 | model: 158 | ... 159 | num_layers: @ [3, 4, 5] 160 | ... 161 | dataset: 162 | ... 163 | ``` 164 | And python code will look like this: 165 | ```python 166 | from nip import nip, run 167 | 168 | @nip 169 | def run_experiment(model, dataset): 170 | # some machine learning staff here 171 | 172 | run('experiment_config.nip') 173 | ``` 174 | This will result in running a number of experiments using generated configs. 175 | 4. Strict nip. Nip can check typing while constructing objects and keys overwriting in dicts. `strict` parameter of `load` function stands for it. By default only typing warnings are generated. 176 | 177 | 178 | Most of the functions mentioned in this short documentation have additional parameters, so feel free to look into the docstrings. :yum: 179 | -------------------------------------------------------------------------------- /nip/__init__.py: -------------------------------------------------------------------------------- 1 | from .constructor import Constructor 2 | from .constructor import nip, wrap_module 3 | from .convertor import pin 4 | from .main import ( 5 | parse, 6 | parse_string, 7 | construct, 8 | load, 9 | load_string, 10 | dump, 11 | dump_string, 12 | convert, 13 | run, 14 | ) 15 | from .elements import Node 16 | from .non_seq_constructor import NonSequentialConstructor 17 | from .parser import Parser 18 | -------------------------------------------------------------------------------- /nip/constructor.py: -------------------------------------------------------------------------------- 1 | # Constructor of tagged objects 2 | import importlib 3 | import importlib.util 4 | from types import FunctionType, ModuleType, BuiltinFunctionType 5 | from typing import Callable, Optional, Union 6 | 7 | from .utils import get_sub_dict 8 | 9 | global_builders = {} # builders shared between Constructors 10 | global_calls = {} # history of object creations 11 | 12 | 13 | class Constructor: 14 | def __init__(self, ignore_rewriting=False, load_builders=True, strict_typing=False): 15 | self.builders = {} 16 | self.ignore_rewriting = ignore_rewriting 17 | if load_builders: 18 | self.load_builders() 19 | self.vars = {} 20 | self.strict_typing = strict_typing 21 | 22 | def construct(self, element): 23 | return element._construct(self) 24 | 25 | def register(self, func: Callable, tag: Optional[str] = None): 26 | """Registers builder function for tag 27 | Parameters 28 | ---------- 29 | func: 30 | Function or class to build the python object. 31 | In case of class its __init__ method will be called to construct object. 32 | tag: str, optional 33 | Tag in yaml/nip file. func.__name__ will be used if not specified. 34 | """ 35 | if tag is None: 36 | tag = func.__name__ 37 | assert self.ignore_rewriting or tag not in self.builders, f"Builder for tag '{tag}' already registered" 38 | self.builders[tag] = func 39 | 40 | def load_builders(self): 41 | self.builders.update(global_builders) 42 | self.builders.update(get_sub_dict(NIPBuilder)) 43 | 44 | 45 | class ConstructorError(Exception): 46 | def __init__(self, element, args, kwargs, e): 47 | self.cls = type(element).__name__ 48 | self.name = element._name 49 | self.args = args 50 | self.kwargs = kwargs 51 | self.e = e 52 | 53 | def __str__(self): 54 | return ( 55 | f"Unable to construct {self.cls} '{self.name}' with args:{self.args} and " 56 | f"kwargs:{self.kwargs}.\nFollowing exception occurred:\n" 57 | f"{self.e.__class__.__name__}: {self.e}" 58 | ) 59 | 60 | 61 | # mb: add meta for auto detecting this class as NIP-builder 62 | # ToDo: Add init wrapper for auto detection init args for convenient object dumping 63 | class NIPBuilder: 64 | pass 65 | 66 | 67 | def nip_decorator(name=None, convertable=False): 68 | assert name is None or len(name) > 0, "name should be nonempty" 69 | 70 | def _(item): 71 | if convertable: 72 | assert isinstance(item, type), "Call wrapping supported only for class type" 73 | make_convertable(item) 74 | local_name = name or item.__name__ 75 | if isinstance(local_name, (list, tuple)): 76 | for n in local_name: 77 | global_builders[n] = item 78 | else: 79 | global_builders[local_name] = item 80 | return item 81 | 82 | return _ 83 | 84 | 85 | # instead of multipledispatch 86 | def nip(item=None, *, wrap_builtins=False, convertable=False): 87 | if isinstance(item, str): # single name is passed 88 | return nip_decorator(item, convertable) 89 | if isinstance(item, (list, tuple)): 90 | for name in item: 91 | if not isinstance(name, str): 92 | raise ValueError("Every specified Tag should be a string.") 93 | return nip_decorator(item, convertable) 94 | if isinstance(item, (type, FunctionType, BuiltinFunctionType)): 95 | return nip_decorator(convertable=convertable)(item) 96 | if isinstance(item, ModuleType): 97 | return wrap_module(item, wrap_builtins=wrap_builtins, convertable=convertable) 98 | if item is not None: 99 | raise ValueError("Unexpected type passed to @nip decorator.") 100 | return nip_decorator(convertable=convertable) 101 | 102 | 103 | def wrap_module(module: Union[str, ModuleType], wrap_builtins=False, convertable=False): 104 | """Wraps everything declared in module with @nip 105 | 106 | Parameters 107 | ---------- 108 | module: str or ModuleType 109 | Module name (e.g. "numpy.random") or module itself 110 | wrap_builtins 111 | Whether to wrap builtin functions or not. 112 | (Useful when wrapping whole module like `numpy`) 113 | """ 114 | if isinstance(module, str): 115 | module = importlib.import_module(module) 116 | 117 | for value in module.__dict__.values(): 118 | if isinstance(value, (type, FunctionType)) or wrap_builtins and isinstance(value, BuiltinFunctionType): 119 | nip(value, convertable=convertable and isinstance(value, type)) 120 | 121 | return module 122 | 123 | 124 | class ArgsKwargs: 125 | def __init__(self, args, kwargs): 126 | self.args = args 127 | self.kwargs = kwargs 128 | 129 | 130 | def _wrap_init_call(self, *args, **kwargs): 131 | self.__init_args = ArgsKwargs(args, kwargs) 132 | self.__origin_init__(*args, **kwargs) 133 | 134 | 135 | def _converter(self): 136 | return self.__init_args 137 | 138 | 139 | def make_convertable(cls): 140 | if hasattr(cls, "__nip__"): 141 | return 142 | cls.__origin_init__ = cls.__init__ 143 | cls.__init__ = _wrap_init_call 144 | cls.__nip__ = _converter 145 | -------------------------------------------------------------------------------- /nip/convertor.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Callable, Union 2 | 3 | from nip.constructor import global_builders, ArgsKwargs 4 | from nip.elements import Node, Args, Tag, Value 5 | 6 | global_convertors = {} 7 | 8 | 9 | class Convertor: 10 | def __init__(self, load_globals: bool = True): 11 | self.convertors = {} 12 | if load_globals: 13 | self._load_globals() 14 | 15 | def convert(self, obj: object) -> Node: 16 | class_name = type(obj).__name__ 17 | if class_name in self.convertors: 18 | tag, convertor = self.convertors[class_name] 19 | return Tag(tag, self.convert(convertor(obj))) 20 | 21 | if hasattr(obj, "__nip__"): 22 | return Tag(class_name, self.convert(obj.__nip__())) 23 | 24 | if isinstance(obj, ArgsKwargs): 25 | args = [self.convert(value) for value in obj.args] 26 | kwargs = {key: self.convert(value) for key, value in obj.kwargs.items()} 27 | return Args("args", (args, kwargs)) 28 | 29 | if isinstance(obj, dict): 30 | kwargs = {} 31 | for key, value in obj.items(): 32 | kwargs[key] = self.convert(value) 33 | return Args("args", ([], kwargs)) 34 | 35 | if isinstance(obj, (list, tuple)): 36 | return Args("args", ([self.convert(value) for value in obj], {})) 37 | 38 | if isinstance(obj, (int, float, str, bool)): 39 | return Value("value", obj) 40 | 41 | raise ConvertorError(obj, "No convertor specified for this class") 42 | 43 | def register(self, class_: Union[type, str], func: Callable, tag: Optional[str] = None): 44 | if isinstance(class_, type): 45 | class_name = class_.__name__ 46 | elif isinstance(class_, str): 47 | class_name = class_ 48 | else: 49 | raise TypeError("Expected type or str as class_ argument") 50 | if tag is None: 51 | for builder_tag, builder in global_builders.items(): 52 | if isinstance(builder, type) and builder.__class__.__name__ == class_name: 53 | tag = builder_tag 54 | tag = tag or class_name 55 | self.convertors[class_name] = (tag, func) 56 | 57 | def _load_globals(self): 58 | self.convertors.update(global_convertors) 59 | for builder_tag, builder in global_builders.items(): 60 | if isinstance(builder, type) and hasattr(builder, "__nip__"): 61 | self.convertors[builder.__name__] = (builder_tag, builder.__nip__) 62 | 63 | 64 | class ConvertorError(Exception): 65 | def __init__(self, obj, message: str): 66 | self.class_name = obj.__class__.__name__ 67 | self.obj = obj 68 | self.message = message 69 | 70 | def __str__(self): 71 | return f"Unable to convert object {self.obj} of class {self.class_name} to nip: " f"{self.message}" 72 | 73 | 74 | def pin(class_name: str, tag: str): 75 | """Wrapper that registers the function as an object dumper. 76 | 77 | Parameters 78 | ---------- 79 | class_name: 80 | Class that should be dumped using this function. 81 | tag: 82 | What tag should be used for this class in config file. 83 | """ 84 | assert tag is None or len(tag) > 0, "name should be nonempty" 85 | 86 | def _(item: type): 87 | if not isinstance(item, type): 88 | raise ValueError("Expected class type to be wrapped.") 89 | global_convertors[class_name] = (tag, item) 90 | return item 91 | 92 | return _ 93 | -------------------------------------------------------------------------------- /nip/directives.py: -------------------------------------------------------------------------------- 1 | """Contains nip directives.""" 2 | 3 | import nip.elements 4 | from .constructor import Constructor 5 | from .parser import Parser, ParserError 6 | from .stream import Stream 7 | 8 | 9 | def insert_directive(right_value, stream: Stream): 10 | if isinstance(right_value, nip.elements.Value): 11 | constructor = Constructor() 12 | path = constructor.construct(right_value) 13 | assert isinstance(path, str), "Load directive expects path as an argument." 14 | parser = Parser() 15 | config = parser.parse(path) # Document 16 | return config._value 17 | 18 | elif isinstance(right_value, nip.elements.Args): 19 | assert len(right_value._value[0]) == 1, "only single positional argument will be treated as config path." 20 | constructor = Constructor() 21 | path = constructor.construct(right_value._value[0][0]) 22 | assert isinstance(path, str), "Load directive expects path as first argument." 23 | parser = Parser() 24 | parser.link_replacements = right_value._value[1] 25 | config = parser.parse(path) # Document 26 | return config._value 27 | 28 | else: 29 | raise ParserError( 30 | stream, 31 | "string or combination of arg and **kwargs are expected as value of !!insert directive", 32 | ) 33 | 34 | 35 | _directives = {"insert": insert_directive} 36 | 37 | 38 | def call_directive(name, right_value, stream: Stream): 39 | if name not in _directives: 40 | raise ParserError(stream, f"Unknown parser directive '{name}'.") 41 | return _directives[name](right_value, stream) 42 | -------------------------------------------------------------------------------- /nip/dumper.py: -------------------------------------------------------------------------------- 1 | # Dumper for Element tree objects 2 | from pathlib import Path 3 | from typing import Union 4 | 5 | 6 | class Dumper: 7 | def __init__(self, indent: int = 0, default_shift: int = 2, create_dirs: bool = True): 8 | self.indent = indent 9 | self.default_shift = default_shift 10 | self.create_dirs = create_dirs 11 | 12 | def dumps(self, element): 13 | return element._dump(self).strip() 14 | 15 | def dump(self, filepath: Union[str, Path], element): 16 | string = element._dump(self).strip() 17 | filepath = Path(filepath) 18 | if self.create_dirs: 19 | filepath.parent.mkdir(parents=True, exist_ok=True) 20 | with filepath.open("w") as f: 21 | f.write(string) 22 | 23 | def __add__(self, shift: int): 24 | return Dumper(self.indent + shift, self.default_shift) 25 | 26 | 27 | class DumpError(Exception): 28 | pass 29 | -------------------------------------------------------------------------------- /nip/elements.py: -------------------------------------------------------------------------------- 1 | """Contains all the elements of nip config files""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from abc import abstractmethod, ABC 7 | from pathlib import Path 8 | from typing import Any, Union, Tuple, Dict 9 | 10 | import nip.constructor # This import pattern because of cycle imports 11 | import nip.directives 12 | import nip.dumper 13 | import nip.non_seq_constructor as nsc 14 | import nip.parser 15 | import nip.stream 16 | import nip.tokens as tokens 17 | import nip.utils 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | class Node(ABC, object): 23 | """Base token for nip file""" 24 | 25 | def __init__(self, name: str = "", value: Union[Node, Any] = None): 26 | self._name = name 27 | self._value = value 28 | self._parent = None 29 | 30 | @classmethod 31 | @abstractmethod 32 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Node, None]: 33 | pass 34 | 35 | def __str__(self): 36 | return f"{self.__class__.__name__}('{self._name}', {self._value})" 37 | 38 | def __getitem__(self, item): 39 | if not isinstance(item, (str, int)): 40 | raise KeyError(f"Unexpected item type: {type(item)}") 41 | if isinstance(item, str) and len(item) == 0: 42 | return self 43 | return self._value[item] 44 | 45 | def __getattr__(self, item): # unable to access names like `construct` and 'dump` via this method 46 | return self.__getitem__(item) 47 | 48 | def __setitem__(self, key, value): 49 | self._value[key] = value 50 | self._value._parent = self 51 | 52 | def __setattr__(self, key, value): 53 | if key.startswith("_"): # mb: ensure not user's node name? 54 | self.__dict__[key] = value 55 | else: 56 | self.__setitem__(key, value) 57 | 58 | def to_python(self): 59 | return self._value.to_python() 60 | 61 | def _construct(self, constructor: nip.constructor.Constructor): 62 | return self._value._construct(constructor) 63 | 64 | def construct(self, base_config: Node = None, strict_typing: bool = False, nonsequential: bool = True): 65 | return nip.construct(self, base_config=base_config, strict_typing=strict_typing, nonsequential=nonsequential) 66 | 67 | def _dump(self, dumper: nip.dumper.Dumper): 68 | return self._value._dump(dumper) 69 | 70 | def dump(self, path: Union[str, Path]): 71 | nip.dump(path, self) 72 | 73 | def dump_string(self): 74 | return nip.dump_string(self) 75 | 76 | def __eq__(self, other): 77 | return self._name == other._name and self._value == other._value 78 | 79 | def flatten(self, delimiter=".") -> Dict: 80 | return nip.utils.flatten(self.to_python(), delimiter) 81 | 82 | def _update_parents(self): 83 | if isinstance(self._value, Node): 84 | self._value._parent = self 85 | self._value._update_parents() 86 | 87 | def _get_root(self): 88 | if self._parent is None: 89 | return self 90 | return self._parent._get_root() 91 | 92 | def update(self): 93 | self._get_root().update() 94 | 95 | 96 | Element = Node # backward compatibility until 1.* version 97 | 98 | 99 | class Document(Node): # ToDo: add multi document support 100 | def __init__(self, name: str = "", value: Union[Node, Any] = None): 101 | super().__init__(name, value) 102 | self._path = None 103 | 104 | @classmethod 105 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Document: 106 | doc_name = cls._read_name(stream) 107 | content = read_node(stream, parser) 108 | return Document(doc_name, content) 109 | 110 | @classmethod 111 | def _read_name(cls, stream: nip.stream.Stream): 112 | read_tokens = stream.peek(tokens.Operator("---"), tokens.Name) or stream.peek(tokens.Operator("---")) 113 | if read_tokens is not None: 114 | stream.step() 115 | if len(read_tokens) == 2: 116 | return read_tokens[1]._value 117 | return "" 118 | 119 | def _dump(self, dumper: nip.dumper.Dumper): 120 | string = "---" 121 | if self._name: 122 | string += " " + self._name + " " 123 | return string + self._value._dump(dumper) 124 | 125 | def update(self): 126 | self.dump(self._path) 127 | 128 | 129 | class Value(Node): 130 | @classmethod 131 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[None, Value]: 132 | tokens_list = [ 133 | tokens.Number, 134 | tokens.Bool, 135 | tokens.String, 136 | tokens.List, 137 | tokens.TupleToken, 138 | tokens.Dict, 139 | ] 140 | for token in tokens_list: 141 | read_tokens = stream.peek(token) 142 | if read_tokens is not None: 143 | stream.step() 144 | return Value(read_tokens[0]._name, read_tokens[0]._value) 145 | 146 | return None 147 | 148 | def to_python(self): 149 | return self._value 150 | 151 | def _construct(self, constructor: nip.constructor.Constructor = None): 152 | return self._value 153 | 154 | def _dump(self, dumper: nip.dumper.Dumper): 155 | if isinstance(self._value, str): 156 | return f'"{self._value}"' 157 | return str(self._value) 158 | 159 | def __len__(self): 160 | return len(self._value) 161 | 162 | 163 | class LinkCreation(Node): 164 | @classmethod 165 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Node, None]: 166 | read_tokens = stream.peek(tokens.Operator("&"), tokens.Name) 167 | if read_tokens is None: 168 | # if stream.peek(tokens.Operator('&')): # mb: do it more certainly: peak operator 169 | # raise nip.parser.ParserError( # mb: firstly and then choose class to read) 170 | # stream, "Found variable creation operator '&' but name is not specified") 171 | return None 172 | 173 | name = read_tokens[1]._value 174 | stream.step() 175 | 176 | value = read_node(stream, parser) 177 | if name in parser.links: 178 | raise nip.parser.ParserError(stream, f"Redefining of link '{name}'") 179 | parser.links.append(name) 180 | 181 | return LinkCreation(name, value) 182 | 183 | def _construct(self, constructor: nip.constructor.Constructor): 184 | if nsc.should_construct(self._name, constructor): 185 | constructor.vars[self._name] = self._value._construct(constructor) 186 | return constructor.vars[self._name] 187 | 188 | def _dump(self, dumper: nip.dumper.Dumper): 189 | return f"&{self._name} {self._value._dump(dumper)}" 190 | 191 | 192 | class Link(Node): 193 | @classmethod 194 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Node, None]: 195 | read_tokens = stream.peek(tokens.Operator("*"), tokens.Name) 196 | if read_tokens is None: 197 | return None 198 | 199 | name = read_tokens[1]._value 200 | stream.step() 201 | 202 | if name in parser.link_replacements: 203 | return parser.link_replacements[name] 204 | 205 | if parser.sequential_links and name not in parser.links: 206 | nip.parser.ParserError(stream, "Link usage before assignment") 207 | 208 | return Link(name) 209 | 210 | def to_python(self): 211 | return "nil" # something that means that object is not constructed yet. 212 | 213 | def _construct(self, constructor: nip.constructor.Constructor): 214 | return constructor.vars[self._name] 215 | 216 | def _dump(self, dumper: nip.dumper.Dumper): 217 | return f"*{self._name}" 218 | 219 | 220 | class Tag(Node): 221 | @classmethod 222 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Tag, None]: 223 | read_tokens = stream.peek(tokens.Operator("!"), tokens.Name) 224 | if read_tokens is None: 225 | return None 226 | name = read_tokens[1]._value 227 | stream.step() 228 | 229 | value = read_node(stream, parser) 230 | 231 | return Tag(name, value) 232 | 233 | def _construct(self, constructor: nip.constructor.Constructor): 234 | if isinstance(self._value, Args): 235 | args, kwargs = self._value._construct(constructor, always_pair=True) 236 | else: 237 | value = self._value._construct(constructor) 238 | if isinstance(value, Nothing): # mb: Add IS_NOTHING method 239 | return constructor.builders[self._name]() 240 | else: 241 | args, kwargs = [value], {} 242 | 243 | if self._name not in constructor.builders: 244 | raise nip.constructor.ConstructorError( 245 | self, 246 | args, 247 | kwargs, 248 | f"Constructor for Tag '{self._name}' is not registered.", 249 | ) 250 | 251 | messages = nip.utils.check_typing(constructor.builders[self._name], args, kwargs) 252 | if len(messages) > 0: 253 | if constructor.strict_typing: 254 | raise nip.constructor.ConstructorError(self, args, kwargs, "\n".join(messages)) 255 | else: 256 | _LOGGER.warning(f"Typing mismatch while constructing {self._name}:\n" + "\n".join(messages)) 257 | 258 | try: # Try to construct 259 | return constructor.builders[self._name](*args, **kwargs) 260 | except Exception as e: 261 | raise nip.constructor.ConstructorError(self, args, kwargs, e) 262 | 263 | def _dump(self, dumper: nip.dumper.Dumper): 264 | return f"!{self._name} " + self._value._dump(dumper) 265 | 266 | 267 | class Class(Node): 268 | @classmethod 269 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Class, None]: 270 | read_tokens = stream.peek(tokens.Operator("!&"), tokens.Name) 271 | if read_tokens is None: 272 | return None 273 | name = read_tokens[1]._value 274 | stream.step() 275 | 276 | value = read_node(stream, parser) 277 | if not isinstance(value, Nothing): 278 | raise nip.parser.ParserError(stream, "Class should be created with nothing to the right.") 279 | 280 | return Class(name, value) 281 | 282 | def _construct(self, constructor: nip.constructor.Constructor): 283 | value = self._value._construct(constructor) 284 | assert isinstance(value, Nothing), "Unexpected right value while constructing Class" 285 | 286 | return constructor.builders[self._name] 287 | 288 | def _dump(self, dumper: nip.dumper.Dumper): 289 | return f"!&{self._name} " + self._value._dump(dumper) 290 | 291 | 292 | class Args(Node): 293 | @classmethod 294 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Args, None]: 295 | start_indent = stream.pos 296 | if start_indent <= parser.last_indent: 297 | return None 298 | 299 | args = [] 300 | kwargs = {} # mb: just dict with integer and string keys ! 301 | read_kwarg = False 302 | while stream and stream.pos == start_indent: 303 | parser.last_indent = start_indent 304 | 305 | item = cls._read_list_item(stream, parser) 306 | if item is not None: 307 | if parser.strict and read_kwarg: 308 | raise nip.parser.ParserError( 309 | stream, 310 | "Positional argument after keyword argument is forbidden in `strict` mode.", 311 | ) 312 | args.append(item) 313 | continue 314 | 315 | key, value = cls._read_dict_pair(stream, parser, kwargs.keys()) 316 | if key is not None: 317 | read_kwarg = True 318 | kwargs[key] = value 319 | continue 320 | 321 | break 322 | 323 | if stream.pos > start_indent: 324 | raise nip.parser.ParserError(stream, "Unexpected indent") 325 | 326 | if not args and not kwargs: 327 | return None 328 | return Args("args", (args, kwargs)) 329 | 330 | @classmethod 331 | def _read_list_item(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Node, None]: 332 | read_tokens = stream.peek(tokens.Operator("- ")) 333 | if read_tokens is None: 334 | return None 335 | stream.step() 336 | 337 | value = read_node(stream, parser) 338 | 339 | return value 340 | 341 | @classmethod 342 | def _read_dict_pair( 343 | cls, stream: nip.stream.Stream, parser: nip.parser.Parser, kwargs_keys 344 | ) -> Union[Tuple[str, Node], Tuple[None, None]]: 345 | # mb: read String instead of Name for keys with spaces, 346 | # mb: but this leads to the case that 347 | read_tokens = stream.peek(tokens.Name, tokens.Operator(": ")) 348 | if read_tokens is None: 349 | return None, None 350 | 351 | key = read_tokens[0]._value 352 | if parser.strict and key in kwargs_keys: 353 | raise nip.parser.ParserError( 354 | stream, 355 | f"Dict key overwriting is forbidden in `strict` " f"mode. Overwritten key: '{key}'.", 356 | ) 357 | stream.step() 358 | 359 | value = read_node(stream, parser) 360 | 361 | return key, value 362 | 363 | def __str__(self): 364 | args_repr = "[" + ", ".join([str(item) for item in self._value[0]]) + "]" 365 | kwargs_repr = "{" + ", ".join([f"{key}: {str(value)}" for key, value in self._value[1].items()]) + "}" 366 | 367 | return f"{self.__class__.__name__}('{self._name}', {args_repr}, {kwargs_repr})" 368 | 369 | def __bool__(self): 370 | return bool(self._value[0]) or bool(self._value[1]) 371 | 372 | def __getitem__(self, item): 373 | if not isinstance(item, (str, int)): 374 | raise KeyError(f"Unexpected item type: {type(item)}") 375 | if isinstance(item, str) and len(item) == 0: 376 | return self 377 | if isinstance(item, int): 378 | return self._value[0][item] 379 | key = None 380 | for key in self._value[1]: 381 | if item.startswith(key): 382 | break 383 | if not item.startswith(key): 384 | raise KeyError(f"'{item}' is not a part of the Node.") 385 | if len(item) == len(key): 386 | return self._value[1][key] 387 | if item[len(key)] != ".": 388 | raise KeyError(f"items should be separated by a dot '.'.") 389 | 390 | item = item[len(key) + 1 :] 391 | return self._value[1][key][item] 392 | 393 | def __setitem__(self, key, value): 394 | value = nip.convert(value) 395 | if isinstance(key, int): 396 | if not -1 <= key < len(self._value[0]): 397 | raise KeyError( 398 | f"You can only access existing positional arguments or add new one using -1 key. " 399 | f"Index {key} is out of range [-1, {len(self._value[0]) - 1}]." 400 | ) 401 | if key == -1: 402 | self._value[0].append(value) 403 | self._value[0][key] = value 404 | else: 405 | self._value[1][key] = value 406 | 407 | def append(self, value): 408 | self._value[0].append(nip.convert(value)) 409 | 410 | def __len__(self): 411 | return len(self._value[0]) + len(self._value[1]) 412 | 413 | def __iter__(self): 414 | for item in self._value[0]: 415 | yield item 416 | for key, item in self._value[1].items(): 417 | yield item 418 | 419 | def to_python(self): 420 | args = list(item.to_python() for item in self._value[0]) 421 | kwargs = {key: value.to_python() for key, value in self._value[1].items()} 422 | assert args or kwargs, "Error converting Args node to python" # This should never happen 423 | if args and kwargs: 424 | result = {} 425 | result.update(nip.utils.iterate_items(args)) 426 | result.update(nip.utils.iterate_items(kwargs)) 427 | return result 428 | return args or kwargs 429 | 430 | def _construct(self, constructor: nip.constructor.Constructor, always_pair=False): 431 | args = list(item._construct(constructor) for item in self._value[0]) 432 | kwargs = {key: value._construct(constructor) for key, value in self._value[1].items()} 433 | assert args or kwargs, "Error converting Args node to python" # This should never happen 434 | if args and kwargs or always_pair: 435 | return args, kwargs 436 | return args or kwargs 437 | 438 | def _dump(self, dumper: nip.dumper.Dumper): 439 | dumped_args = "\n".join( 440 | [" " * dumper.indent + f"- {item._dump(dumper + dumper.default_shift)}" for item in self._value[0]] 441 | ) 442 | string = ("\n" if dumped_args else "") + dumped_args 443 | 444 | dumped_kwargs = "\n".join( 445 | [ 446 | " " * dumper.indent + f"{key}: {value._dump(dumper + dumper.default_shift)}" 447 | for key, value in self._value[1].items() 448 | ] 449 | ) 450 | string += ("\n" if dumped_kwargs else "") + dumped_kwargs 451 | 452 | return string 453 | 454 | def _update_parents(self): 455 | self.__dict__.update(self._value[1]) 456 | for item in self: 457 | item._parent = self 458 | item._update_parents() 459 | 460 | 461 | class Iter(Node): # mark all parents as Iterable and allow construct specific instance 462 | def __init__(self, name: str = "", value: Any = None): 463 | super(Iter, self).__init__(name, value) 464 | self._return_index = -1 465 | # mb: name all the iterators and get the value from constructor rather then use this index 466 | 467 | @classmethod 468 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Iter, None]: 469 | read_tokens = stream.peek(tokens.Operator("@"), tokens.Name) or stream.peek(tokens.Operator("@")) 470 | if read_tokens is None: 471 | return None 472 | stream.step() 473 | value = read_node(stream, parser) 474 | if isinstance(value, Value) and isinstance(value._value, list): 475 | value = value._value 476 | elif isinstance(value, Args) and len(value._value[1]) == 0: 477 | value = value 478 | else: 479 | raise nip.parser.ParserError(stream, "List is expected as a value for Iterable node") 480 | if len(read_tokens) == 1: 481 | iterator = Iter("", value) 482 | else: 483 | iterator = Iter(read_tokens[1]._value, value) 484 | 485 | parser.iterators.append(iterator) 486 | return iterator 487 | 488 | def to_python(self): 489 | if self._return_index == -1: 490 | raise iter(self._value) 491 | if isinstance(self._value[self._return_index], Node): 492 | return self._value[self._return_index].to_python() 493 | return self._value[self._return_index] 494 | 495 | def _construct(self, constructor: nip.constructor.Constructor): 496 | if self._return_index == -1: 497 | raise Exception("Iterator index was not specified by IterParser") 498 | if isinstance(self._value, list): 499 | return self._value[self._return_index] 500 | elif isinstance(self._value, Args): 501 | return self._value[self._return_index]._construct(constructor) 502 | else: 503 | raise nip.constructor.ConstructorError(self, (), {}, "Unexpected iter value type") 504 | 505 | def _dump(self, dumper: nip.dumper.Dumper): 506 | if self._return_index == -1: 507 | raise nip.dumper.DumpError("Dumping an iterator but index was not specified by IterParser") 508 | if isinstance(self._value, list): 509 | return str(self._value[self._return_index]) 510 | elif isinstance(self._value, Args): 511 | return self._value[self._return_index]._dump(dumper) 512 | else: 513 | raise nip.dumper.DumpError("Unable to dump Iterable node: unexpected value type") 514 | 515 | 516 | class InlinePython(Node): 517 | @classmethod 518 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[InlinePython, None]: 519 | read_tokens = stream.peek(tokens.InlinePython) 520 | if read_tokens is None: 521 | return None 522 | stream.step() 523 | exec_string = read_tokens[0]._value 524 | return InlinePython(value=exec_string) 525 | 526 | def _construct(self, constructor: nip.constructor.Constructor): 527 | nsc.preload_vars(self._value, constructor) 528 | locals().update(constructor.vars) 529 | return eval(self._value) 530 | 531 | def _dump(self, dumper: nip.dumper.Dumper): 532 | return f"`{self._value}`" 533 | 534 | def to_python(self): 535 | return f"`{self._value}`" 536 | 537 | 538 | class Nothing(Node): 539 | @classmethod 540 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[Nothing, None]: 541 | if not stream: 542 | return Nothing() 543 | 544 | indent = stream.pos 545 | if stream.pos == 0 or (stream.lines[stream.n][: stream.pos].isspace() and indent <= parser.last_indent): 546 | return Nothing() 547 | 548 | def _construct(self, constructor: nip.constructor.Constructor): 549 | return self 550 | 551 | def _dump(self, dumper: nip.dumper.Dumper): 552 | return "" 553 | 554 | def to_python(self): 555 | return None 556 | 557 | 558 | class FString(Node): # Includes f-string and r-string 559 | @classmethod 560 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[FString, None]: 561 | read_tokens = stream.peek(tokens.PythonString) 562 | if read_tokens is None: 563 | return None 564 | stream.step() 565 | string, t = read_tokens[0]._value 566 | if t == "r": 567 | print( 568 | "Warning: all strings in NIP are already python r-string. " "You don't have to explicitly specify it." 569 | ) 570 | return FString(value=string) 571 | 572 | def _construct(self, constructor: nip.constructor.Constructor): 573 | nsc.preload_vars(f"f{self._value}", constructor) 574 | locals().update(constructor.vars) 575 | return eval(f"f{self._value}") 576 | 577 | def _dump(self, dumper: nip.dumper.Dumper): 578 | return f"f{self._value}" 579 | 580 | def to_python(self): 581 | return f"f{self._value}" 582 | 583 | 584 | class Directive(Node): 585 | @classmethod 586 | def read(cls, stream: nip.stream.Stream, parser: nip.parser.Parser) -> Union[FString, None]: 587 | read_tokens = stream.peek(tokens.Operator("!!"), tokens.Name) 588 | if read_tokens is None: 589 | return None 590 | name = read_tokens[1]._value 591 | stream.step() 592 | 593 | value = read_node(stream, parser) 594 | 595 | return nip.directives.call_directive(name, value, stream) 596 | 597 | 598 | def read_node(stream: nip.stream.Stream, parser: nip.parser.Parser) -> Node: 599 | value = ( 600 | Directive.read(stream, parser) 601 | or LinkCreation.read(stream, parser) 602 | or Link.read(stream, parser) 603 | or Class.read(stream, parser) 604 | or Tag.read(stream, parser) 605 | or Iter.read(stream, parser) 606 | or Args.read(stream, parser) 607 | or FString.read(stream, parser) 608 | or Nothing.read(stream, parser) 609 | or InlinePython.read(stream, parser) 610 | or Value.read(stream, parser) 611 | ) 612 | 613 | if value is None: 614 | raise nip.parser.ParserError(stream, "Wrong right value") 615 | 616 | return value 617 | -------------------------------------------------------------------------------- /nip/iter_parser.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from itertools import product 3 | from typing import Iterable 4 | 5 | from .elements import Node 6 | from .parser import Parser 7 | 8 | 9 | class IterParser: # mb: insert this functionality into Parser (save parsed tree in Parser) 10 | def __init__(self, parser: Parser, element: Node = None): 11 | self.iterators = parser.iterators 12 | self.element = element 13 | 14 | def iter_configs(self, element: Node) -> Iterable[Node]: 15 | iter_groups = defaultdict(list) 16 | for i, iterator in enumerate(self.iterators): 17 | name = iterator._name if iterator._name else f"_{i}" 18 | iter_groups[name].append(iterator) 19 | for group_name, group in iter_groups.items(): 20 | iter_len = len(group[0]._value) 21 | for iterator in group: 22 | if len(iterator._value) != iter_len: 23 | raise IterParserError(f"Iterators of group '{group_name}' have different lengths") 24 | 25 | group_names = sorted(iter_groups.keys()) 26 | group_lengths = [len(iter_groups[name][0]._value) for name in group_names] 27 | index_sets = product(*(range(length) for length in group_lengths)) 28 | 29 | for indexes in index_sets: 30 | for index, group_name in zip(indexes, group_names): 31 | for iterator in iter_groups[group_name]: 32 | iterator._return_index = index 33 | yield element 34 | 35 | def __iter__(self): 36 | if self.element is None: 37 | raise IterParserError("config element to iterate through was not defined in __init__") 38 | return self.iter_configs(self.element) 39 | 40 | 41 | class IterParserError(Exception): 42 | pass 43 | -------------------------------------------------------------------------------- /nip/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union, Any, Iterable, Callable, Optional 3 | 4 | from . import elements 5 | from .constructor import Constructor 6 | from .convertor import Convertor 7 | from .dumper import Dumper 8 | from .iter_parser import IterParser 9 | from .non_seq_constructor import NonSequentialConstructor 10 | from .parser import Parser 11 | 12 | __all__ = [ 13 | "parse", 14 | "parse_string", 15 | "construct", 16 | "load", 17 | "load_string", 18 | "run", 19 | "dump", 20 | "dump_string", 21 | "convert", 22 | ] 23 | 24 | 25 | def parse( 26 | path: Union[str, Path], 27 | always_iter: bool = False, 28 | implicit_fstrings: bool = True, 29 | strict: bool = False, 30 | ) -> Union[elements.Node, Iterable[elements.Node]]: 31 | """Parses config providing Element tree 32 | 33 | Parameters 34 | ---------- 35 | path: str or Path 36 | path to config file 37 | always_iter: bool 38 | If True will always return iterator over configs. 39 | implicit_fstrings: boot, default: True 40 | If True, all quoted strings will be treated as python f-strings. 41 | strict: 42 | It True, checks overwriting dict keys and positioning (`args` before `kwargs`). 43 | 44 | Returns 45 | ------- 46 | tree: Element or Iterable[Element] 47 | """ 48 | parser = Parser(implicit_fstrings=implicit_fstrings, strict=strict) 49 | tree = parser.parse(path) 50 | if parser.has_iterators() or always_iter: 51 | return IterParser(parser).iter_configs(tree) 52 | return tree 53 | 54 | 55 | def parse_string( 56 | config_string: str, 57 | always_iter: bool = False, 58 | implicit_fstrings: bool = True, 59 | strict: bool = False, 60 | ) -> Union[elements.Node, Iterable[elements.Node]]: 61 | """Parses config providing Element tree 62 | 63 | Parameters 64 | ---------- 65 | config_string: str 66 | Config as a string. 67 | always_iter: bool 68 | If True will always return iterator over configs. 69 | implicit_fstrings: boot, default: True 70 | If True, all quoted strings will be treated as python f-strings. 71 | strict: 72 | It True, checks overwriting dict keys and positioning (`args` before `kwargs`). 73 | 74 | Returns 75 | ------- 76 | tree: Element or Iterable[Element] 77 | """ 78 | parser = Parser(implicit_fstrings=implicit_fstrings, strict=strict) 79 | tree = parser.parse_string(config_string) 80 | if parser.has_iterators() or always_iter: 81 | return IterParser(parser).iter_configs(tree) 82 | return tree 83 | 84 | 85 | def construct( 86 | config: elements.Node, 87 | base_config: elements.Node = None, 88 | strict_typing: bool = False, 89 | nonsequential: bool = True, 90 | ) -> Any: 91 | """Constructs python object based on config and known nip-objects 92 | 93 | Parameters 94 | ---------- 95 | config: Element 96 | Config node to be constructed. 97 | base_config: 98 | Base config, in case of construction a part of a config with external links. 99 | strict_typing: 100 | If True, raises Exception when typing mismatch. 101 | nonsequential: 102 | If True, allows to use links before creation. 103 | Always true if base_config is specified. 104 | 105 | Returns 106 | ------- 107 | obj: Any 108 | """ 109 | if nonsequential or base_config is not None: 110 | base_config = base_config or config._get_root() 111 | constructor = NonSequentialConstructor(base_config, strict_typing=strict_typing) 112 | else: 113 | constructor = Constructor(strict_typing=strict_typing) 114 | return constructor.construct(config) 115 | 116 | 117 | def _iter_load(configs, strict_typing, nonsequential): # Otherwise load() will always be an iterator 118 | for config in configs: 119 | yield construct(config, strict_typing=strict_typing, nonsequential=nonsequential) 120 | 121 | 122 | def load( 123 | path: Union[str, Path], 124 | always_iter: bool = False, 125 | strict: bool = False, 126 | nonsequential: bool = False, 127 | ) -> Union[Any, Iterable[Any]]: 128 | """Parses config and constructs python object 129 | Parameters 130 | ---------- 131 | path: str or Path 132 | Path to config file 133 | always_iter: bool 134 | If True will always return iterator over configs. 135 | strict: 136 | If True, raises Exception when typing mismatch or overwriting dict key. 137 | nonsequential: 138 | If True, allows to use links before creation. 139 | 140 | Returns 141 | ------- 142 | obj: Any or Iterable[Any] 143 | """ 144 | config = parse(path, always_iter, strict=strict) 145 | 146 | if isinstance(config, Iterable): 147 | return _iter_load(config, strict, nonsequential) 148 | 149 | return construct(config, strict_typing=strict, nonsequential=nonsequential) 150 | 151 | 152 | def load_string( 153 | config_string: str, 154 | always_iter: bool = False, 155 | strict: bool = False, 156 | nonsequential: bool = False, 157 | ) -> Union[Any, Iterable[Any]]: 158 | """Parses config and constructs python object 159 | Parameters 160 | ---------- 161 | config_string: str 162 | Config as a string. 163 | always_iter: bool 164 | If True will always return iterator over configs. 165 | strict: 166 | If True, raises Exception when typing mismatch or overwriting dict key. 167 | nonsequential: 168 | If True, allows to use links before creation. 169 | 170 | Returns 171 | ------- 172 | obj: Any or Iterable[Any] 173 | """ 174 | config = parse_string(config_string, always_iter, strict=strict) 175 | 176 | if isinstance(config, Iterable): 177 | return _iter_load(config, strict, nonsequential) 178 | 179 | return construct(config, strict_typing=strict, nonsequential=nonsequential) 180 | 181 | 182 | def dump(path: Union[str, Path], obj: Union[elements.Node, object]): 183 | """Dumps config tree to file. 184 | 185 | Parameters 186 | ---------- 187 | path: str or Path 188 | Path to save the config. 189 | obj: Element or object 190 | Read or generated config if Element. In case of any other object `convert` will be called. 191 | """ 192 | if not isinstance(obj, elements.Node): 193 | obj = convert(obj) 194 | # mb: wrap with Document to ensure getting `---` at the beginning of the file. 195 | dumper = Dumper() 196 | dumper.dump(path, obj) 197 | 198 | 199 | def dump_string(obj: Union[elements.Node, object]) -> str: 200 | """Dumps config tree to string. 201 | 202 | Parameters 203 | ---------- 204 | obj: Element 205 | Read or generated config tree. 206 | 207 | Returns 208 | ------- 209 | string: str 210 | Dumped element as a string. 211 | """ 212 | if not isinstance(obj, elements.Node): 213 | obj = convert(obj) 214 | dumper = Dumper() 215 | return dumper.dumps(obj) 216 | 217 | 218 | def convert(obj): 219 | """Converts object to nip.Element 220 | 221 | Parameters 222 | ---------- 223 | obj: nip-serializable object. 224 | 225 | Returns 226 | ------- 227 | Element: 228 | Object representation as Nip.Element 229 | 230 | Notes 231 | ----- 232 | Any standard python objects that can be casted to and from nip are supported. 233 | For custom classes method `__nip__` that casts them to the standard object are needed. 234 | Anything that is returned from `__nip__` method will be recursively converted. 235 | `__nip__` method is expected to return everything needed for object construction with __init__ 236 | or any other used constructor. 237 | By default, tag specified in @nip or default class name otherwise will be used as a tag. 238 | """ 239 | convertor = Convertor() 240 | return convertor.convert(obj) 241 | 242 | 243 | def _run_return(value, config, return_values, return_configs): 244 | return_tuple = [] 245 | if return_values: 246 | return_tuple.append(value) 247 | if return_configs: 248 | return_tuple.append(config) 249 | if len(return_tuple) == 0: 250 | return None 251 | if len(return_tuple) == 1: 252 | return return_tuple[0] 253 | return tuple(return_tuple) 254 | 255 | 256 | def _single_run( 257 | config, 258 | func, 259 | verbose, 260 | return_values, 261 | return_configs, 262 | config_parameter, 263 | strict, 264 | nonsequential, 265 | ): 266 | if verbose: 267 | print("=" * 20) 268 | print("Running config:") 269 | print(dump_string(config)) 270 | print("----") 271 | 272 | value = construct(config, strict, nonsequential) 273 | if func is not None: 274 | if isinstance(value, tuple) and isinstance(value[0], list) and isinstance(value[1], dict): 275 | args, kwargs = value 276 | elif isinstance(value, list): 277 | args, kwargs = value, {} 278 | elif isinstance(value, dict): 279 | args, kwargs = [], value 280 | else: 281 | raise RuntimeError("Value constructed by the config cant be parsed as args and kwargs") 282 | 283 | if config_parameter: 284 | if config_parameter in kwargs: 285 | raise RuntimeWarning( 286 | f"nip.run() was asked to add config parameter '{config_parameter}', " 287 | f"but it is already specified by the config. It will be overwritten." 288 | ) 289 | kwargs[config_parameter] = config 290 | 291 | value = func(*args, **kwargs) 292 | 293 | if verbose: 294 | print("----") 295 | print("Run value:") 296 | print(value) 297 | 298 | return _run_return(value, config, return_values, return_configs) 299 | 300 | 301 | def _iter_run( 302 | configs, 303 | func, 304 | verbose, 305 | return_values, 306 | return_configs, 307 | config_parameter, 308 | strict, 309 | nonsequential, 310 | ): 311 | for config in configs: 312 | run_return = _single_run( 313 | config, 314 | func, 315 | verbose, 316 | return_values, 317 | return_configs, 318 | config_parameter, 319 | strict, 320 | nonsequential, 321 | ) 322 | if run_return: 323 | yield run_return 324 | 325 | 326 | def run( 327 | path: Union[str, Path], 328 | func: Optional[Callable] = None, 329 | verbose: bool = True, 330 | strict: bool = False, 331 | nonsequential: bool = False, 332 | return_values: bool = True, 333 | return_configs: bool = False, 334 | always_iter: bool = False, 335 | config_parameter: Optional[str] = None, 336 | ): 337 | """Runs config. Config should be declared with function to run as a tag for the Document. 338 | In case of iterable configs we will iterate over and run each of them. 339 | 340 | Parameters 341 | ---------- 342 | path: str or Path 343 | path to config to run. 344 | func: 345 | Function to be called with loaded configs. 346 | If not specified, config will be constructed as is. 347 | verbose: bool, optional 348 | Whether to print information about currently running experiment or not. 349 | strict: 350 | If True, raises Exception when typing mismatch or overwriting dict key. 351 | nonsequential: 352 | If True, allows to use links before creation. 353 | return_values: bool, optional 354 | Whether to return values of ran functions or not. 355 | return_configs: bool, optional 356 | Whether to return config for ran function or not. 357 | always_iter: bool, optional 358 | Result will always be iterable. 359 | config_parameter: str, optional 360 | If specified, parsed config will be passed to called function as a parameter with this name. 361 | `func` parameter must be specified. 362 | 363 | Returns 364 | ------- 365 | Iterator or single value depending on whether the config is iterable 366 | value or config or (value, config): Any or str or (Any, str) 367 | returned values and configs of runs 368 | """ 369 | assert ( 370 | config_parameter is None or config_parameter is not None and func is not None 371 | ), "`config_parameter` can be used only with specified `func`" 372 | config = parse(path, always_iter=always_iter, strict=strict) 373 | if isinstance(config, Iterable): 374 | return list( 375 | _iter_run( 376 | config, 377 | func, 378 | verbose, 379 | return_values, 380 | return_configs, 381 | config_parameter, 382 | strict, 383 | nonsequential, 384 | ) 385 | ) # mb iter? 386 | 387 | return _single_run( 388 | config, 389 | func, 390 | verbose, 391 | return_values, 392 | return_configs, 393 | config_parameter, 394 | strict, 395 | nonsequential, 396 | ) 397 | -------------------------------------------------------------------------------- /nip/non_seq_constructor.py: -------------------------------------------------------------------------------- 1 | import symtable 2 | 3 | import nip.elements 4 | from .constructor import Constructor 5 | 6 | 7 | class VarsDict: 8 | def __init__(self, constructor): 9 | super().__init__() 10 | self.constructor = constructor 11 | self.vars = {} 12 | self.in_progress = set() 13 | 14 | def __getitem__(self, item): 15 | if item in self.vars: 16 | return self.vars[item] 17 | if item not in self.constructor.links: 18 | raise NonSequentialConstructorError(f"Unresolved link '{item}'") 19 | if item in self.in_progress: 20 | raise NonSequentialConstructorError(f"Recursive construction of '{item}'.") 21 | self.in_progress.add(item) 22 | self.constructor.links[item]._construct(self.constructor) 23 | self.in_progress.remove(item) 24 | return self.vars[item] 25 | 26 | def __setitem__(self, key, value): 27 | if key not in self.vars: # was not constructed earlier 28 | self.vars[key] = value 29 | 30 | def __iter__(self): 31 | return iter(self.vars) 32 | 33 | def items(self): 34 | return self.vars.items() 35 | 36 | def keys(self): 37 | return self.vars.keys() 38 | 39 | 40 | class NonSequentialConstructor(Constructor): 41 | def __init__( 42 | self, 43 | base_config: "nip.elements.Node", 44 | ignore_rewriting=False, 45 | load_builders=True, 46 | strict_typing=False, 47 | ): 48 | super().__init__(ignore_rewriting, load_builders, strict_typing) 49 | self.vars = VarsDict(self) 50 | self.links = {} 51 | self._find_links(base_config) 52 | 53 | def _find_links(self, node: "nip.elements.Node"): 54 | if isinstance(node, nip.elements.LinkCreation): 55 | assert node._name not in self.links, "Redefined link." 56 | self.links[node._name] = node 57 | if isinstance(node, nip.elements.Args): 58 | for sub_node in node: 59 | self._find_links(sub_node) 60 | if isinstance(node._value, nip.elements.Node): 61 | self._find_links(node._value) 62 | 63 | 64 | class NonSequentialConstructorError(Exception): 65 | def __init__(self, massage): 66 | self.massage = massage 67 | 68 | def __str__(self): 69 | return self.massage 70 | 71 | 72 | def preload_vars(code, constructor: Constructor): 73 | if not isinstance(constructor, NonSequentialConstructor): 74 | return 75 | table = symtable.symtable(code, "string", "exec") 76 | for name in constructor.links: 77 | try: 78 | if table.lookup(name).is_global(): 79 | constructor.vars[name] 80 | except KeyError: 81 | pass 82 | 83 | 84 | def should_construct(name, constructor: Constructor): 85 | if isinstance(constructor, NonSequentialConstructor): 86 | return not (name in constructor.vars) 87 | return True # we can reconstruct 88 | -------------------------------------------------------------------------------- /nip/parser.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import nip.elements as elements 5 | from .stream import Stream 6 | 7 | 8 | class Parser: 9 | def __init__( 10 | self, 11 | implicit_fstrings: bool = True, 12 | strict: bool = False, 13 | sequential_links: bool = False, 14 | ): 15 | self.links = [] 16 | self.iterators = [] 17 | self.link_replacements = {} # used with !!insert directive 18 | self.implicit_fstrings = implicit_fstrings 19 | self.strict = strict 20 | self.sequential_links = sequential_links 21 | self.last_indent = -1 22 | self.stack = [] 23 | 24 | def parse(self, path: Union[str, Path]): 25 | path = Path(path) 26 | with path.open() as f_stream: 27 | string_representation = f_stream.read() 28 | 29 | tree = self.parse_string(string_representation) 30 | tree._path = path 31 | return tree 32 | 33 | def __call__(self, path: Union[str, Path]): 34 | return self.parse(path) 35 | 36 | def parse_string(self, string): 37 | stream = Stream(string) # mb: add stream to parser and log errors more convenient 38 | tree = elements.Document.read(stream, self) 39 | if stream: 40 | raise ParserError(stream, "Wrong statement.") 41 | tree._update_parents() 42 | return tree 43 | 44 | def has_iterators(self) -> bool: 45 | return len(self.iterators) > 0 46 | 47 | 48 | class ParserError(Exception): 49 | def __init__(self, stream: Stream, msg: str): 50 | self.line = stream.n 51 | self.pos = stream.pos 52 | self.msg = msg 53 | 54 | def __str__(self): 55 | return f"{self.line + 1}:{self.pos + 1}: {self.msg}" 56 | -------------------------------------------------------------------------------- /nip/stream.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Type 2 | 3 | import nip.tokens as tokens 4 | 5 | 6 | class Stream: 7 | def __init__(self, sstream: str): 8 | self.lines = sstream.split("\n") 9 | self.lines = [line + " " for line in self.lines] 10 | self.n = 0 11 | self.pos = 0 12 | self.last_peak_pos = -1 13 | self._pass_forward() 14 | 15 | def peek(self, *args: Union[tokens.Token, Type[tokens.Token]]): 16 | """Reads several tokens from stream""" 17 | if not self: 18 | return None 19 | line = self.lines[self.n] 20 | pos = self.pos 21 | self.last_peak_pos = -1 # prevent step() after failed peek() 22 | read_tokens = [] 23 | for arg in args: 24 | if isinstance(arg, tokens.Token): 25 | token_type = arg.__class__ 26 | else: 27 | token_type = arg 28 | 29 | # skip empty line ending 30 | while pos < len(line) and line[pos].isspace(): 31 | pos += 1 32 | if pos >= len(line): 33 | return None 34 | 35 | try: 36 | length, token = token_type.read(line[pos:]) 37 | # mb: pass full stream to token. (This will allow multiline string parsing) 38 | except tokens.TokenError as e: 39 | raise StreamError(self.n, pos, e) 40 | 41 | if token is None: 42 | return None 43 | if isinstance(arg, tokens.Token) and token != arg: 44 | return None 45 | 46 | token.set_position(self.n, pos) 47 | read_tokens.append(token) 48 | pos += length 49 | 50 | self.last_peak_pos = pos 51 | return read_tokens 52 | 53 | def step(self): 54 | assert self.last_peak_pos > 0, "step() called before peaking any Token" 55 | self.pos = self.last_peak_pos 56 | self._pass_forward() 57 | 58 | def _pass_forward(self): 59 | while self and ( 60 | self.pos >= len(self.lines[self.n]) 61 | or self.lines[self.n][self.pos :].isspace() 62 | or self.lines[self.n][self.pos :].strip()[0] == "#" 63 | ): 64 | self.n += 1 65 | self.pos = 0 66 | 67 | if not self: 68 | return 69 | 70 | while self.lines[self.n][self.pos].isspace(): 71 | self.pos += 1 72 | 73 | def __bool__(self): 74 | return self.n < len(self.lines) 75 | 76 | 77 | class StreamError(Exception): 78 | def __init__(self, line: int, position: int, msg: Exception): 79 | self.line = line 80 | self.pos = position 81 | self.msg = msg 82 | 83 | def __str__(self): 84 | return f"{self.line + 1}:{self.pos + 1}: {self.msg}" 85 | -------------------------------------------------------------------------------- /nip/tokens.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import abstractmethod, ABC 4 | from typing import Tuple, Any, Union 5 | 6 | 7 | class Token(ABC): 8 | """Abstract of token reader""" 9 | 10 | def __init__(self, value: Any = None): 11 | self._value = value 12 | 13 | @staticmethod 14 | @abstractmethod 15 | def read(stream: str) -> Tuple[int, Union[None, Token]]: 16 | pass 17 | 18 | def set_position(self, line, pos): 19 | self.line = line 20 | self.pos = pos 21 | 22 | def __eq__(self, other: Token): 23 | return self.__class__ == other.__class__ and self._value == other._value 24 | 25 | @property 26 | def _name(self): 27 | return self.__class__.__name__ 28 | 29 | 30 | class TokenError(Exception): 31 | def __init__(self, msg: str): 32 | self.msg = msg 33 | 34 | def __str__(self): 35 | return f"{self.msg}" 36 | 37 | 38 | class Number(Token): 39 | @staticmethod 40 | def read(stream: str) -> Tuple[int, Union[None, Number]]: 41 | string = stream[: stream.find(" #")].strip() # ignore comment 42 | value = None 43 | for t in (int, float): 44 | try: 45 | value = t(string) 46 | break 47 | except: 48 | pass 49 | if value is not None: 50 | return len(string), Number(value) 51 | else: 52 | return 0, None 53 | 54 | @staticmethod 55 | def _read_number_string(stream: str): 56 | string_number = "" 57 | for c in stream[:].strip(): 58 | if c.isnumeric(): 59 | string_number += c 60 | continue 61 | if c == "-" and (len(string_number) == 0 or string_number[-1] == "e"): 62 | string_number += c 63 | continue 64 | if c in ["e", "."] and c not in string_number: 65 | string_number += c 66 | continue 67 | break 68 | return string_number 69 | 70 | 71 | class Bool(Token): 72 | @staticmethod 73 | def read(stream: str) -> Tuple[int, Union[None, Bool]]: 74 | string = stream[: stream.find(" #")].strip() # ignore comment 75 | if string in ["true", "True", "yes"]: 76 | return len(string), Bool(True) 77 | if string in ["false", "False", "no"]: 78 | return len(string), Bool(False) 79 | return 0, None 80 | 81 | 82 | # class NoneType(Token): 83 | # @staticmethod 84 | # def read(stream: str) -> Tuple[int, Any]: 85 | # string = stream[:strip] 86 | 87 | 88 | class String(Token): 89 | @staticmethod 90 | def read(stream: str) -> Tuple[int, Union[None, String]]: 91 | for op in Operator.operators: 92 | if stream.startswith(op): 93 | return 0, None 94 | pos = 0 95 | if stream[pos] in "'\"": 96 | start_char = stream[pos] 97 | pos += 1 98 | while stream[pos] and stream[pos] != start_char: 99 | pos += 1 100 | if stream[pos] != start_char: 101 | raise TokenError("Not closed string expression") 102 | pos += 1 103 | return pos, String(stream[1 : pos - 1]) 104 | 105 | pos = len(stream) 106 | for op in ["#"]: # mb: other operators stops string. (hard to raise good exception) 107 | found_pos = stream.find(op) 108 | if found_pos >= 0: 109 | pos = min(pos, found_pos) 110 | 111 | if pos == 0: 112 | return 0, None 113 | return pos, String(stream[:pos].strip()) 114 | 115 | 116 | class Name(String): 117 | @staticmethod 118 | def read(stream: str) -> Tuple[int, Union[None, Name]]: 119 | pos = 0 120 | if not stream[pos].isalpha(): 121 | return 0, None 122 | 123 | while pos < len(stream) and (stream[pos].isalnum() or stream[pos] == "_" or stream[pos] == "."): 124 | pos += 1 125 | 126 | return pos, Name(stream[:pos]) 127 | 128 | 129 | class Operator(Token): 130 | operators = ["---", "@", "#", "&", "!&", "!!", "!", "- ", ": ", "*", "{", "}", "[", "]", "(", ")"] 131 | 132 | @staticmethod 133 | def read(stream: str) -> Tuple[int, Union[None, Operator]]: 134 | for op in Operator.operators: 135 | if stream.startswith(op): 136 | return len(op), Operator(op) 137 | return 0, None 138 | 139 | 140 | class Indent(Token): 141 | @staticmethod 142 | def read(stream: str) -> Tuple[int, Union[None, Indent]]: 143 | indent = 0 144 | pos = 0 145 | while stream[pos].isspace(): 146 | if stream[pos] == " ": 147 | indent += 1 148 | elif stream[pos] == "\t": 149 | indent += 4 150 | else: 151 | raise TokenError("Unknown indent symbol") 152 | pos += 1 153 | 154 | return pos, Indent(indent) 155 | 156 | 157 | class List(Token): 158 | @staticmethod 159 | def read(stream: str) -> Tuple[int, Union[None, List]]: 160 | pos = 0 161 | if stream[pos] != "[": 162 | return pos, None 163 | while stream and stream[pos] != "]": 164 | pos += 1 165 | if stream[pos] != "]": 166 | raise TokenError("List was not closed") 167 | pos += 1 168 | read_list = eval(stream[:pos]) 169 | return pos, List(read_list) 170 | 171 | 172 | class TupleToken(Token): 173 | @staticmethod 174 | def read(stream: str) -> Tuple[int, Union[None, List]]: 175 | pos = 0 176 | if stream[pos] != "(": 177 | return pos, None 178 | while stream and stream[pos] != ")": 179 | pos += 1 180 | if stream[pos] != ")": 181 | raise TokenError("Tuple was not closed") 182 | pos += 1 183 | read_list = eval(stream[:pos]) 184 | return pos, List(read_list) 185 | 186 | 187 | class Dict(Token): 188 | @staticmethod 189 | def read(stream: str) -> Tuple[int, Union[None, Dict]]: 190 | pos = 0 191 | if stream[pos] != "{": 192 | return pos, None 193 | while stream and stream[pos] != "}": 194 | pos += 1 195 | if stream[pos] != "}": 196 | raise TokenError("Dict was not closed") 197 | pos += 1 198 | read_dict = eval(stream[:pos]) 199 | return pos, Dict(read_dict) 200 | 201 | 202 | class InlinePython(Token): 203 | @staticmethod 204 | def read(stream: str) -> Tuple[int, Union[None, InlinePython]]: 205 | pos = 0 206 | if stream[pos] != "`": 207 | return pos, None 208 | pos += 1 209 | while stream and stream[pos] != "`": 210 | pos += 1 211 | if stream[pos] != "`": 212 | raise TokenError("Inline python string was not clothed") 213 | pos += 1 214 | return pos, InlinePython(stream[1 : pos - 1]) 215 | 216 | 217 | class PythonString(Token): 218 | @classmethod 219 | def read(cls, stream: str, implicit_fstrings: bool = False) -> Tuple[int, Union[None, PythonString]]: 220 | string = stream[:].strip() 221 | if implicit_fstrings and string[0] in "\"'": 222 | if string[-1] != string[0]: 223 | raise TokenError("Not closed f-string") 224 | return len(string), PythonString((string, "f")) 225 | if string[0] == "f" and string[1] in "\"'": 226 | if string[-1] != string[1]: 227 | raise TokenError("Not closed f-string") 228 | return len(string), PythonString((string[1:], "f")) 229 | if string[0] == "r" and string[1] in "\"'": 230 | if string[-1] != string[1]: 231 | raise TokenError("Not closed r-string") 232 | return len(string), PythonString((string[1:], "r")) 233 | return 0, None 234 | -------------------------------------------------------------------------------- /nip/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import List, Dict, Union, Any 3 | 4 | import typeguard 5 | 6 | 7 | def get_subclasses(cls): 8 | return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in get_subclasses(c)]) 9 | 10 | 11 | def get_sub_dict(base_cls): 12 | return {cls.__name__: cls for cls in get_subclasses(base_cls)} 13 | 14 | 15 | def deep_equals(first: Union[Dict, List, Any], second: Union[Dict, List, Any]): 16 | if type(first) is type(second): 17 | return False 18 | if isinstance(first, dict): 19 | for key in first: 20 | if key not in second: 21 | return False 22 | if not deep_equals(first[key], second[key]): 23 | return False 24 | elif isinstance(first, list): 25 | if len(first) != len(second): 26 | return False 27 | for i, j in zip(first, second): 28 | if not deep_equals(i, j): 29 | return False 30 | else: 31 | return first == second 32 | return True 33 | 34 | 35 | def iterate_items(obj): 36 | if isinstance(obj, (list, tuple)): 37 | for i, value in enumerate(obj): 38 | yield i, value 39 | if isinstance(obj, dict): 40 | for key, value in obj.items(): 41 | yield key, value 42 | 43 | 44 | def flatten(obj, delimiter=".", keys=()): 45 | if not isinstance(obj, (list, tuple, dict)): 46 | return {delimiter.join(keys): obj} 47 | 48 | result = {} 49 | for key, value in iterate_items(obj): 50 | result.update(flatten(value, delimiter, keys=keys + (str(key),))) 51 | return result 52 | 53 | 54 | def check_typing(func, args, kwargs) -> List[str]: 55 | try: 56 | signature = inspect.signature(func) 57 | except ValueError: # unable to load function signature (common case for builtin functions) 58 | return [] 59 | messages = [] 60 | typeguard.typechecked() 61 | if len(args) > len(signature.parameters.values()): 62 | messages.append("Too many arguments") 63 | for arg, param in zip(args, signature.parameters.values()): 64 | print(arg, param) 65 | if param.annotation is inspect.Parameter.empty: 66 | continue 67 | try: 68 | typeguard.check_type(arg, param.annotation) 69 | except typeguard.TypeCheckError as e: 70 | messages.append(f"{param.name}: {e}") 71 | 72 | for name, value in kwargs.items(): 73 | if name not in signature.parameters: 74 | continue # handled by python 75 | annotation = signature.parameters[name].annotation 76 | # print(name, annotation, value) 77 | if annotation is inspect.Parameter.empty: 78 | continue 79 | try: 80 | typeguard.check_type(value, annotation) 81 | except typeguard.TypeCheckError as e: 82 | messages.append(f"{name}: {e}") 83 | 84 | return messages 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = nip-config 3 | version = 0.11.0 4 | author = Ilya Vasiliev 5 | author_email = spairet@bk.ru 6 | description = Advanced configs for python 7 | long_description = 8 | NIP is a yaml-like config files parser for convenient python object construction and experiment holding. 9 | Short guide can be found here: https://github.com/spairet/nip/tree/main/doc 10 | long_description_content_type = text/markdown 11 | url = https://github.com/spairet/nip 12 | project_urls = 13 | Guide = https://github.com/spairet/nip/tree/main/doc 14 | Issues = https://github.com/spairet/nip/issues 15 | classifiers = 16 | Programming Language :: Python :: 3 17 | License :: OSI Approved :: MIT License 18 | Operating System :: OS Independent 19 | 20 | [options] 21 | packages = find: 22 | python_requires = >=3.7.4 23 | install_requires = 24 | typeguard >= 3.0.0 25 | -------------------------------------------------------------------------------- /tests/base_tests/comments/configs/comment.nip: -------------------------------------------------------------------------------- 1 | #start comment 2 | 3 | main: &asd # comment after var creationg 4 | number: 1 #first_inline 5 | float: 0.314159265 #second inline comment 6 | modern_float: 3e-5 #comment after modern float 7 | string: this is some string 123^& ! #aaaand third one 8 | bool: True #4th simple comment 9 | quated_string: " !&and another one string 123 'qwe' @#!*^@%# " #and a comment of cause 10 | 11 | other: *asd # comment after var usage 12 | 13 | 14 | #middle comment block 15 | # 16 | # 17 | # still comment block 18 | #################################### 19 | #######COOL####COMMENT############## 20 | #################################### 21 | #the end of comment block 22 | 23 | 24 | obj: !SimpleClass # comment after tag 25 | name: "Ilya" 26 | 27 | 28 | 29 | # just a comment at the end -------------------------------------------------------------------------------- /tests/base_tests/comments/configs/empty_comment.nip: -------------------------------------------------------------------------------- 1 | #comment -------------------------------------------------------------------------------- /tests/base_tests/comments/test_comments.py: -------------------------------------------------------------------------------- 1 | from utils.test_utils import IS_NOTHING 2 | 3 | 4 | def test_comment(): 5 | from utils import builders 6 | from nip import load 7 | 8 | res = load("base_tests/comments/configs/comment.nip") 9 | assert isinstance(res, dict) 10 | assert "main" in res 11 | assert res["main"] == { 12 | "number": 1, 13 | "float": 0.314159265, 14 | "modern_float": 3e-5, 15 | "string": "this is some string 123^& !", 16 | "bool": True, 17 | "quated_string": " !&and another one string 123 'qwe' @#!*^@%# ", 18 | } 19 | assert "other" in res 20 | assert res["other"] == res["main"] 21 | assert "obj" in res 22 | assert isinstance(res["obj"], builders.SimpleClass) 23 | assert res["obj"].name == "Ilya" 24 | 25 | 26 | def test_empty(): 27 | from nip import load 28 | 29 | res = load("base_tests/comments/configs/empty_comment.nip") 30 | assert IS_NOTHING(res) 31 | -------------------------------------------------------------------------------- /tests/base_tests/document/configs/document.nip: -------------------------------------------------------------------------------- 1 | --- Strange_and_2_long_DocumentName !MyClass 2 | 3 | name: some_name 4 | f: !myfunc 5 | a: 1 6 | b: 2 -------------------------------------------------------------------------------- /tests/base_tests/document/test_document.py: -------------------------------------------------------------------------------- 1 | def test_document(): 2 | from nip import load, parse 3 | from nip.elements import Document 4 | from utils import builders 5 | 6 | parsed = parse("base_tests/document/configs/document.nip") 7 | assert isinstance(parsed, Document) 8 | assert parsed._name == "Strange_and_2_long_DocumentName" 9 | 10 | result = load("base_tests/document/configs/document.nip") 11 | assert isinstance(result, builders.MyClass) 12 | assert result.name == "some_name" 13 | assert result.f == 5 14 | -------------------------------------------------------------------------------- /tests/base_tests/numbers/configs/numbers.nip: -------------------------------------------------------------------------------- 1 | small_number: 213 2 | big_number: 308432049871029384612384762130 3 | huge_number: 21093486304876124023441923746102384712309487123049827340921384761230948721340192384715987338581438161655146683641674865548633466485439541345343465289764254657848565684865368843566578423465642456365847434596874539788567483656875424639687446398773554672123472248137658947390568483685344663284634556745631755624716352845642368644536562741356574366285462345652217346558744566352845641345625473556874264355642345662571536755673557632474573567356783554731452624328753685241693654684783356123322511 4 | 5 | small_negative: -123 6 | big_negative: -10230123984720394862342394 7 | huge_negative: -128937461230847612309847123049812374019238471203948712305986235005982374509835639482756349857643058716340598743205983465423987564398756105879634056731509617508512012649087246938147565102857634958736 8 | 9 | small_float: 1.3 10 | big_float: 43.402983470923847109487 11 | huge_float: 2532423.40923184710239487123094821374091238471209348123740192387420394872150981670923847209348724105987204982374023984712 12 | 13 | small_e: 1e2 14 | big_e: 123e3873 15 | huge_e: 32.323123213298e-323403 16 | -------------------------------------------------------------------------------- /tests/base_tests/numbers/test_numbers.py: -------------------------------------------------------------------------------- 1 | def test_numbers(): 2 | from nip import load 3 | res = load("base_tests/numbers/configs/numbers.nip") 4 | assert res['small_number'] == 213 5 | assert res['big_number'] == 308432049871029384612384762130 6 | assert res['huge_number'] == 21093486304876124023441923746102384712309487123049827340921384761230948721340192384715987338581438161655146683641674865548633466485439541345343465289764254657848565684865368843566578423465642456365847434596874539788567483656875424639687446398773554672123472248137658947390568483685344663284634556745631755624716352845642368644536562741356574366285462345652217346558744566352845641345625473556874264355642345662571536755673557632474573567356783554731452624328753685241693654684783356123322511 7 | 8 | assert res['small_negative'] == -123 9 | assert res['big_negative'] == -10230123984720394862342394 10 | assert res['huge_negative'] == -128937461230847612309847123049812374019238471203948712305986235005982374509835639482756349857643058716340598743205983465423987564398756105879634056731509617508512012649087246938147565102857634958736 11 | 12 | assert res['small_float'] == 1.3 13 | assert res['big_float'] == 43.402983470923847109487 14 | assert res['huge_float'] == 2532423.40923184710239487123094821374091238471209348123740192387420394872150981670923847209348724105987204982374023984712 15 | 16 | assert res['small_e'] == 1e2 17 | assert res['big_e'] == 123e3873 18 | assert res['huge_e'] == 32.323123213298e-323403 19 | -------------------------------------------------------------------------------- /tests/base_tests/raises/configs/wrong_dict_indent.nip: -------------------------------------------------------------------------------- 1 | main: qwe 2 | sd: fdf 3 | 4 | qwe: 123 -------------------------------------------------------------------------------- /tests/base_tests/raises/configs/wrong_item.nip: -------------------------------------------------------------------------------- 1 | main: 2 | sd: fdf 3 | 4 | d 5 | qwe: 123 -------------------------------------------------------------------------------- /tests/base_tests/raises/test_wrong_dicts.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nip import load 4 | from nip.parser import ParserError 5 | 6 | 7 | def test_wrong_indent(): 8 | with pytest.raises(ParserError, match="2:3: Unexpected indent"): 9 | load("base_tests/raises/configs/wrong_dict_indent.nip") 10 | 11 | 12 | def test_wrong_item(): 13 | with pytest.raises(ParserError, match="Wrong statement"): 14 | load("base_tests/raises/configs/wrong_item.nip") 15 | -------------------------------------------------------------------------------- /tests/base_tests/strings/configs/long_strings_config.nip: -------------------------------------------------------------------------------- 1 | double_quote_string: "asdvjhdbvkjdhfbv" 2 | single_quote_string: 'vfduds84l8yv8hvdln4uk' 3 | no_quote_abc_string: vdfvsbdfvklurjflr 4 | no_quote_1a_string: 12d123fhfl494fhfnvnv84lih5 -------------------------------------------------------------------------------- /tests/base_tests/strings/test_strings.py: -------------------------------------------------------------------------------- 1 | def test_different_strings(): 2 | from nip import load 3 | result = load("base_tests/strings/configs/long_strings_config.nip") 4 | expected = { 5 | 'double_quote_string': 'asdvjhdbvkjdhfbv', 6 | 'single_quote_string': 'vfduds84l8yv8hvdln4uk', 7 | 'no_quote_abc_string': 'vdfvsbdfvklurjflr', 8 | 'no_quote_1a_string': '12d123fhfl494fhfnvnv84lih5' 9 | } 10 | assert expected == result 11 | 12 | 13 | if __name__ == "__main__": 14 | test_different_strings() 15 | -------------------------------------------------------------------------------- /tests/base_tests/tags/configs/empty_args.nip: -------------------------------------------------------------------------------- 1 | obj: !def_class 2 | another_one: !def_class 3 | constructed: !def_class smothing more interesting 4 | -------------------------------------------------------------------------------- /tests/base_tests/tags/configs/multi_tag.nip: -------------------------------------------------------------------------------- 1 | first_object: !first_tag 2 | name: abc 3 | 4 | second_object: !second_tag 5 | name: cba 6 | -------------------------------------------------------------------------------- /tests/base_tests/tags/configs/no_args_func_error.nip: -------------------------------------------------------------------------------- 1 | func: !myfunc -------------------------------------------------------------------------------- /tests/base_tests/tags/configs/numpy.nip: -------------------------------------------------------------------------------- 1 | array: !zeros [2, 3, 4] -------------------------------------------------------------------------------- /tests/base_tests/tags/configs/simple_tag_config.nip: -------------------------------------------------------------------------------- 1 | obj: !SimpleClass 2 | name: "Hello World!" 3 | 4 | func: !myfunc 5 | a: 1 6 | c: 2 7 | 8 | single: !myfunc 1 9 | -------------------------------------------------------------------------------- /tests/base_tests/tags/test_tag.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_numpy(): 5 | import numpy as np 6 | from nip import load, nip 7 | 8 | nip(np, wrap_builtins=True) 9 | result = load("base_tests/tags/configs/numpy.nip") 10 | assert len(result.keys()) and "array" in result 11 | assert np.all(result["array"] == np.zeros((2, 3, 4))) 12 | 13 | 14 | def test_simple_class(): 15 | from utils import builders 16 | from nip import load 17 | 18 | result = load("base_tests/tags/configs/simple_tag_config.nip") 19 | assert "obj" in result 20 | assert isinstance(result["obj"], builders.SimpleClass) 21 | assert result["obj"].name == "Hello World!" 22 | 23 | 24 | def test_simple_func(): 25 | from nip import load 26 | 27 | result = load("base_tests/tags/configs/simple_tag_config.nip") 28 | assert "func" in result 29 | assert result["func"] == 7 30 | 31 | 32 | def test_one_line_func(): 33 | from nip import load 34 | 35 | result = load("base_tests/tags/configs/simple_tag_config.nip") 36 | assert "single" in result 37 | assert result["single"] == 1 38 | 39 | 40 | def test_empty_construction(): 41 | from utils import builders 42 | from nip import load 43 | 44 | result = load("base_tests/tags/configs/empty_args.nip") 45 | assert isinstance(result["obj"], builders.ClassWithDefaults) and result["obj"].name == "something" 46 | assert isinstance(result["another_one"], builders.ClassWithDefaults) and result["another_one"].name == "something" 47 | assert ( 48 | isinstance(result["constructed"], builders.ClassWithDefaults) 49 | and result["constructed"].name == "smothing more interesting" 50 | ) 51 | 52 | 53 | def test_no_args(): 54 | from nip import load 55 | 56 | with pytest.raises(TypeError, match="missing 1 required positional argument:"): 57 | load("base_tests/tags/configs/no_args_func_error.nip") 58 | 59 | 60 | def test_multi_tag(): 61 | from utils import builders 62 | from nip import load 63 | 64 | result = load("base_tests/tags/configs/multi_tag.nip") 65 | assert ( 66 | "first_object" in result 67 | and isinstance(result["first_object"], builders.MultiTagClass) 68 | and result["first_object"].name == "abc" 69 | ) 70 | assert ( 71 | "second_object" in result 72 | and isinstance(result["second_object"], builders.MultiTagClass) 73 | and result["second_object"].name == "cba" 74 | ) 75 | -------------------------------------------------------------------------------- /tests/complex/config_dumps/ababra.yaml: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | main: &main 3 | first: 4 | in1: "11" 5 | in2: &float -12.5 6 | second: [1, 2, 3] 7 | third: {'a': '123', '123': 'qweqwe'} 8 | other: 9 | main.other: *main 10 | list: 11 | - f"this is float value {float}" 12 | - True 13 | - 14 | - 15 | sfds: 16 | abra: ababra 17 | - 18 | - "nested value" 19 | - "one more" 20 | - 21 | nested: "dict" 22 | sdf: -------------------------------------------------------------------------------- /tests/complex/config_dumps/avocado.yaml: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | main: &main 3 | first: 4 | in1: "11" 5 | in2: &float -12.5 6 | second: [1, 2, 3] 7 | third: {'a': '123', '123': 'qweqwe'} 8 | other: 9 | main.other: *main 10 | list: 11 | - f"this is float value {float}" 12 | - True 13 | - 14 | - 15 | sfds: 16 | abra: avocado 17 | - 18 | - "nested value" 19 | - "one more" 20 | - 21 | nested: "dict" 22 | sdf: -------------------------------------------------------------------------------- /tests/complex/config_dumps/cadabra.yaml: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | main: &main 3 | first: 4 | in1: "11" 5 | in2: &float -12.5 6 | second: [1, 2, 3] 7 | third: {'a': '123', '123': 'qweqwe'} 8 | other: 9 | main.other: *main 10 | list: 11 | - f"this is float value {float}" 12 | - True 13 | - 14 | - 15 | sfds: 16 | abra: cadabra 17 | - 18 | - "nested value" 19 | - "one more" 20 | - 21 | nested: "dict" 22 | sdf: -------------------------------------------------------------------------------- /tests/complex/configs/config.nip: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | 3 | main: &main 4 | first: 5 | in1: "11" 6 | in2: &float -12.5 7 | second: [1, 2, 3] 8 | 9 | third: {'a': "123", '123': "qweqwe"} 10 | inline_tuple: (17, 32, 42, 'name') 11 | 12 | 13 | other: 14 | main.other: *main 15 | list: # comment 16 | - f"this is float value {float}" 17 | - true 18 | - 19 | # more comments 20 | sdf: 21 | - sfds: 22 | abra: @ 23 | - "cadabra" 24 | - "ababra" 25 | - "avocado" 26 | - - nested value 27 | - one more 28 | - nested: dict -------------------------------------------------------------------------------- /tests/complex/test_complex_example.py: -------------------------------------------------------------------------------- 1 | from utils.test_utils import NOTHING, deep_conditioned_compare 2 | 3 | 4 | def config_replacer(config, name): 5 | config["other"]["list"][1]["sdf"][0]["sfds"]["abra"] = name 6 | 7 | 8 | def test_config(): 9 | from utils import builders 10 | from nip import load, nip 11 | 12 | nip(builders) 13 | result_iter = load("complex/configs/config.nip") 14 | expected_result = { 15 | "main": { 16 | "first": {"in1": "11", "in2": -12.5}, 17 | "second": [1, 2, 3], 18 | "third": {"a": "123", "123": "qweqwe"}, 19 | "inline_tuple": (17, 32, 42, "name"), 20 | }, 21 | "other": { 22 | "main.other": { 23 | "first": {"in1": "11", "in2": -12.5}, 24 | "second": [1, 2, 3], 25 | "third": {"a": "123", "123": "qweqwe"}, 26 | "inline_tuple": (17, 32, 42, "name"), 27 | }, 28 | "list": ( 29 | ["this is float value -12.5", True, NOTHING], 30 | {"sdf": [{"sfds": {"abra": None}}, ["nested value", "one more", {"nested": "dict"}]]}, 31 | ), 32 | }, 33 | } 34 | 35 | iter_values = ["cadabra", "ababra", "avocado"] 36 | for result, iter_value in zip(result_iter, iter_values): 37 | config_replacer(expected_result, iter_value) 38 | assert deep_conditioned_compare(expected_result, result) 39 | -------------------------------------------------------------------------------- /tests/features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spairet/nip/a019ab56284f072d79fd74812b48d370e133e464/tests/features/__init__.py -------------------------------------------------------------------------------- /tests/features/args/configs/args_config.nip: -------------------------------------------------------------------------------- 1 | main: 2 | - arg1 3 | - 321 4 | one: 1 5 | two: 2 6 | three: "three" 7 | 8 | other: 9 | four: 4 10 | ff: five 11 | 12 | func: !show 13 | - 1 14 | c: 10 15 | -------------------------------------------------------------------------------- /tests/features/args/test_args.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_args(): 5 | from nip import load, wrap_module 6 | 7 | wrap_module("utils.builders") 8 | 9 | result = load("features/args/configs/args_config.nip") 10 | expected_result = { 11 | "main": (["arg1", 321], {"one": 1, "two": 2, "three": "three"}), 12 | "other": {"four": 4, "ff": "five"}, 13 | "func": None, 14 | } 15 | 16 | assert result == expected_result 17 | -------------------------------------------------------------------------------- /tests/features/class_construction/configs/class_const_config.nip: -------------------------------------------------------------------------------- 1 | var: &var 123 2 | 3 | main: 4 | just: 'some_stuff' 5 | simple_class: !&SimpleClass 6 | def_class: !&def_class 7 | -------------------------------------------------------------------------------- /tests/features/class_construction/test_class_const.py: -------------------------------------------------------------------------------- 1 | import nip 2 | from utils import builders 3 | 4 | 5 | def test_class_const(): 6 | result = nip.load("features/class_construction/configs/class_const_config.nip") 7 | expected_result = { 8 | "var": 123, 9 | "main": {"just": "some_stuff", "simple_class": builders.SimpleClass, "def_class": builders.ClassWithDefaults}, 10 | } 11 | assert result == expected_result 12 | -------------------------------------------------------------------------------- /tests/features/directives/configs/directive_config.nip: -------------------------------------------------------------------------------- 1 | var: &var 123 2 | 3 | main: 4 | just: 'some_stuff' 5 | inserted: &l !!insert 'features/directives/configs/loaded_config.nip' 6 | 7 | loaded_copy: *l 8 | -------------------------------------------------------------------------------- /tests/features/directives/configs/expected_config.nip: -------------------------------------------------------------------------------- 1 | var: &var 123 2 | 3 | main: 4 | just: 'some_stuff' 5 | inserted: &l 6 | just_a_list: 7 | - 1 8 | - 2 9 | - 'c' 10 | - *var # can't be loaded by itself. 11 | 12 | loaded_copy: *l 13 | -------------------------------------------------------------------------------- /tests/features/directives/configs/loaded_config.nip: -------------------------------------------------------------------------------- 1 | just_a_list: 2 | - 1 3 | - 2 4 | - 'c' 5 | - *var # can't be loaded by itself. -------------------------------------------------------------------------------- /tests/features/directives/test_directives.py: -------------------------------------------------------------------------------- 1 | import nip 2 | 3 | 4 | def test_simple_load_directive(): 5 | result = nip.parse("features/directives/configs/directive_config.nip") 6 | expected_result = nip.parse("features/directives/configs/expected_config.nip") 7 | assert nip.construct(result) == nip.construct(expected_result) 8 | # mb: not true comparison 9 | -------------------------------------------------------------------------------- /tests/features/flatten/configs/complex.nip: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | 3 | main: &main 4 | first: 5 | in1: "11" 6 | in2: &float -12.5 7 | second: [1, 2, 3] 8 | 9 | third: {'a': "123", '123': "qweqwe"} 10 | 11 | 12 | other: 13 | main.other: *main 14 | list: # comment 15 | - f"this is float value {float}" 16 | - true 17 | - 18 | # more comments 19 | sdf: 20 | - sfds: 21 | abra: @ 22 | - "cadabra" 23 | - "ababra" 24 | - "avocado" 25 | - - nested value 26 | - one more 27 | - nested: dict -------------------------------------------------------------------------------- /tests/features/flatten/configs/simple.nip: -------------------------------------------------------------------------------- 1 | main: 2 | number: 1 3 | string: 'qwe' 4 | 5 | something_more: 6 | float: 0.123 7 | bool: True 8 | 9 | list: 10 | - 'some string' 11 | - - 123 -------------------------------------------------------------------------------- /tests/features/flatten/test_flatten.py: -------------------------------------------------------------------------------- 1 | def test_basics(): 2 | from nip import parse 3 | config = parse("features/flatten/configs/simple.nip") 4 | expected_dict = { 5 | 'main.number': 1, 6 | 'main.string': 'qwe', 7 | 'something_more.float': 0.123, 8 | 'something_more.bool': True, 9 | 'list.0': 'some string', 10 | 'list.1.0': 123 11 | } 12 | assert config.flatten() == expected_dict 13 | 14 | 15 | def test_complex(): 16 | from nip import parse 17 | config = parse("features/flatten/configs/complex.nip") 18 | expected_result = { 19 | 'main.first.in1': '11', 20 | 'main.first.in2': -12.5, 21 | 'main.second.0': 1, 22 | 'main.second.1': 2, 23 | 'main.second.2': 3, 24 | 'main.third.a': '123', 25 | 'main.third.123': 'qweqwe', 26 | 'other.main.other': 'nil', 27 | 'other.list.0': 'f"this is float value {float}"', 28 | 'other.list.1': True, 29 | 'other.list.2': None, 30 | 'other.list.sdf.0.sfds.abra': 'cadabra', 31 | 'other.list.sdf.1.0': 'nested value', 32 | 'other.list.sdf.1.1': 'one more', 33 | 'other.list.sdf.1.2.nested': 'dict', 34 | } 35 | 36 | first_config = next(config) 37 | flattened = first_config.flatten() 38 | assert len(expected_result) == len(flattened) 39 | for key, value in expected_result.items(): 40 | assert key in flattened 41 | assert flattened[key] == value 42 | -------------------------------------------------------------------------------- /tests/features/fstrings/configs/fstrings.nip: -------------------------------------------------------------------------------- 1 | first_name: &first_name Ilya 2 | second_name: &second_name Vasiliev 3 | 4 | call: f"Your highness the great and beautiful lord, {first_name} {second_name}" 5 | implicit: "{first_name} the {second_name}" 6 | -------------------------------------------------------------------------------- /tests/features/fstrings/configs/iter_fstrings.nip: -------------------------------------------------------------------------------- 1 | main: 2 | first: &first @ [1, 2, 3] 3 | name: f"name_{first}" 4 | -------------------------------------------------------------------------------- /tests/features/fstrings/test_fstrings.py: -------------------------------------------------------------------------------- 1 | def test_implicit(): 2 | from nip import load 3 | res = load("features/fstrings/configs/fstrings.nip") 4 | expected = { 5 | 'first_name': "Ilya", 6 | 'second_name': "Vasiliev", 7 | 'call': "Your highness the great and beautiful lord, Ilya Vasiliev", 8 | 'implicit': "{first_name} the {second_name}" # implicit fstrings are not supported 9 | } 10 | assert res == expected 11 | 12 | 13 | def test_iter_fstrings(): 14 | from nip import load 15 | iter_values = [1, 2, 3] 16 | for config, value in zip(load("features/fstrings/configs/iter_fstrings.nip"), iter_values): 17 | assert 'main' in config 18 | assert config['main']['first'] == value 19 | assert config['main']['name'] == f"name_{value}" 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/features/inline/configs/inline_config.nip: -------------------------------------------------------------------------------- 1 | main: &main "Hello World!" 2 | iqwe: &i 2 3 | sdkichabskjchskjdhb: &qwe !load_numpy 4 | 5 | other: `f"hey ho {main}"` 6 | other_int: `i + 5` 7 | array: &a `qwe.random.random((3, 4))` 8 | array_2: &b `qwe.ones((3, 4)) * 5` 9 | array_3: `a + b` 10 | -------------------------------------------------------------------------------- /tests/features/inline/test_inline.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from nip import load, nip 3 | 4 | 5 | @nip 6 | def load_numpy(): 7 | return np 8 | 9 | 10 | def test_inline(): 11 | np.random.seed(42) 12 | a = np.random.random((3, 4)) 13 | b = np.ones((3, 4)) * 5 14 | c = a + b 15 | np.random.seed(42) 16 | res = load("features/inline/configs/inline_config.nip") 17 | 18 | assert res['main'] == 'Hello World!' 19 | assert res['iqwe'] == 2 20 | assert res['sdkichabskjchskjdhb'] == np 21 | assert res['other'] == "hey ho Hello World!" 22 | assert res['other_int'] == 7 23 | assert np.all(res['array'] == a) 24 | assert np.all(res['array_2'] == b) 25 | assert np.all(res['array_3'] == c) 26 | -------------------------------------------------------------------------------- /tests/features/modification/configs/config.nip: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | 3 | main: &main 4 | first: 5 | in1: "11" 6 | in2: &float -12.5 7 | second: [1, 2, 3] 8 | 9 | third: {'a': "123", '123': "qweqwe"} 10 | 11 | 12 | other: 13 | main.other: *main 14 | list: # comment 15 | - f"this is float value {float}" 16 | - true 17 | - 18 | # more comments 19 | sdf: 20 | - sfds: 21 | abra: "cadabra" 22 | - - nested value 23 | - one more 24 | - nested: dict -------------------------------------------------------------------------------- /tests/features/modification/configs/object_config.nip: -------------------------------------------------------------------------------- 1 | - !Note 2 | name: first note 3 | comment: nothing special here 4 | 5 | - !Note 6 | name: interesting note 7 | comment: this comment should be changed 8 | 9 | -------------------------------------------------------------------------------- /tests/features/modification/dumps/config.nip: -------------------------------------------------------------------------------- 1 | --- MyDocument 2 | main: &main 3 | first: "modified_value" 4 | second: 5 | - 1 6 | - 3 7 | third: {'a': '123', '123': 'qweqwe'} 8 | other: 9 | main.other: *main 10 | list: 11 | - f"this is float value {float}" 12 | - True 13 | - 14 | sdf: 15 | - 16 | sfds: 17 | abra: "cadabra" 18 | - 19 | - "nested value" 20 | - "one more" 21 | - 22 | nested: "dict" -------------------------------------------------------------------------------- /tests/features/modification/dumps/object_config.nip: -------------------------------------------------------------------------------- 1 | --- 2 | - !Note 3 | name: "first note" 4 | comment: "nothing special here" 5 | - !Note 6 | name: "interesting note" 7 | comment: "new comment" -------------------------------------------------------------------------------- /tests/features/modification/dumps/object_config_2.nip: -------------------------------------------------------------------------------- 1 | --- 2 | - !Note 3 | name: "first note" 4 | comment: "nothing special here" 5 | - !Note 6 | name: "interesting note" 7 | comment: "what a comment!" -------------------------------------------------------------------------------- /tests/features/modification/test_modification.py: -------------------------------------------------------------------------------- 1 | def test_config(): 2 | from nip import parse, dump 3 | 4 | config = parse("features/modification/configs/config.nip") 5 | config["main"]["first"] = "modified_value" 6 | config["main"]["second"] = (1, 3) 7 | dump("features/modification/dumps/config.nip", config) 8 | 9 | 10 | def test_object(): 11 | from nip import parse, dump, load, nip 12 | from utils.builders import Note 13 | 14 | config = parse("features/modification/configs/object_config.nip") 15 | config[1] = Note("interesting note", "new comment") 16 | dump("features/modification/dumps/object_config.nip", config) 17 | 18 | nip(Note) 19 | result = load("features/modification/dumps/object_config.nip") 20 | assert result[0] == Note("first note", "nothing special here") 21 | assert result[1] == Note("interesting note", "new comment") 22 | 23 | 24 | def test_object_2(): 25 | from nip import parse, dump, load, nip 26 | from utils.builders import Note 27 | 28 | config = parse("features/modification/configs/object_config.nip") 29 | config[1]["comment"] = "what a comment!" 30 | dump("features/modification/dumps/object_config_2.nip", config) 31 | 32 | nip(Note) 33 | result = load("features/modification/dumps/object_config_2.nip") 34 | assert result[0] == Note("first note", "nothing special here") 35 | assert result[1] == Note("interesting note", "what a comment!") 36 | -------------------------------------------------------------------------------- /tests/features/module_wrap/configs/auto_wrap_config.yaml: -------------------------------------------------------------------------------- 1 | class: !NotNipClass 2 | name: abra 3 | 4 | func: !NoNipFunc 5 | name: cadabra -------------------------------------------------------------------------------- /tests/features/module_wrap/test_module.py: -------------------------------------------------------------------------------- 1 | from nip import load, wrap_module 2 | 3 | 4 | def main(): 5 | wrap_module("builders") 6 | # alternative variant: 7 | # import builders 8 | # wrap_module(builders) 9 | 10 | res = load("configs/auto_wrap_config.yaml") 11 | print(res['class']) 12 | print(res['func']) 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /tests/features/non_seq/configs/harder_non_seq.nip: -------------------------------------------------------------------------------- 1 | main: 2 | - some 3 | - 123 4 | - items: &it 5 | - 4 6 | - 5 7 | - *ll 8 | - 7 9 | 10 | other_main: 11 | interesting: *it 12 | here_is_the_ll: &ll 6 13 | -------------------------------------------------------------------------------- /tests/features/non_seq/configs/inline_non_seq.nip: -------------------------------------------------------------------------------- 1 | ## add test with inline expressions like `model.parameters()` 2 | main: 3 | - f"some f string with var {v}" 4 | - 123 5 | - items: &it 6 | - 4 7 | - 5 8 | - *ll 9 | - &v 12 10 | 11 | other_main: 12 | iteresting: *it 13 | here_is_the_ll: &ll 6 14 | 15 | inline_python: `int(t) / 2` 16 | t: &t 5 17 | -------------------------------------------------------------------------------- /tests/features/non_seq/configs/recursive_non_seq.nip: -------------------------------------------------------------------------------- 1 | main: 2 | - some 3 | - 123 4 | - items: &it 5 | - 4 6 | - 5 7 | - *ll 8 | - 7 9 | 10 | other_main: &ll 11 | iteresting: *it 12 | here_is_the_ll: 6 13 | -------------------------------------------------------------------------------- /tests/features/non_seq/configs/simple_non_seq.nip: -------------------------------------------------------------------------------- 1 | main: *main 2 | other_main: &main 3 | just: some 4 | values: 123 -------------------------------------------------------------------------------- /tests/features/non_seq/test_non_seq.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import nip.non_seq_constructor 3 | 4 | from nip import load 5 | from nip.non_seq_constructor import NonSequentialConstructorError 6 | 7 | 8 | def test_simple_non_seq(): 9 | output = load("features/non_seq/configs/simple_non_seq.nip", nonsequential=True) 10 | expected = { 11 | 'main': { 12 | 'just': 'some', 13 | 'values': 123 14 | }, 15 | 'other_main': { 16 | 'just': 'some', 17 | 'values': 123 18 | }, 19 | } 20 | assert output == expected 21 | 22 | 23 | def test_harder_non_seq(): 24 | output = load("features/non_seq/configs/harder_non_seq.nip", nonsequential=True) 25 | expected = { 26 | 'main': [ 27 | 'some', 28 | 123, 29 | { 30 | 'items': [4, 5, 6, 7] 31 | } 32 | ], 33 | 'other_main': { 34 | 'interesting': [4, 5, 6, 7], 35 | 'here_is_the_ll': 6 36 | } 37 | } 38 | assert output == expected 39 | 40 | 41 | def test_part_harder_non_seq(): 42 | config = nip.parse("features/non_seq/configs/harder_non_seq.nip") 43 | constructor = nip.non_seq_constructor.NonSequentialConstructor(config) 44 | expected = { 45 | 'interesting': [4, 5, 6, 7], 46 | 'here_is_the_ll': 6 47 | } 48 | output = constructor.construct(config['other_main']) 49 | assert output == expected 50 | 51 | output = config['other_main']._construct(constructor) 52 | assert output == expected 53 | 54 | output = nip.construct(config['other_main']) 55 | assert output == expected 56 | 57 | 58 | def test_recursive_non_seq(): 59 | with pytest.raises(NonSequentialConstructorError, match="Recursive construction"): 60 | load("features/non_seq/configs/recursive_non_seq.nip", nonsequential=True) 61 | 62 | 63 | def test_inline_non_seq(): 64 | output = nip.load("features/non_seq/configs/inline_non_seq.nip", nonsequential=True) 65 | expected = { 66 | 'main': [ 67 | "some f string with var 12", 68 | 123, 69 | { 70 | 'items': [4, 5, 6, 12] 71 | } 72 | ], 73 | 'other_main': { 74 | 'iteresting': [4, 5, 6, 12], 75 | 'here_is_the_ll': 6 76 | }, 77 | 'inline_python': 2.5, 78 | 't': 5 79 | } 80 | assert output == expected 81 | -------------------------------------------------------------------------------- /tests/features/nothing/configs/nothing.nip: -------------------------------------------------------------------------------- 1 | qwe: 2 | asd: 3 | dfg: 4 | 5 | zxc: 6 | 7 | - 2 8 | - 213 9 | - qwe: 10 | - 3 11 | - 12 | - 13 | - fgh: 14 | fgr: 15 | - 16 | - 17 | 18 | fff: -------------------------------------------------------------------------------- /tests/features/nothing/test_nothing.py: -------------------------------------------------------------------------------- 1 | from utils.test_utils import NOTHING, deep_conditioned_compare, nothing_comparison 2 | 3 | 4 | def test_nothing(): 5 | from nip import load 6 | 7 | result = load("features/nothing/configs/nothing.nip") 8 | expected_result = ( 9 | [2, 213, {"qwe": [3, NOTHING, NOTHING, {"fgh": ([NOTHING], {"fgr": NOTHING})}]}, NOTHING], 10 | {"qwe": {"asd": {"dfg": NOTHING}}, "zxc": NOTHING, "fff": NOTHING}, 11 | ) 12 | assert deep_conditioned_compare(result, expected_result, [nothing_comparison]) 13 | -------------------------------------------------------------------------------- /tests/features/object_dump/dumps/complex.nip: -------------------------------------------------------------------------------- 1 | !Complex 2 | - 3 | dict: "with" 4 | some: "data" 5 | number: 42 6 | - 7 | - !Small "Popo" 8 | - !Small "Pepe" -------------------------------------------------------------------------------- /tests/features/object_dump/dumps/no_default.nip: -------------------------------------------------------------------------------- 1 | !NoDefaultDumper 2 | - "just_a_name" 3 | value: !BigComplexClass 4 | - "data_value" 5 | - 6 | - 1 7 | - 2 8 | - 3 -------------------------------------------------------------------------------- /tests/features/object_dump/dumps/obj.nip: -------------------------------------------------------------------------------- 1 | first: 1 2 | second: "2" -------------------------------------------------------------------------------- /tests/features/object_dump/some_classes.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from nip import nip 3 | 4 | 5 | class A: 6 | def question(self): 7 | return "Ultimate Question of Life, the Universe, and Everything" 8 | 9 | 10 | class B: 11 | def answer(self): 12 | return 42 13 | 14 | 15 | @nip("Complex") 16 | class BigComplexClass(A, B): 17 | def __init__(self, data, childs: List): 18 | self.data = data 19 | self.childs = childs 20 | 21 | def __nip__(self): 22 | return [self.data, self.childs] 23 | 24 | 25 | @nip("Small") 26 | class SmallButValuableClass: 27 | def __init__(self, name): 28 | self.just_name = name 29 | 30 | def __nip__(self): 31 | return self.just_name 32 | 33 | 34 | class NoDefaultDumper: 35 | def __init__(self, name, value): 36 | self.name = name 37 | self.value = value 38 | -------------------------------------------------------------------------------- /tests/features/object_dump/test_dump.py: -------------------------------------------------------------------------------- 1 | def test_base(): 2 | from nip import dump, load 3 | 4 | obj = {"first": 1, "second": "2"} 5 | dump("features/object_dump/dumps/obj.nip", obj) 6 | assert load("features/object_dump/dumps/obj.nip") == obj 7 | 8 | 9 | def test_complex(): 10 | from some_classes import BigComplexClass, SmallButValuableClass 11 | from nip import dump, load 12 | 13 | small_obj_1 = SmallButValuableClass("Popo") 14 | small_obj_2 = SmallButValuableClass("Pepe") 15 | big_obj = BigComplexClass({"dict": "with", "some": "data", "number": 42}, childs=[small_obj_1, small_obj_2]) 16 | dump("features/object_dump/dumps/complex.nip", big_obj) 17 | result = load("features/object_dump/dumps/complex.nip") 18 | assert result.data == {"dict": "with", "some": "data", "number": 42} 19 | assert isinstance(result.childs[0], SmallButValuableClass) and result.childs[0].just_name == "Popo" 20 | assert isinstance(result.childs[1], SmallButValuableClass) and result.childs[1].just_name == "Pepe" 21 | 22 | 23 | def test_no_defalut_dumper(): 24 | import some_classes 25 | import nip 26 | 27 | nip.nip(some_classes, convertable=True) 28 | value = some_classes.BigComplexClass("data_value", [1, 2, 3]) 29 | obj = some_classes.NoDefaultDumper("just_a_name", value=value) 30 | nip.dump("features/object_dump/dumps/no_default.nip", obj) 31 | result = nip.load("features/object_dump/dumps/no_default.nip") 32 | assert isinstance(result, some_classes.NoDefaultDumper) 33 | assert isinstance(result.value, some_classes.BigComplexClass) 34 | assert result.value.data == "data_value" and result.value.childs == [1, 2, 3] 35 | -------------------------------------------------------------------------------- /tests/features/replacement/configs/config.nip: -------------------------------------------------------------------------------- 1 | object: &obj !SimpleClass 2 | name: abra 3 | 4 | value: &value 123 5 | 6 | inserted: !!insert 7 | - "features/replacement/configs/sub_config.nip" 8 | object: *obj 9 | key: secret-key -------------------------------------------------------------------------------- /tests/features/replacement/configs/sub_config.nip: -------------------------------------------------------------------------------- 1 | some: 2 | deep: 3 | key: *key 4 | value: *value 5 | object: *object -------------------------------------------------------------------------------- /tests/features/replacement/test_replacement.py: -------------------------------------------------------------------------------- 1 | import nip 2 | from utils import builders 3 | 4 | 5 | def test_inter_run(): 6 | result = nip.load("features/replacement/configs/config.nip") 7 | assert isinstance(result["object"], builders.SimpleClass) and result["object"].name == "abra" 8 | assert result["value"] == 123 9 | assert result["inserted"]["some"]["deep"]["key"] == "secret-key" 10 | assert result["inserted"]["some"]["deep"]["value"] == 123 11 | assert result["inserted"]["some"]["deep"]["object"] == result["object"] 12 | -------------------------------------------------------------------------------- /tests/features/run/class_example.py: -------------------------------------------------------------------------------- 1 | class SimpleClass: 2 | def __init__(self, name: str, value: int): 3 | self.name = name 4 | self.value = value 5 | 6 | 7 | def simple_class_printer(obj: SimpleClass): 8 | print(f"Name is: {obj.name}") 9 | return obj.value * 3 10 | -------------------------------------------------------------------------------- /tests/features/run/configs/run.nip: -------------------------------------------------------------------------------- 1 | obj: !SimpleClass 2 | name: "some_name" 3 | value: 14 4 | 5 | -------------------------------------------------------------------------------- /tests/features/run/configs/run_config_param.nip: -------------------------------------------------------------------------------- 1 | --- 2 | param: some parameter value 3 | -------------------------------------------------------------------------------- /tests/features/run/test_run.py: -------------------------------------------------------------------------------- 1 | def test_simple_run(): 2 | from nip import run, nip 3 | import class_example 4 | 5 | nip(class_example) 6 | value = run("features/run/configs/run.nip", func=class_example.simple_class_printer, verbose=False) 7 | assert value == 42 8 | 9 | 10 | def test_inter_run(): 11 | from nip import run, nip 12 | from utils import builders 13 | 14 | nip(builders) 15 | value, dumped = run( 16 | "features/run/configs/run_config_param.nip", func=builders.main, config_parameter="config", verbose=False 17 | ) 18 | assert value == "some parameter value from main with love" 19 | assert dumped == '---\nparam: "some parameter value"' 20 | '--- \nparam: "some parameter value"' 21 | 22 | 23 | # mb: test verbose? 24 | -------------------------------------------------------------------------------- /tests/features/strict/configs/double_names.nip: -------------------------------------------------------------------------------- 1 | main: 2 | some_name: "string" 3 | some_other_name: 234 4 | some_name: 34 5 | -------------------------------------------------------------------------------- /tests/features/strict/configs/strict_func_types_1.nip: -------------------------------------------------------------------------------- 1 | simple: &simple !SimpleClass 2 | name: "Hello" 3 | 4 | f: !myfunc 5 | - 1 6 | b: 2 7 | c: "23" 8 | -------------------------------------------------------------------------------- /tests/features/strict/configs/strict_func_types_2.nip: -------------------------------------------------------------------------------- 1 | simple: &simple !SimpleClass 2 | name: "Hello" 3 | 4 | f: !myfunc 5 | # - 1 6 | - *simple 7 | b: 2 8 | #d: 3 9 | -------------------------------------------------------------------------------- /tests/features/strict/configs/strict_func_types_3.nip: -------------------------------------------------------------------------------- 1 | simple: &simple !SimpleClass 2 | name: 234872364 3 | 4 | f: !myfunc 5 | - 1 6 | b: 2 7 | -------------------------------------------------------------------------------- /tests/features/strict/configs/strict_func_types_correct.nip: -------------------------------------------------------------------------------- 1 | simple: &simple !SimpleClass 2 | name: "Hello" 3 | 4 | f: !myfunc 5 | - 1 6 | b: 2 7 | #d: 3 8 | -------------------------------------------------------------------------------- /tests/features/strict/test_strict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nip.constructor import ConstructorError 4 | from nip.parser import ParserError 5 | 6 | 7 | def test_correct_strict(): 8 | from utils import builders 9 | from nip import load 10 | 11 | result = load("features/strict/configs/strict_func_types_correct.nip", strict=True) 12 | assert isinstance(result["simple"], builders.SimpleClass) and result["f"] == 5 13 | 14 | 15 | def test_strict_1(): 16 | from nip import load 17 | 18 | with pytest.raises(ConstructorError, match="c: str is not an instance of int"): 19 | load("features/strict/configs/strict_func_types_1.nip", strict=True) 20 | 21 | 22 | def test_strict_2(): 23 | from nip import load 24 | 25 | with pytest.raises(ConstructorError, match="a: utils.builders.SimpleClass is not an instance of int"): 26 | load("features/strict/configs/strict_func_types_2.nip", strict=True) 27 | 28 | 29 | def test_strict_3(): 30 | from nip import load 31 | 32 | with pytest.raises(ConstructorError, match="name: int is not an instance of str"): 33 | load("features/strict/configs/strict_func_types_3.nip", strict=True) 34 | 35 | 36 | def test_strict_names(): 37 | from nip import load 38 | 39 | with pytest.raises( 40 | ParserError, match="4:3: Dict key overwriting is forbidden " "in `strict` mode. Overwritten key: 'some_name'." 41 | ): 42 | load("features/strict/configs/double_names.nip", strict=True) 43 | 44 | 45 | # def test_wrong_args(): 46 | # with pytest.warns(warnings.WarningMessage, 47 | # math="Typing mismatch while constructing {self.name}"): 48 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spairet/nip/a019ab56284f072d79fd74812b48d370e133e464/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/builders.py: -------------------------------------------------------------------------------- 1 | from nip import nip, dump_string 2 | 3 | 4 | @nip 5 | class SimpleClass: 6 | def __init__(self, name: str): 7 | self.name = name 8 | 9 | @nip("print_method") 10 | def print(self): 11 | print("My name is: ", self.name) 12 | return 312983 13 | 14 | 15 | @nip("myfunc") 16 | def MySecretFunction(a: int, b: int = 0, c: int = 0): 17 | return a + 2 * b + 3 * c 18 | 19 | 20 | @nip 21 | class MyClass: 22 | def __init__(self, name: str, f: object): 23 | self.name = name 24 | self.f = f 25 | 26 | def __str__(self): 27 | return f"name: {self.name}, func result: {self.f}" 28 | 29 | 30 | class NotNipClass: 31 | def __init__(self, name): 32 | self.name = name 33 | 34 | 35 | def NoNipFunc(name): 36 | print("NoYapFunc:", name) 37 | 38 | 39 | def show(*args, **kwargs): 40 | print("args:", args) 41 | print("kwargs:", kwargs) 42 | 43 | 44 | def main(param, config): 45 | print(dump_string(config)) 46 | return param + " from main with love", dump_string(config) 47 | 48 | 49 | @nip("def_class") 50 | class ClassWithDefaults: 51 | def __init__(self, name: str = "something"): 52 | self.name = name 53 | 54 | 55 | class Note: 56 | def __init__(self, name, comment): 57 | self.name = name 58 | self.comment = comment 59 | 60 | def __nip__(self): 61 | return self.__dict__ 62 | 63 | def __eq__(self, other): 64 | return self.name == other.name and self.comment == other.comment 65 | 66 | 67 | @nip(["first_tag", "second_tag"]) 68 | class MultiTagClass: 69 | def __init__(self, name: str = ""): 70 | self.name = name 71 | -------------------------------------------------------------------------------- /tests/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Callable 2 | 3 | from nip.elements import Nothing 4 | 5 | # mb: depending on config 6 | # mb: make Nothing a singleton 7 | # NOTHING = None 8 | # IS_NOTHING = lambda obj, None: obj is None 9 | 10 | NOTHING = Nothing() 11 | IS_NOTHING = lambda obj: isinstance(obj, Nothing) 12 | 13 | 14 | def nothing_comparison(first, second): 15 | return IS_NOTHING(first) and IS_NOTHING(second) 16 | 17 | 18 | def deep_conditioned_compare(first: object, second: object, conditions: List[Callable] = ()): 19 | if first.__class__ != second.__class__: # mb: let them be subclasses of one super class 20 | return False 21 | if isinstance(first, (list, tuple)) and isinstance(second, (list, tuple)): 22 | if len(first) != len(second): 23 | return False 24 | return all( 25 | [ 26 | deep_conditioned_compare(first_item, second_item, conditions) 27 | for first_item, second_item in zip(first, second) 28 | ] 29 | ) 30 | if isinstance(first, dict) and isinstance(second, dict): 31 | if len(first) != len(second): 32 | return False 33 | for key in first: 34 | if key not in second: 35 | return False 36 | if not deep_conditioned_compare(first[key], second[key], conditions): 37 | return False 38 | for cond in conditions: 39 | if cond(first, second): 40 | return True 41 | return first == second 42 | --------------------------------------------------------------------------------