From dadf859905370bf81f8f675c1cfde7816904e73e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 8 Jul 2017 15:59:56 +0200 Subject: [PATCH] better cache --- ChangeLog | 4 + test/test_cache.py | 271 ++++++++++++++++++++++-- test/test_state.py | 11 +- tiramisu/__init__.py | 2 +- tiramisu/autolib.py | 2 +- tiramisu/config.py | 111 +++++++--- tiramisu/error.py | 2 +- tiramisu/i18n.py | 2 +- tiramisu/option/baseoption.py | 73 ++++++- tiramisu/option/masterslave.py | 7 +- tiramisu/option/option.py | 4 +- tiramisu/option/optiondescription.py | 18 +- tiramisu/setting.py | 33 +-- tiramisu/storage/dictionary/__init__.py | 2 +- tiramisu/storage/dictionary/option.py | 8 +- tiramisu/storage/dictionary/setting.py | 2 +- tiramisu/storage/dictionary/storage.py | 2 +- tiramisu/storage/dictionary/value.py | 2 +- tiramisu/storage/sqlite3/__init__.py | 2 +- tiramisu/storage/util.py | 11 +- tiramisu/value.py | 16 +- 21 files changed, 476 insertions(+), 109 deletions(-) diff --git a/ChangeLog b/ChangeLog index ade2d3c..ee61e37 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +Sat Jul 8 15:57:13 2017 +0200 Emmanuel Garette + * better cache, only remove value/property from cache for value + modified and for all value affected by this modification + Sat May 20 16:27:09 2017 +0200 Emmanuel Garette * add 'operator' to requirement diff --git a/test/test_cache.py b/test/test_cache.py index b3963b2..c4611ca 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -5,7 +5,7 @@ do_autopath() from tiramisu import setting, value setting.expires_time = 1 value.expires_time = 1 -from tiramisu.option import IntOption, StrOption, OptionDescription +from tiramisu.option import BoolOption, IPOption, IntOption, StrOption, OptionDescription from tiramisu.config import Config from tiramisu.error import ConfigError from tiramisu.setting import groups @@ -77,31 +77,42 @@ def test_cache_reset(): settings = c.cfgimpl_get_settings() #when change a value c.u1 + c.u2 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) + assert 'u2' in values._p_.get_cached(c) + assert 'u2' in settings._p_.get_cached(c) c.u2 = 1 - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u2' not in values._p_.get_cached(c) + assert 'u2' not in settings._p_.get_cached(c) #when remove a value c.u1 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) del(c.u2) - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u2' not in values._p_.get_cached(c) + assert 'u2' not in settings._p_.get_cached(c) #when add/del property c.u1 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_get_settings()[od1.u2].append('test') - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u2' not in values._p_.get_cached(c) + assert 'u2' not in settings._p_.get_cached(c) c.u1 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_get_settings()[od1.u2].remove('test') - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u2' not in values._p_.get_cached(c) + assert 'u2' not in settings._p_.get_cached(c) #when enable/disabled property c.u1 assert 'u1' in values._p_.get_cached(c) @@ -122,34 +133,51 @@ def test_cache_reset_multi(): c = Config(od1) values = c.cfgimpl_get_values() settings = c.cfgimpl_get_settings() - #when change a value c.u1 + c.u3 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) + assert 'u3' in values._p_.get_cached(c) + assert 'u3' in settings._p_.get_cached(c) + #when change a value c.u3 = [1] - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u3' not in values._p_.get_cached(c) + assert 'u3' not in settings._p_.get_cached(c) #when append value c.u1 + c.u3 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) + assert 'u3' in values._p_.get_cached(c) + assert 'u3' in settings._p_.get_cached(c) c.u3.append(1) - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u3' not in values._p_.get_cached(c) + assert 'u3' not in settings._p_.get_cached(c) #when pop value c.u1 + c.u3 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) + assert 'u3' in values._p_.get_cached(c) + assert 'u3' in settings._p_.get_cached(c) c.u3.pop(1) - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u3' not in values._p_.get_cached(c) + assert 'u3' not in settings._p_.get_cached(c) #when remove a value c.u1 assert 'u1' in values._p_.get_cached(c) assert 'u1' in settings._p_.get_cached(c) del(c.u3) - assert 'u1' not in values._p_.get_cached(c) - assert 'u1' not in settings._p_.get_cached(c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + assert 'u3' not in values._p_.get_cached(c) + assert 'u3' not in settings._p_.get_cached(c) def test_reset_cache(): @@ -348,3 +376,210 @@ def test_cache_master_slave(): assert set(cache['ip_admin_eth0.ip_admin_eth0'].keys()) == set([None]) assert set(cache['ip_admin_eth0.netmask_admin_eth0'].keys()) == set([None, 0, 1]) #DEL, insert, ... + + +def return_value(value=None): + return value + + +def test_cache_callback(): + val1 = StrOption('val1', "", 'val') + val2 = StrOption('val2', "", callback=return_value, callback_params={'': ((val1, False),)}, properties=('mandatory',)) + val3 = StrOption('val3', "", callback=return_value, callback_params={'': ('yes',)}) + val4 = StrOption('val4', "", callback=return_value, callback_params={'value': ((val1, False),)}) + val5 = StrOption('val5', "", callback=return_value, callback_params={'value': ('yes',)}, multi=True) + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3, val4, val5]) + cfg = Config(maconfig) + cfg.cfgimpl_get_settings().remove('expire') + cfg.read_write() + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('val', None)}, + 'val2': {None: ('val', None)}, + 'val3': {None: ('yes', None)}, + 'val4': {None: ('val', None)}, + 'val5': {None: (['yes'], None)}} + cfg.val1 = 'new' + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val3': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val3': {None: ('yes', None)}, + 'val5': {None: (['yes'], None)}} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('yes', None)}, + 'val4': {None: ('new', None)}, + 'val5': {None: (['yes'], None)}} + cfg.val3 = 'new2' + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val4': {None: ('new', None)}, + 'val5': {None: (['yes'], None)}} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('new2', None)}, + 'val4': {None: ('new', None)}, + 'val5': {None: (['yes'], None)}} + cfg.val4 = 'new3' + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('new2', None)}, + 'val5': {None: (['yes'], None)}} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('new2', None)}, + 'val4': {None: ('new3', None)}, + 'val5': {None: (['yes'], None)}} + cfg.val5.append('new4') + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('new2', None)}, + 'val4': {None: ('new3', None)}} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val2': {None: (set(['mandatory']), None)}, + 'val3': {None: (set([]), None)}, + 'val4': {None: (set([]), None)}, + 'val5': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1': {None: ('new', None)}, + 'val2': {None: ('new', None)}, + 'val3': {None: ('new2', None)}, + 'val4': {None: ('new3', None)}, + 'val5': {None: (['yes', 'new4'], None)}} + + +def test_cache_master_and_slaves_master(): + val1 = StrOption('val1', "", multi=True) + val2 = StrOption('val2', "", multi=True) + interface1 = OptionDescription('val1', '', [val1, val2]) + interface1.impl_set_group_type(groups.master) + maconfig = OptionDescription('rootconfig', '', [interface1]) + cfg = Config(maconfig) + cfg.cfgimpl_get_settings().remove('expire') + cfg.read_write() + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}, + 'val1.val2': {None: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([], None)}, 'val1.val2': {None: ([], None)}} + cfg.val1.val1.append() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}, + 'val1.val2': {None: (set([]), None), 0: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([None], None)}, + 'val1.val2': {None: ([None], None), 0: (None, None)}} + cfg.val1.val1.append() + cfg.cfgimpl_get_values().force_cache() + cfg.val1.val2[1] = 'oui' + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([None, None], None)}} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}, + 'val1.val2': {None: (set([]), None), 0: (set([]), None), 1: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([None, None], None)}, + 'val1.val2': {None: ([None, 'oui'], None), 0: (None, None), 1: ('oui', None)}} + + +def test_cache_master_callback(): + val1 = StrOption('val1', "", multi=True) + val2 = StrOption('val2', "", multi=True, callback=return_value, callback_params={'value': ((val1, False),)}) + interface1 = OptionDescription('val1', '', [val1, val2]) + interface1.impl_set_group_type(groups.master) + maconfig = OptionDescription('rootconfig', '', [interface1]) + cfg = Config(maconfig) + cfg.cfgimpl_get_settings().remove('expire') + cfg.read_write() + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}, + 'val1.val2': {None: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([], None)}, 'val1.val2': {None: ([], None)}} + cfg.val1.val1.append() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {} + cfg.cfgimpl_get_values().force_cache() + assert cfg.cfgimpl_get_settings()._p_.get_cached(cfg) == {'val1': {None: (set([]), None)}, + 'val1.val1': {None: (set(['empty']), None)}, + 'val1.val2': {None: (set([]), None), 0: (set([]), None)}} + assert cfg.cfgimpl_get_values()._p_.get_cached(cfg) == {'val1.val1': {None: ([None], None)}, + 'val1.val2': {None: ([None], None), 0: (None, None)}} + + +def test_cache_requires(): + a = BoolOption('activate_service', '', True) + b = IPOption('ip_address_service', '', + requires=[{'option': a, 'expected': False, 'action': 'disabled'}]) + od = OptionDescription('service', '', [a, b]) + c = Config(od) + c.cfgimpl_get_settings().remove('expire') + c.read_write() + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {} + assert c.cfgimpl_get_values()._p_.get_cached(c) == {} + assert c.ip_address_service == None + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {'activate_service': {None: (set([]), None)}, + 'ip_address_service': {None: (set([]), None)}} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {'ip_address_service': {None: (None, None)}} + c.cfgimpl_get_values().force_cache() + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {'activate_service': {None: (set([]), None)}, + 'ip_address_service': {None: (set([]), None)}} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {'ip_address_service': {None: (None, None)}, + 'activate_service': {None: (True, None)}} + c.ip_address_service = '1.1.1.1' + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {'activate_service': {None: (set([]), None)}} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {'activate_service': {None: (True, None)}} + c.cfgimpl_get_values().force_cache() + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {'activate_service': {None: (set([]), None)}, + 'ip_address_service': {None: (set([]), None)}} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {'ip_address_service': {None: ('1.1.1.1', None)}, + 'activate_service': {None: (True, None)}} + c.activate_service = False + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {} + c.cfgimpl_get_values().force_cache() + assert c.cfgimpl_get_settings()._p_.get_cached(c) == {'activate_service': {None: (set([]), None)}, + 'ip_address_service': {None: (set(['disabled']), None)}} + + assert c.cfgimpl_get_values()._p_.get_cached(c) == {'activate_service': {None: (False, None)}} diff --git a/test/test_state.py b/test/test_state.py index dd56b63..7a78ebc 100644 --- a/test/test_state.py +++ b/test/test_state.py @@ -3,7 +3,7 @@ do_autopath() from tiramisu.option import BoolOption, UnicodeOption, SymLinkOption, \ IntOption, IPOption, NetmaskOption, StrOption, OptionDescription, \ - DynOptionDescription + DynOptionDescription, MasterSlaves from tiramisu.config import Config, GroupConfig, MetaConfig from tiramisu.setting import groups, owners from tiramisu.storage import delete_session @@ -118,8 +118,15 @@ def _diff_opt(opt1, opt2): assert val1.impl_getname() == val2.impl_getname() except AttributeError: assert val1 == val2 + elif attr == '_dependencies': + assert len(val1) == len(val2) + for idx, val in enumerate(val1): + if isinstance(val, MasterSlaves): + assert val._p_.master.impl_getname() == val2[idx]._p_.master.impl_getname() + else: + assert val.impl_getname() == val2[idx].impl_getname() else: - assert val1 == val2 + assert val1 == val2, "error for {}".format(attr) def _diff_opts(opt1, opt2): diff --git a/tiramisu/__init__.py b/tiramisu/__init__.py index f5f3f32..b92b780 100644 --- a/tiramisu/__init__.py +++ b/tiramisu/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/autolib.py b/tiramisu/autolib.py index 1ceb0a7..3c4530b 100644 --- a/tiramisu/autolib.py +++ b/tiramisu/autolib.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/config.py b/tiramisu/config.py index 7fd8269..cc61b29 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -21,6 +21,7 @@ "options handler global entry point" import weakref import sys +from time import time from .error import PropertiesOptionError, ConfigError, ConflictError @@ -70,10 +71,78 @@ class SubConfig(object): self._impl_context = context self._impl_path = subpath - def cfgimpl_reset_cache(self, only_expired=False, only=('values', - 'settings')): - "remove cache (in context)" - self._cfgimpl_get_context().cfgimpl_reset_cache(only_expired, only) # pragma: optional cover + def cfgimpl_reset_cache(self, + only_expired=False, + only=('values', 'settings'), + opt=None, + path=None): + """reset all settings in cache + + :param only_expired: if True reset only expired cached values + :type only_expired: boolean + """ + context = self._cfgimpl_get_context() + if 'values' in only: + values = context.cfgimpl_get_values() + if 'settings' in only: + settings = context.cfgimpl_get_settings() + if only_expired: + if 'values' in only: + values._p_.reset_expired_cache(int(time())) + if 'settings' in only: + settings._p_.reset_expired_cache(int(time())) + elif not None in (opt, path): + if opt.__class__.__name__ == 'DynOptionDescription': + descr = context.cfgimpl_get_description() + spath = path.split('.') + subpath = '.'.join(spath[:-1]) + dynopt = getattr(descr, subpath)._getattr(spath[-1], context=context, + dyn=False) + for suffix in dynopt._impl_get_suffixes(context): + path = subpath + '.' + spath[-1] + suffix + if 'values' in only: + values._p_.delcache(path) + if 'settings' in only: + settings._p_.delcache(path) + elif not isinstance(opt, DynSymLinkOption) and opt._is_subdyn(): + descr = context.cfgimpl_get_description() + spath = path.split('.') + try: + subpath = '.'.join(spath[:-2]) + dynsubopt = getattr(descr, subpath) + spath1 = spath[-2] + spath2 = spath[-1] + spath3 = None + except AttributeError: + subpath = '.'.join(spath[:-3]) + dynsubopt = getattr(descr, subpath) + spath1 = spath[-3] + spath2 = spath[-2] + spath3 = spath[-1] + dynopt = dynsubopt._getattr(spath1, context=context, dyn=False) + for suffix in dynopt._impl_get_suffixes(context): + path = subpath + '.' + spath1 + suffix + '.' + spath2 + suffix + if spath3: + path += '.' + spath3 + suffix + if 'values' in only: + values._p_.delcache(path) + if 'settings' in only: + settings._p_.delcache(path) + else: + if 'values' in only: + values._p_.delcache(path) + if 'settings' in only: + settings._p_.delcache(path) + for option in getattr(opt, '_dependencies', []): + if 'values' in only: + option.reset_cache(opt, values, 'values') + if 'settings' in only: + option.reset_cache(opt, settings, 'settings') + else: + if 'values' in only: + values._p_.reset_all_cache() + if 'settings' in only: + settings._p_.reset_all_cache() def cfgimpl_get_home_by_path(self, path, force_permissive=False, returns_raise=False): @@ -678,7 +747,7 @@ class Config(_CommonConfig): def __init__(self, descr, session_id=None, persistent=False, name=undefined, force_values=None, force_settings=None, - _duplicate=False): + _duplicate=False, mandatory_name=False): """ Configuration option management master class :param descr: describes the configuration schema @@ -700,6 +769,8 @@ class Config(_CommonConfig): name = 'config' if session_id is not None: name += session_id + if mandatory_name and name is None: + raise ValueError(_("name is mandatory for the config").format(name)) if name is not None and not valid_name(name): # pragma: optional cover raise ValueError(_("invalid name: {0} for config").format(name)) self._impl_settings = Settings(self, settings) @@ -712,14 +783,6 @@ class Config(_CommonConfig): self._impl_build_all_caches() self._impl_name = name - def cfgimpl_reset_cache(self, - only_expired=False, - only=('values', 'settings')): - if 'values' in only: - self.cfgimpl_get_values().reset_cache(only_expired=only_expired) - if 'settings' in only: - self.cfgimpl_get_settings().reset_cache(only_expired=only_expired) - def impl_getname(self): return self._impl_name @@ -763,19 +826,15 @@ class GroupConfig(_CommonConfig): def cfgimpl_get_children(self): return self._impl_children - #def cfgimpl_get_context(self): - # "a meta config is a config which has a setting, that is itself" - # return self - def cfgimpl_reset_cache(self, only_expired=False, - only=('values', 'settings')): - if 'values' in only: - self.cfgimpl_get_values().reset_cache(only_expired=only_expired) - if 'settings' in only: - self.cfgimpl_get_settings().reset_cache(only_expired=only_expired) + only=('values', 'settings'), + opt=None, + path=None): + if isinstance(self, MetaConfig): + super(GroupConfig, self).cfgimpl_reset_cache(only_expired=only_expired, only=only, opt=opt, path=path) for child in self._impl_children: - child.cfgimpl_reset_cache(only_expired=only_expired, only=only) + child.cfgimpl_reset_cache(only_expired=only_expired, only=only, opt=opt, path=path) def set_value(self, path, value): """Setattr not in current GroupConfig, but in each children @@ -906,3 +965,7 @@ class MetaConfig(GroupConfig): setattr(child, path, child_value) setattr(self, path, value) + + def new_config(self, session_id=None, name=undefined): + return Config(self._impl_descr, _duplicate=True, session_id=session_id, name=name, + mandatory_name=True) diff --git a/tiramisu/error.py b/tiramisu/error.py index 987263e..a904bd3 100644 --- a/tiramisu/error.py +++ b/tiramisu/error.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/i18n.py b/tiramisu/i18n.py index b9931b8..580767a 100644 --- a/tiramisu/i18n.py +++ b/tiramisu/i18n.py @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index a2435ef..13c99d5 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2014-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -25,7 +25,7 @@ import sys from inspect import getargspec from ..i18n import _ -from ..setting import log, undefined, debug +from ..setting import log, undefined, debug, groups from ..autolib import carry_out_calculation from ..error import (ConfigError, ValueWarning, PropertiesOptionError, display_list) @@ -58,7 +58,7 @@ def valid_name(name): return False -def validate_callback(callback, callback_params, type_): +def validate_callback(callback, callback_params, type_, callbackoption): if not isinstance(callback, FunctionType): raise ValueError(_('{0} must be a function').format(type_)) if callback_params is not None: @@ -94,6 +94,17 @@ def validate_callback(callback, callback_params, type_): ' not a {} for second argument' ).format(type_, type( force_permissive))) + if isinstance(option, SymLinkOption): + cur_opt = option._impl_getopt() + else: + cur_opt = option + if cur_opt != callbackoption: + if not getattr(cur_opt, '_dependencies', None): + options = [] + else: + options = list(cur_opt._dependencies) + options.append(callbackoption) + cur_opt._dependencies = tuple(options) #____________________________________________________________ # @@ -127,7 +138,7 @@ class Base(StorageBase): if not is_multi and unique is True: raise ValueError(_('unique must be set only with multi value')) if requires is not None: - calc_properties, requires = validate_requires_arg(is_multi, + calc_properties, requires = validate_requires_arg(self, is_multi, requires, name) else: calc_properties = frozenset() @@ -143,7 +154,7 @@ class Base(StorageBase): if multi: # and validator_params is None: validator_params = self._build_validator_params(validator, validator_params) - validate_callback(validator, validator_params, 'validator') + validate_callback(validator, validator_params, 'validator', self) self._set_validator(validator, validator_params) self._set_has_dependency() if calc_properties != frozenset([]) and properties is not tuple(): @@ -217,7 +228,7 @@ class Base(StorageBase): "cannot set another one's").format(self.impl_getname())) self._validate_callback(callback, callback_params) if callback is not None: - validate_callback(callback, callback_params, 'callback') + validate_callback(callback, callback_params, 'callback', self) self._set_callback(callback, callback_params) def impl_is_optiondescription(self): @@ -239,6 +250,36 @@ class BaseOption(Base): # ____________________________________________________________ # serialize object + def _impl_convert_dependencies(self, descr, load=False): + """export of the requires during the serialization process + + :type descr: :class:`tiramisu.option.OptionDescription` + :param load: `True` if we are at the init of the option description + :type load: bool + """ + if not load and getattr(self, '_dependencies', None) is None: + self._state_dependencies = None + elif load and self._state_dependencies is None: + del(self._state_dependencies) + else: + if load: + self._dependencies = [] + for dependency in self._state_dependencies: + option = descr.impl_get_opt_by_path(dependency) + if option.impl_is_optiondescription() and \ + option.impl_get_group_type() == groups.master: + master_path = dependency + '.' + dependency.split('.')[-1] + option = descr.impl_get_opt_by_path(master_path).impl_get_master_slaves() + self._dependencies.append(option) + del(self._state_dependencies) + else: + self._state_dependencies = [] + for dependency in self._dependencies: + if isinstance(dependency, MasterSlaves): + self._state_dependencies.append('.'.join(descr.impl_get_path_by_opt(dependency._p_.master).split('.')[:-1])) + else: + self._state_dependencies.append(descr.impl_get_path_by_opt(dependency)) + def _impl_convert_requires(self, descr, load=False): """export of the requires during the serialization process @@ -417,6 +458,14 @@ class BaseOption(Base): name = name.encode('utf8') return name + def reset_cache(self, opt, obj, type_): + context = obj._getcontext() + path = self.impl_getpath(context) + obj._p_.delcache(path) + context.cfgimpl_reset_cache(only=(type_,), + opt=self, + path=path) + class OnlyOption(BaseOption): __slots__ = tuple() @@ -927,7 +976,7 @@ class Option(OnlyOption): "is calculated").format(self.impl_getname())) -def validate_requires_arg(multi, requires, name): +def validate_requires_arg(new_option, multi, requires, name): """check malformed requirements and tranform dict to internal tuple @@ -936,6 +985,14 @@ def validate_requires_arg(multi, requires, name): know more about the description of the requires dictionary """ + def set_dependency(option): + if not getattr(option, '_dependencies', None): + options = [] + else: + options = list(option._dependencies) + options.append(new_option) + option._dependencies = tuple(options) + def get_option(require): option = require['option'] if not isinstance(option, Option): @@ -945,6 +1002,7 @@ def validate_requires_arg(multi, requires, name): raise ValueError(_('malformed requirements ' 'multi option must not set ' 'as requires of non multi option {0}').format(name)) + set_dependency(option) return option def _set_expected(action, inverse, transitive, same_action, option, expected, operator): @@ -970,6 +1028,7 @@ def validate_requires_arg(multi, requires, name): raise ValueError(_('malformed requirements expected must have ' 'option and value for option {0}').format(name)) option = exp['option'] + set_dependency(option) if option is not None: err = option._validate(exp['value']) if err: diff --git a/tiramisu/option/masterslave.py b/tiramisu/option/masterslave.py index 4f7e61c..5fded26 100644 --- a/tiramisu/option/masterslave.py +++ b/tiramisu/option/masterslave.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "master slave support" -# Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2014-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -275,3 +275,8 @@ class MasterSlaves(object): raise SlaveError(_("invalid len for the slave: {0}" " which has {1} as master").format( name, self.getmaster(opt).impl_getname())) + + def reset_cache(self, opt, values, type_): + for slave in self.getslaves(opt): + slave_path = slave.impl_getpath(values._getcontext()) + values._p_.delcache(slave_path) diff --git a/tiramisu/option/option.py b/tiramisu/option/option.py index 90634ae..9997be5 100644 --- a/tiramisu/option/option.py +++ b/tiramisu/option/option.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "option types and option description" -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -49,7 +49,7 @@ class ChoiceOption(Option): :param values: is a list of values the option can possibly take """ if isinstance(values, FunctionType): - validate_callback(values, values_params, 'values') + validate_callback(values, values_params, 'values', self) else: if values_params is not None: raise ValueError(_('values is not a function, so values_params must be None')) diff --git a/tiramisu/option/optiondescription.py b/tiramisu/option/optiondescription.py index 991274b..32abf7b 100644 --- a/tiramisu/option/optiondescription.py +++ b/tiramisu/option/optiondescription.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2014-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -125,6 +125,13 @@ class OptionDescription(BaseOption, StorageOptionDescription): cache_option, force_store_values) #cannot set multi option as OptionDescription requires else: + if option.impl_is_master_slaves('master'): + if not getattr(option, '_dependencies', None): + options = [] + else: + options = list(option._dependencies) + options.append(option.impl_get_master_slaves()) + option._dependencies = tuple(options) option._set_readonly(True) is_multi = option.impl_is_multi() if not isinstance(option, SymLinkOption) and 'force_store_value' in option.impl_getproperties(): @@ -132,8 +139,8 @@ class OptionDescription(BaseOption, StorageOptionDescription): for func, all_cons_opts, params in option._get_consistencies(): option._valid_consistencies(all_cons_opts[1:], init=False) if func not in allowed_const_list and is_multi: - is_slave = option.impl_is_master_slaves() - if not is_slave: + is_masterslaves = option.impl_is_master_slaves() + if not is_masterslaves: raise ValueError(_('malformed consistency option "{0}" ' 'must be a master/slaves').format( option.impl_getname())) @@ -178,7 +185,6 @@ class OptionDescription(BaseOption, StorageOptionDescription): 'must not be a multi for {1}').format( require_opt.impl_getname(), option.impl_getname())) if init: - session = config._impl_values._p_.getsession() if len(cache_option) != len(set(cache_option)): for idx in xrange(1, len(cache_option) + 1): opt = cache_option.pop(0) @@ -194,7 +200,6 @@ class OptionDescription(BaseOption, StorageOptionDescription): self._cache_consistencies[opt] = tuple(cons) self._cache_force_store_values = force_store_values self._set_readonly(False) - del(session) def impl_build_force_store_values(self, config): @@ -408,8 +413,7 @@ class SynDynOptionDescription(object): def _impl_getchildren(self, dyn=True, context=undefined): children = [] for child in self._opt._impl_getchildren(): - children.append(self._opt._impl_get_dynchild(child, self._suffix)) - return children + yield(self._opt._impl_get_dynchild(child, self._suffix)) def impl_getchildren(self): return self._impl_getchildren() diff --git a/tiramisu/setting.py b/tiramisu/setting.py index ce3d1e5..7a1cc90 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "sets the options of the configuration objects Config object itself" -# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -269,7 +269,7 @@ class Property(object): raise ConfigError(_('cannot add those properties: {0}').format(propname)) self._properties.add(propname) if save: - self._setting._setproperties(self._properties, self._path, force=True) + self._setting._setproperties(self._properties, self._opt, self._path, force=True) def remove(self, propname): """Removes a property named propname @@ -279,7 +279,7 @@ class Property(object): """ if propname in self._properties: self._properties.remove(propname) - self._setting._setproperties(self._properties, self._path) + self._setting._setproperties(self._properties, self._opt, self._path) def extend(self, propnames): """Extends properties to the existing properties @@ -289,7 +289,7 @@ class Property(object): """ for propname in propnames: self._append(propname, save=False) - self._setting._setproperties(self._properties, self._path) + self._setting._setproperties(self._properties, self._opt, self._path) def reset(self): """resets the properties (does not **clear** the properties, @@ -370,7 +370,7 @@ class Settings(object): if opt is not None and _path is None: _path = opt.impl_getpath(self._getcontext()) self._p_.delproperties(_path) - self._getcontext().cfgimpl_reset_cache() + self._getcontext().cfgimpl_reset_cache(opt=opt, path=_path) def _getproperties(self, opt=None, path=None, setting_properties=undefined, read_write=True, @@ -415,20 +415,20 @@ class Settings(object): props = self._p_.getproperties(None, default_properties) if propname not in props: props.add(propname) - self._setproperties(props, None) + self._setproperties(props, None, None) def remove(self, propname): "deletes property propname in the Config's properties attribute" props = self._p_.getproperties(None, default_properties) if propname in props: props.remove(propname) - self._setproperties(props, None) + self._setproperties(props, None, None) def extend(self, propnames): for propname in propnames: self.append(propname) - def _setproperties(self, properties, path, force=False): + def _setproperties(self, properties, opt, path, force=False): """save properties for specified path (never save properties if same has option properties) """ @@ -438,7 +438,7 @@ class Settings(object): raise ConfigError(_('cannot add those properties: {0}').format( ' '.join(forbidden_properties))) self._p_.setproperties(path, properties) - self._getcontext().cfgimpl_reset_cache() + self._getcontext().cfgimpl_reset_cache(opt=opt, path=path) #____________________________________________________________ def validate_properties(self, opt_or_descr, is_descr, check_frozen, path, @@ -541,7 +541,7 @@ class Settings(object): if not isinstance(permissive, tuple): # pragma: optional cover raise TypeError(_('permissive must be a tuple')) self._p_.setpermissive(path, permissive) - self._getcontext().cfgimpl_reset_cache() + self._getcontext().cfgimpl_reset_cache(opt=opt, path=path) #____________________________________________________________ def setowner(self, owner): @@ -564,7 +564,7 @@ class Settings(object): props = props | append modified = True if modified: - self._setproperties(props, None) + self._setproperties(props, None, None) def read_only(self): "convenience method to freeze, hide and disable" @@ -574,17 +574,6 @@ class Settings(object): "convenience method to freeze, hide and disable" self._read(rw_remove, rw_append) - def reset_cache(self, only_expired): - """reset all settings in cache - - :param only_expired: if True reset only expired cached values - :type only_expired: boolean - """ - if only_expired: - self._p_.reset_expired_cache(int(time())) - else: - self._p_.reset_all_cache() - def apply_requires(self, opt, path, setting_properties, index, debug): """carries out the jit (just in time) requirements between options diff --git a/tiramisu/storage/dictionary/__init__.py b/tiramisu/storage/dictionary/__init__.py index cf7e03d..9ff39ef 100644 --- a/tiramisu/storage/dictionary/__init__.py +++ b/tiramisu/storage/dictionary/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2013-2014 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/storage/dictionary/option.py b/tiramisu/storage/dictionary/option.py index 716eab0..b866330 100644 --- a/tiramisu/storage/dictionary/option.py +++ b/tiramisu/storage/dictionary/option.py @@ -56,6 +56,7 @@ class StorageBase(object): '_choice_values_params', #other '_has_dependency', + '_dependencies', '_state_master_slaves', '_state_val_call', '_state_requires', @@ -64,8 +65,9 @@ class StorageBase(object): '_state_informations', '_state_extra', '_state_readonly', + '_state_dependencies', '__weakref__' - ) + ) def __init__(self, name, multi, warnings_only, doc, extra, calc_properties, requires, properties, allow_empty_list, unique, opt=undefined, @@ -588,10 +590,10 @@ class StorageMasterSlaves(object): def __init__(self, master, slaves): self.master = master - self.slaves = slaves + self.slaves = tuple(slaves) def _sm_getmaster(self): return self.master def _sm_getslaves(self): - return tuple(self.slaves) + return self.slaves diff --git a/tiramisu/storage/dictionary/setting.py b/tiramisu/storage/dictionary/setting.py index 2d8ab06..9022bc1 100644 --- a/tiramisu/storage/dictionary/setting.py +++ b/tiramisu/storage/dictionary/setting.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "default plugin for setting: set it in a simple dictionary" -# Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/storage/dictionary/storage.py b/tiramisu/storage/dictionary/storage.py index c868928..e356b07 100644 --- a/tiramisu/storage/dictionary/storage.py +++ b/tiramisu/storage/dictionary/storage.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/storage/dictionary/value.py b/tiramisu/storage/dictionary/value.py index 0a03e99..a37388a 100644 --- a/tiramisu/storage/dictionary/value.py +++ b/tiramisu/storage/dictionary/value.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "default plugin for value: set it in a simple dictionary" -# Copyright (C) 2013-2014 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/storage/sqlite3/__init__.py b/tiramisu/storage/sqlite3/__init__.py index a9f67d7..3bd8e0c 100644 --- a/tiramisu/storage/sqlite3/__init__.py +++ b/tiramisu/storage/sqlite3/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the diff --git a/tiramisu/storage/util.py b/tiramisu/storage/util.py index 064d0a1..c0c55f5 100644 --- a/tiramisu/storage/util.py +++ b/tiramisu/storage/util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- "utils used by storage" -# Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) +# Copyright (C) 2013-2017 Team tiramisu (see AUTHORS for all contributors) # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the @@ -98,6 +98,9 @@ class Cache(object): setattr(self, key, value) def setcache(self, path, val, time, index): + """add val in cache for a specified path + if slave, add index + """ self._cache.setdefault(path, {})[index] = (val, time) def getcache(self, path, exp, index): @@ -106,6 +109,12 @@ class Cache(object): return True, value return False, None # pragma: no cover + def delcache(self, path): + """remove cache for a specified path + """ + if path in self._cache: + del self._cache[path] + def hascache(self, path, index): """ path is in the cache diff --git a/tiramisu/value.py b/tiramisu/value.py index afe8ea5..82e4ce9 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -201,7 +201,7 @@ class Values(object): self._setvalue(opt, path, value, force_owner=owners.forced) else: self._p_.resetvalue(path, session) - context.cfgimpl_reset_cache() + context.cfgimpl_reset_cache(opt=opt, path=path) def _isempty(self, opt, value, force_allow_empty_list=False, index=None): "convenience method to know if an option is empty" @@ -427,7 +427,7 @@ class Values(object): def _setvalue(self, opt, path, value, force_owner=undefined, index=None): context = self._getcontext() - context.cfgimpl_reset_cache() + context.cfgimpl_reset_cache(opt=opt, path=path) if force_owner is undefined: owner = context.cfgimpl_get_settings().getowner() else: @@ -599,15 +599,6 @@ class Values(object): index=index, force_permissive=force_permissive) return d == owners.default - def reset_cache(self, only_expired): - """ - clears the cache if necessary - """ - if only_expired: - self._p_.reset_expired_cache(int(time())) - else: - self._p_.reset_all_cache() - # information def set_information(self, key, value): """updates the information's attribute @@ -699,8 +690,7 @@ class Values(object): if not 'cache' in context.cfgimpl_get_settings(): raise ConfigError(_('can force cache only if cache ' 'is actived in config')) - #remove all cached properties and value to update "expired" time - context.cfgimpl_reset_cache() + #FIXME properties and value should update "expired" time for path in context.cfgimpl_get_description().impl_getpaths( include_groups=True): err = context.getattr(path, returns_raise=True)