From a1dd2cfce76d4f07c4365cf6f2f0de3735ce14cc Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 27 Jan 2014 17:16:05 +0100 Subject: [PATCH] require works well in sqlalchemy storage --- test/test_option_setting.py | 4 +- tiramisu/option.py | 84 +++++++++++------- tiramisu/setting.py | 69 +++++++-------- tiramisu/storage/sqlalchemy/option.py | 119 ++++++++++++++++---------- 4 files changed, 167 insertions(+), 109 deletions(-) diff --git a/test/test_option_setting.py b/test/test_option_setting.py index 044b61b..e857a8b 100644 --- a/test/test_option_setting.py +++ b/test/test_option_setting.py @@ -311,10 +311,10 @@ def test_append_properties(): cfg = Config(descr) setting = cfg.cfgimpl_get_settings() option = cfg.cfgimpl_get_description().gc.dummy - assert tuple(option.impl_getproperties()) == tuple() + assert tuple(option._properties) == tuple() assert not 'test' in setting[option] setting[option].append('test') - assert tuple(option.impl_getproperties()) == tuple() + assert tuple(option._properties) == tuple() assert 'test' in setting[option] diff --git a/tiramisu/option.py b/tiramisu/option.py index 014be90..587772d 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -67,11 +67,9 @@ class Base(StorageBase): raise ValueError(_("invalid name: {0} for option").format(name)) self._name = name self.impl_set_information('doc', doc) - requires = validate_requires_arg(requires, self._name) if requires is not None: - for values in requires.values(): - for require in values.values(): - self._add_require(require) + self._calc_properties, self._requires = validate_requires_arg( + requires, self._name) if not multi and default_multi is not None: raise ValueError(_("a default_multi is set whereas multi is False" " in option: {0}").format(name)) @@ -118,8 +116,8 @@ class Base(StorageBase): if callback_params is not None: for key, values in callback_params.items(): self._add_callback(key, values) - if requires is not None and properties is not tuple(): - set_forbidden_properties = set(properties) & set(requires.keys()) + if self._calc_properties != frozenset([]) and properties is not tuple(): + set_forbidden_properties = self._calc_properties & set(properties) if set_forbidden_properties != frozenset(): raise ValueError('conflict: properties already set in ' 'requirement {0}'.format( @@ -133,8 +131,9 @@ class Base(StorageBase): self._default = [] else: self._default = default - for prop in properties: - self._properties.append(self._get_property_object(prop)) + self._properties = properties + #for prop in properties: + #self._properties.append(self._get_property_object(prop)) self._warnings_only = warnings_only return super(Base, self).__init__() @@ -148,6 +147,29 @@ class BaseOption(Base): # '_calc_properties', '_impl_informations', # '_state_readonly', '_state_requires', '_stated') + # information + def impl_set_information(self, key, value): + """updates the information's attribute + (which is a dictionary) + + :param key: information's key (ex: "help", "doc" + :param value: information's value (ex: "the help string") + """ + self._informations[key] = value + + def impl_get_information(self, key, default=None): + """retrieves one information's item + + :param key: the item string (ex: "help") + """ + if key in self._informations: + return self._informations[key] + elif default is not None: + return default + else: + raise ValueError(_("information's item not found: {0}").format( + key)) + # ____________________________________________________________ # serialize object def _impl_convert_requires(self, descr, load=False): @@ -349,9 +371,9 @@ class Option(BaseOption): # name)) # object.__setattr__(self, name, value) - def impl_getproperties(self): - for prop in self._properties: - yield(prop.name) + #def impl_getproperties(self): + # for prop in self._properties: + # yield(prop.name) def impl_getrequires(self): return self._requires @@ -1161,10 +1183,10 @@ class OptionDescription(BaseOption, StorageOptionDescription): # the group_type is useful for filtering OptionDescriptions in a config self._optiondescription_group_type = groups.default - def impl_getproperties(self): - #FIXME - for prop in self._properties: - yield(prop.name) + #def impl_getproperties(self): + # #FIXME + # for prop in self._properties: + # yield(prop.name) def impl_getrequires(self): #FIXME @@ -1223,14 +1245,16 @@ class OptionDescription(BaseOption, StorageOptionDescription): #for child in self._children: # yield(session.query(child._type).filter_by(id=child.id).first()) - def impl_build_cache_consistency(self, _consistencies=None): + def impl_build_cache_consistency(self, _consistencies=None, cache_option=None): #FIXME cache_option ! if _consistencies is None: init = True _consistencies = {} + cache_option = [] else: init = False for option in self.impl_getchildren(): + cache_option.append(option.id) if not isinstance(option, OptionDescription): for consistency in option._consistencies: func = consistency.func @@ -1240,15 +1264,15 @@ class OptionDescription(BaseOption, StorageOptionDescription): []).append((func, all_cons_opts)) else: - option.impl_build_cache_consistency(_consistencies) + option.impl_build_cache_consistency(_consistencies, cache_option) if init and _consistencies != {}: self._cache_consistencies = {} for opt, cons in _consistencies.items(): #FIXME dans le cache ... - #if opt.id not in cache_option: - # raise ConfigError(_('consistency with option {0} ' - # 'which is not in Config').format( - # opt.impl_getname())) + if opt.id not in cache_option: + raise ConfigError(_('consistency with option {0} ' + 'which is not in Config').format( + opt.impl_getname())) self._cache_consistencies[opt] = tuple(cons) def impl_validate_options(self, cache_option=None): @@ -1509,15 +1533,15 @@ def validate_requires_arg(requires, name): inverse, transitive, same_action) else: ret_requires[action][option][1].append(expected) - ## transform dict to tuple - #ret = [] - #for opt_requires in ret_requires.values(): - # ret_action = [] - # for require in opt_requires.values(): - # ret_action.append((require[0], tuple(require[1]), require[2], - # require[3], require[4], require[5])) - # ret.append(tuple(ret_action)) - return ret_requires + # transform dict to tuple + ret = [] + for opt_requires in ret_requires.values(): + ret_action = [] + for require in opt_requires.values(): + ret_action.append((require[0], tuple(require[1]), require[2], + require[3], require[4], require[5])) + ret.append(tuple(ret_action)) + return frozenset(config_action.keys()), tuple(ret) def validate_callback(callback, callback_params, type_): diff --git a/tiramisu/setting.py b/tiramisu/setting.py index b491d47..4d7da51 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -385,7 +385,7 @@ class Settings(object): if is_cached: return props #FIXME - props = self._p_.getproperties(path, opt.impl_getproperties()) + props = self._p_.getproperties(path, opt._properties) if is_apply_req: props |= self.apply_requires(opt, path) if 'cache' in self: @@ -589,44 +589,45 @@ class Settings(object): :param path: the option's path in the config :type path: str """ - if opt.impl_getrequires() is None: + if opt._requires is None: return frozenset() # filters the callbacks calc_properties = set() context = self._getcontext() - for require in opt.impl_getrequires(): - expected = tuple(require.get_expected()) - inverse = require.inverse - option = require.option - reqpath = self._get_path_by_opt(option) - if reqpath == path or reqpath.startswith(path + '.'): - raise RequirementError(_("malformed requirements " - "imbrication detected for option:" - " '{0}' with requirement on: " - "'{1}'").format(path, reqpath)) - try: - value = context._getattr(reqpath, - force_permissive=True) - except PropertiesOptionError as err: - if not require.transitive: - continue - properties = err.proptype - if require.same_action and require.action not in properties: - raise RequirementError(_("option '{0}' has " - "requirement's property " - "error: " - "{1} {2}").format(opt.impl_getname(), - reqpath, - properties)) - # transitive action, force expected - value = expected[0] - inverse = False - if not inverse and value in expected or \ - inverse and value not in expected: - calc_properties.add(require.action) - # the calculation cannot be carried out - #break + for requires in opt._requires: + for require in requires: + option, expected, action, inverse, \ + transitive, same_action = require + reqpath = self._get_path_by_opt(option) + if reqpath == path or reqpath.startswith(path + '.'): + raise RequirementError(_("malformed requirements " + "imbrication detected for option:" + " '{0}' with requirement on: " + "'{1}'").format(path, reqpath)) + try: + value = context._getattr(reqpath, + force_permissive=True) + except PropertiesOptionError as err: + if not transitive: + continue + properties = err.proptype + if same_action and action not in properties: + raise RequirementError(_("option '{0}' has " + "requirement's property " + "error: " + "{1} {2}").format(opt._name, + reqpath, + properties)) + # transitive action, force expected + value = expected[0] + inverse = False + if (not inverse and + value in expected or + inverse and value not in expected): + calc_properties.add(action) + # the calculation cannot be carried out + break return calc_properties def _get_path_by_opt(self, opt): diff --git a/tiramisu/storage/sqlalchemy/option.py b/tiramisu/storage/sqlalchemy/option.py index 54c3256..1b7fbca 100644 --- a/tiramisu/storage/sqlalchemy/option.py +++ b/tiramisu/storage/sqlalchemy/option.py @@ -20,10 +20,12 @@ from tiramisu.i18n import _ from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy import create_engine, Column, Integer, String, Boolean, \ PickleType, ForeignKey, Table from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.collections import attribute_mapped_collection #FIXME @@ -39,68 +41,91 @@ SqlAlchemyBase = declarative_base() #_Base : object dans la base de donnée -# => _RequireOption => il y a une liste d'espect dans _RequireExpected # => _PropertyOption => liste des propriétés -# => _Information => dictionnaire avec clef valeur # => _CallbackParam avec des Options -require_table = Table('require', SqlAlchemyBase.metadata, - Column('left_id', Integer, ForeignKey('requireoption.id')), - Column('right_id', Integer, ForeignKey('baseoption.id')) - ) +def load_requires(collection_type, proxy): + def getter(obj): + if obj is None: + return None + ret = [] + requires = getattr(obj, proxy.value_attr) + for require in requires: + option = session.query(_Base).filter_by(id=require.option).first() + ret.append(tuple([option, require.expected, require.action, require.inverse, require.transitive, require.same_action])) + return tuple(ret) + + def setter(obj, value): + setattr(obj, proxy.value_attr, value) + return getter, setter -class _RequireExpected(SqlAlchemyBase): - __tablename__ = 'expected' +class _Require(SqlAlchemyBase): + __tablename__ = "require" id = Column(Integer, primary_key=True) - expected = Column(PickleType) - require = Column(Integer, ForeignKey('requireoption.id')) + requires_id = Column(Integer, ForeignKey("baseoption.id"), nullable=False) + requires = relationship('_RequireOption') - def __init__(self, expected): - self.expected = expected + def __init__(self, requires): + for require in requires: + self.requires.append(_RequireOption(require)) class _RequireOption(SqlAlchemyBase): __tablename__ = 'requireoption' id = Column(Integer, primary_key=True) - option = relationship('_Base', lazy='joined', cascade="all, delete-orphan") - #option = relationship('_Base') - expected = relationship("_RequireExpected") + require_id = Column(Integer, ForeignKey("require.id"), nullable=False) + option = Column(Integer, nullable=False) + _expected = relationship("_RequireExpected", collection_class=list, + cascade="all, delete-orphan") + expected = association_proxy("_expected", "expected") + #expected = Column(String) action = Column(String, nullable=False) inverse = Column(Boolean, default=False) transitive = Column(Boolean, default=True) same_action = Column(Boolean, default=True) - def __init__(self, option, expected, action, inverse, transitive, - same_action): - #self.r_opt = option.id - self.option = option - for expect in expected: - self.expected.append(_RequireExpected(expect)) + def __init__(self, values): + option, expected, action, inverse, transitive, same_action = values + self.option = option.id + self.expected = expected self.action = action self.inverse = inverse self.transitive = transitive self.same_action = same_action - def get_expected(self): - for expected in self.expected: - yield(expected.expected) - #def get_option(self, config): - # return config.cfgimpl_get_description().impl_get_opt_by_id(self.r_opt) +class _RequireExpected(SqlAlchemyBase): + __tablename__ = 'expected' + id = Column(Integer, primary_key=True) + require = Column(Integer, ForeignKey('requireoption.id'), nullable=False) + expected = Column(PickleType) + + def __init__(self, expected): + #FIXME ne pas creer plusieurs fois la meme _expected_ + #FIXME pareil avec calc_properties + self.expected = expected + + +class _CalcProperties(SqlAlchemyBase): + __tablename__ = 'calcproperty' + id = Column(Integer, primary_key=True) + require = Column(Integer, ForeignKey('baseoption.id'), nullable=False) + name = Column(PickleType) + + def __init__(self, name): + #FIXME ne pas creer plusieurs fois la meme _expected_ + #FIXME pareil avec calc_properties + self.name = name #____________________________________________________________ # # properties -property_table = Table('property', SqlAlchemyBase.metadata, - Column('left_id', Integer, ForeignKey('propertyoption.name')), - Column('right_id', Integer, ForeignKey('baseoption.id')) - ) - - class _PropertyOption(SqlAlchemyBase): __tablename__ = 'propertyoption' - name = Column(String, primary_key=True) + id = Column(Integer, primary_key=True) + option = Column(Integer, ForeignKey('baseoption.id'), nullable=False) + name = Column(String) def __init__(self, name): self.name = name @@ -112,7 +137,7 @@ class _PropertyOption(SqlAlchemyBase): class _Information(SqlAlchemyBase): __tablename__ = 'information' id = Column(Integer, primary_key=True) - option = Column(Integer, ForeignKey('baseoption.id')) + option = Column(Integer, ForeignKey('baseoption.id'), nullable=False) key = Column(String) value = Column(PickleType) @@ -184,11 +209,15 @@ class _Base(SqlAlchemyBase): __tablename__ = 'baseoption' id = Column(Integer, primary_key=True) _name = Column(String) - _informations = relationship('_Information') + #FIXME not autoload + _infos = relationship("_Information", + collection_class=attribute_mapped_collection('key'), + cascade="all, delete-orphan") + _informations = association_proxy("_infos", "value") _default = Column(PickleType) _default_multi = Column(PickleType) - _requires = relationship('_RequireOption', secondary=require_table, - backref=backref('self_option', enable_typechecks=False)) + _reqs = relationship("_Require", collection_class=list) + _requires = association_proxy("_reqs", "requires", getset_factory=load_requires) _multi = Column(Boolean) _multitype = Column(String) _callback = Column(PickleType) @@ -197,8 +226,14 @@ class _Base(SqlAlchemyBase): _validator_params = relationship('_CallbackParam') _parent = Column(Integer, ForeignKey('baseoption.id')) _children = relationship('BaseOption', enable_typechecks=False) - _properties = relationship('_PropertyOption', secondary=property_table, - backref=backref('options', enable_typechecks=False)) + #FIXME pas 2 fois la meme properties dans la base ... + #FIXME not autoload + #FIXME normalement tuple ... transforme en set ! + _props = relationship("_PropertyOption", collection_class=set) + _properties = association_proxy("_props", "name") + #FIXME fusion avec expected + _calc_props = relationship("_CalcProperties", collection_class=set) + _calc_properties = association_proxy("_calc_props", "name") _warnings_only = Column(Boolean) _readonly = Column(Boolean, default=False) _consistencies = relationship('_Consistency', secondary=consistency_table, @@ -206,7 +241,6 @@ class _Base(SqlAlchemyBase): _choice_values = Column(PickleType) _choice_open_values = Column(Boolean) _type = Column(String(50)) - _r_option = Column(Integer, ForeignKey('requireoption.id')) __mapper_args__ = { 'polymorphic_identity': 'option', 'polymorphic_on': _type @@ -227,8 +261,8 @@ class _Base(SqlAlchemyBase): prop_obj = _PropertyOption(propname) return prop_obj - def _add_require(self, require): - self._requires.append(_RequireOption(*require)) + #def _add_require(self, require): + # self._requires.append(_RequireOption(*require)) def _add_callback(self, key, values): self._callback_params.append(_CallbackParam(key, values)) @@ -282,7 +316,6 @@ class StorageOptionDescription(object): def impl_get_path_by_opt(self, opt): try: - print opt, type(opt) return self._cache_paths[1][self._cache_paths[0].index(opt.id)] except ValueError: raise AttributeError(_('no option {0} found').format(opt))