def test_nested_working(self): config1 = Config(dict(x=1, y=[2, 3], z=dict(a=4, b=5))) config2 = Config(dict(w=6, y=[7], z=dict(b=8, c=9))) config1.merge(config2) compare(config1.data, expected=dict(x=1, w=6, y=[2, 3, 7], z=dict(a=4, b=8, c=9)))
def test_other_to_dict(self): config1 = Config(1) config2 = Config(1) with ShouldRaise(type_error( "Cannot merge <class 'int'> with <class 'int'>" )): config1.merge(config2)
def test_type_returns_new_object(self): config1 = Config((1, 2)) config2 = Config((3, 4)) def concat(context, source, target): return target + source config1.merge(config2, mergers={tuple: concat}) compare(config1.data, expected=(1, 2, 3, 4))
def test_override_type_mapping(self): config1 = Config([1, 2]) config2 = Config([3, 4]) def zipper(context, source, target): return zip(target, source) config1.merge(config2, mergers={list: zipper}) compare(config1.data, expected=[(1, 3), (2, 4)])
def test_supplement_type_mapping(self): config1 = Config({'x': (1, 2)}) config2 = Config({'x': (3, 4)}) def concat(context, source, target): return target + source config1.merge(config2, mergers=default_mergers+{tuple: concat}) compare(config1.data, expected={'x': (1, 2, 3, 4)})
def test_blank_type_mapping(self): config1 = Config({'foo': 'bar'}) config2 = Config({'baz': 'bob'}) with ShouldRaise(type_error( "Cannot merge <class 'dict'> with <class 'dict'>" )): config2.merge(config1, mergers={})
def test_mapping_strings(self): config = Config({'x': 'old'}) data = {'foo': 'bar'} config.merge(data, mapping={ 'foo': 'x' }) compare(config.data, expected={'x': 'bar'})
def test_overlay(self): path1 = self.dir.write('etc/myapp.yml', ''' base: 1 user: bad file: bad ''') path2 = self.dir.write('home/.myapp.yml', ''' user: 2 file: bad-user ''') path3 = self.dir.write('app.yml', ''' file: 3 ''') config = Config.from_file(path1) config.merge(Config.from_file(path2)) config.merge(Config.from_file(path3)) config.validate(Schema({str: int})) compare(config.base, expected=1) compare(config.user, expected=2) compare(config.file, expected=3)
def test_mapping_type_conversion(self): config = Config({'x': 0}) data = {'y': '1'} config.merge(data, mapping={ convert(source['y'], int): target['x'] }) compare(config.data, expected={'x': 1})
def test_mapping_dotted_strings(self): config = Config({'a': {'b': 'old'}}) data = {'c': {'d': 'new'}} config.merge(data, mapping={ 'c.d': 'a.b' }) compare(config.data, expected={'a': {'b': 'new'}})
def test_mapping_with_top_level_merge(self): config = Config({'x': {'y': 1}}) data = {'z': 2} config.merge(data, mapping={ source: target.merge() }) compare(config.data, expected={'x': {'y': 1}, 'z': 2})
def test_list_to_dict(self): config1 = Config({'x': 1}) config2 = Config([1, 2]) with ShouldRaise(type_error( "Cannot merge <class 'list'> with <class 'dict'>" )): config1.merge(config2)
def test_mapping_paths(self): config = Config({'x': 'old'}) data = {'foo': 'bar'} config.merge(data, mapping={ source['foo']: target['x'] }) compare(config.data, expected={'x': 'bar'})
def test_mapping_extensive_conversation(self): config = Config({'a': 0}) data = {'x': 2, 'y': -1} def best(possible): return max(possible.values()) config.merge(data, mapping={ convert(source, best): target['a'] }) compare(config.data, expected={'a': 2})
def test_defaults_and_env(self): config = Config({'present': 'dp', 'absent': 'da'}) environ = {'ENV_PRESENT': '1'} config.merge(environ, { convert('ENV_PRESENT', int): 'present', convert('ENV_ABSENT', int): 'absent', }) compare(config.present, expected=1) compare(config.absent, expected='da')
def test_templated(self): self.dir.write('etc/myapp.yml', ''' {% set base = dict(one=1)%} base_template: {{ base.one }} base_env: {{ MYVAR }} file_template: bad file_env: bad ''') self.dir.write('app.yml', ''' {% set local=2 %} file_template: {{ local }} file_env: {{ MYVAR }} ''') with Replace('os.environ.MYVAR', 'hello', strict=False) as r: env = Environment(loader=FileSystemLoader(self.dir.path)) config = None context = dict(os.environ) for path in 'etc/myapp.yml', 'app.yml': text = env.get_template(path).render(context) layer = Config.from_text(text) if config is None: config = layer else: config.merge(layer) compare(config.base_template, expected=1) compare(config.base_env, expected='hello') compare(config.file_template, expected=2) compare(config.file_env, expected='hello')
def test_stream_with_name_guess_parser(self): with NamedTemporaryFile(suffix='.json') as source: source.write(b'{"x": 1}') source.flush() source.seek(0) config = Config.from_stream(source) compare(config.x, expected=1)
def test_explicit_extends(self): raise SkipTest('not yet') path1 = self.dir.write('base.yml', ''' base: 1 file: bad ''') path2 = self.dir.write('app.yml', ''' extends: {} file: 2 '''.format(path1)) config = Config(path2) config.validate(Schema({ 'extends': config.merge(), 'base': int, 'file': int, })) compare(config.base, expected=1) compare(config.file, expected=2)
def test_defaults_and_argparse(self): parser = ArgumentParser() parser.add_argument('--first-url') parser.add_argument('--attempts', type=int, default=2) parser.add_argument('--bool-a', action='store_true') parser.add_argument('--bool-b', action='store_false') args = parser.parse_args(['--first-url', 'override_url', '--bool-a']) compare(args.bool_b, expected=True) config = Config({'urls': ['default_url'], 'bool_b': False}) config.merge(args, { 'first_url': target['urls'].insert(0), 'attempts': target['attempts'], 'bool_a': target['bool_a'], 'bool_b': target['bool_b'], }) compare(config.urls, expected=['override_url', 'default_url']) compare(config.attempts, expected=2) compare(config.bool_a, expected=True) compare(config.bool_b, expected=True)
def test_include_list(self): raise SkipTest('not yet') path1 = self.dir.write('other.yml', ''' - 2 - 3 ''') path2 = self.dir.write('app.yml', ''' root: - 1 - include: {} '''.format(path1)) config = Config(path2) config.validate(Schema({ 'include': config.merge(), 'base': int, 'file': int, })) compare(config.base, expected=1) compare(config.file, expected=2)
def test_overlay(self, dir): pytest.importorskip("yaml") path1 = dir.write('etc/myapp.yml', ''' base: 1 user: bad file: bad ''') path2 = dir.write('home/.myapp.yml', ''' user: 2 file: bad-user ''') path3 = dir.write('app.yml', ''' file: 3 ''') config = Config.from_path(path1) config.merge(Config.from_path(path2)) config.merge(Config.from_path(path3)) compare(config.base, expected=1) compare(config.user, expected=2) compare(config.file, expected=3)
def test_layered(self, dir): # defaults config = Config({ 'database': {'user': '******'}, 'special': False }) # from system file: path = dir.write('etc/myapp.json', '{"special": true}') config.merge(Config.from_path(path)) # from user file: path = dir.write('home/user/myapp.json', '{"database": {"password": "******"}}') config.merge(Config.from_path(path)) # end result: compare(config.database.user, expected='foo') compare(config.database.password, expected='123') compare(config.special, expected=True)
def test_path_with_encoding(self): with NamedTemporaryFile() as source: source.write(b'{"x": "\xa3"}') source.flush() config = Config.from_path(source.name, 'json', encoding='latin-1') compare(config.x, expected=u'\xa3')
def test_path_explicit_string_parser(self): with NamedTemporaryFile() as source: source.write(b'{"x": 1}') source.flush() config = Config.from_path(source.name, 'json') compare(config.x, expected=1)
def test_path_guess_parser_no_extension(self): with TempDirectory() as dir: path = dir.write('nope', b'{"x": 1}') with ShouldRaise(ParseError("No parser found for None")): Config.from_path(path)
def test_text_missing_parser(self): with ShouldRaise(ParseError("No parser found for 'lolwut'")): Config.from_text("{'foo': 'bar'}", 'lolwut')
source_path.rename(dest_path) class IncomingEventHandler(FileSystemEventHandler): def __init__(self, dest: Path): self.dest = dest def on_created(self, event): source_path = Path(event.src_path) if source_path.name == FILENAME: move(source_path, self.dest) if __name__ == "__main__": config = Config.from_path('config.yaml') source_dir = Path(config.directories.incoming).expanduser() dest = Path(config.directories.storage).expanduser() path = source_dir / FILENAME if path.exists(): move(path, dest) event_handler = IncomingEventHandler(dest) observer = Observer() observer.schedule(event_handler, str(source_dir)) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt:
config = Config({ 'user': { 'name': 'unsetname', 'affiliation': 'unsetaffiliation', 'style': 'double', }, 'reconfig': True, 'compiler': 'none', 'script': { 'locations': { '~/.reportinator/scripts', '~/.config/reportinator/scripts', '~/AppData/Local/Programs/reportinator/scripts', }, }, 'layout': { 'locations': { '~/.reportinator/layouts', '~/.config/reportinator/layouts', '~/AppData/Local/Programs/reportinator/layouts', }, }, 'locations': { '/etc/reportinator.yaml', '~/.reportinator/config.yaml', '~/.config/reportinator/config.yaml', '~/AppData/Local/Programs/reportinator/config.yaml', }, })
def test_empty_config(self): config = Config() config.merge(Config()) compare(config.data, expected={})
def test_list(self): config = Config([1, 2]) compare(config[0], expected=1) compare(config[1], expected=2) compare(list(config), expected=[1, 2])
def test_list_to_list(self): config = Config([1, 2]) config.merge([3, 4]) compare(config.data, expected=[1, 2, 3, 4])
def test_dict(self): config = Config(dict(x=1)) compare(config.x, expected=1)
def test_dict_to_dict(self): config = Config({'x': 1}) config.merge({'y': 2}) compare(config.data, expected={'x': 1, 'y': 2})
def test_simple_type(self): config = Config() with ShouldRaise(type_error( "Cannot merge <class 'str'> with <class 'dict'>" )): config.merge('foo')
def test_stream_no_name_no_parser(self): source = StringIO(u'{"x": 1}') with ShouldRaise(ParseError("No parser found for None")): Config.from_stream(source)
def test_attr_access(self): config = Config({'foo': 'bar'}) compare(config.foo, expected='bar')
config = Config({ "user": { "name": "unsetname", "affiliation": "unsetaffiliation", "style": "double", }, "reconfig": True, "compiler": "none", "script": { "location": cdir, "locations": { "~/.reportinator/scripts", "~/.config/reportinator/scripts", "~/AppData/Local/Programs/reportinator/scripts", }, }, "layout": { "location": cdir, "locations": { "~/.reportinator/layouts", "~/.config/reportinator/layouts", "~/AppData/Local/Programs/reportinator/layouts", }, }, "locations": { "/etc/reportinator.yaml", "~/.reportinator/config.yaml", "~/.config/reportinator/config.yaml", "~/AppData/Local/Programs/reportinator/config.yaml", }, "location": "", })
def test_dict_access(self): config = Config({'foo': 'bar'}) compare(config['foo'], expected='bar')
def test_context_manager_push_deep(self): config = Config({'x': {'y': 'z'}}) with config.push(): config.data['x']['y'] = 'a' compare(config.x.y, expected='a') compare(config.x.y, expected='z')
def test_empty(self): config = Config() compare(config.data, expected={})
def test_text_callable_parser(self): config = Config.from_text("{'foo': 'bar'}", python_literal) compare(config.data, expected={'foo': 'bar'})
def test_pop_without_push(self): config = Config({'x': 1, 'y': 2}) with ShouldRaise(IndexError('pop from empty list')): config.pop()
def test_path_guess_parser(self): with NamedTemporaryFile(suffix='.json') as source: source.write(b'{"x": 1}') source.flush() config = Config.from_path(source.name) compare(config.x, expected=1)
def test_push_non_config(self): config = Config({'x': 1}) compare(config.x, expected=1) config.push({'x': 2}) compare(config.x, expected=2)
def test_path_guess_parser_bad_extension(self): with NamedTemporaryFile(suffix='.nope') as source: with ShouldRaise(ParseError("No parser found for 'nope'")): Config.from_path(source.name)
def test_stream_callable_parser(self): source = StringIO(u'{"x": 1}') config = Config.from_stream(source, python_literal) compare(config.x, expected=1)
def test_path_explicit_callable_parser(self): with NamedTemporaryFile() as source: source.write(b'{"x": 1}') source.flush() config = Config.from_path(source.name, python_literal) compare(config.x, expected=1)
def test_stream_string_parser(self): source = StringIO(u'{"x": 1}') config = Config.from_stream(source, 'json') compare(config.x, expected=1)
def test_fake_fs(fs): fs.create_file('/foo/bar.yml', contents='foo: 1\n') config = Config.from_path('/foo/bar.yml') compare(config.foo, expected=1)
def test_non_empty_config(self): config = Config({'foo': 'bar'}) config.merge(Config({'baz': 'bob'})) compare(config.data, {'foo': 'bar', 'baz': 'bob'})