From c01f14920d5d6f63d099684b2106aaf96e26e17e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 30 Aug 2013 09:40:28 +0200 Subject: [PATCH 01/50] test more sloted options --- test/test_slots.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/test_slots.py b/test/test_slots.py index 0104e84..524006f 100644 --- a/test/test_slots.py +++ b/test/test_slots.py @@ -4,10 +4,13 @@ from py.test import raises from tiramisu.config import Config, SubConfig from tiramisu.option import ChoiceOption, BoolOption, IntOption, FloatOption, \ - StrOption, OptionDescription, SymLinkOption, UnicodeOption + StrOption, SymLinkOption, UnicodeOption, IPOption, OptionDescription, \ + PortOption, NetworkOption, NetmaskOption, DomainnameOption def test_slots_option(): + c = ChoiceOption('a', '', ('a',)) + raises(AttributeError, "c.x = 1") c = BoolOption('a', '') raises(AttributeError, "c.x = 1") c = IntOption('a', '') @@ -20,10 +23,18 @@ def test_slots_option(): raises(AttributeError, "c.x = 1") c = UnicodeOption('a', '') raises(AttributeError, "c.x = 1") - c = ChoiceOption('a', '', ('a',)) + c = IPOption('a', '') raises(AttributeError, "c.x = 1") c = OptionDescription('a', '', []) raises(AttributeError, "c.x = 1") + c = PortOption('a', '') + raises(AttributeError, "c.x = 1") + c = NetworkOption('a', '') + raises(AttributeError, "c.x = 1") + c = NetmaskOption('a', '') + raises(AttributeError, "c.x = 1") + c = DomainnameOption('a', '') + raises(AttributeError, "c.x = 1") def test_slots_config(): From 5893f8ad721681798bbd108f6c4b730986aeae4c Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 30 Aug 2013 21:15:55 +0200 Subject: [PATCH 02/50] attributes in Option are now read-only if option set in Config (_name is everytime read-only) --- test/test_slots.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ tiramisu/option.py | 47 ++++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/test/test_slots.py b/test/test_slots.py index 524006f..f99484f 100644 --- a/test/test_slots.py +++ b/test/test_slots.py @@ -37,6 +37,84 @@ def test_slots_option(): raises(AttributeError, "c.x = 1") +def test_slots_option_readonly(): + a = ChoiceOption('a', '', ('a',)) + b = BoolOption('b', '') + c = IntOption('c', '') + d = FloatOption('d', '') + e = StrOption('e', '') + g = UnicodeOption('g', '') + h = IPOption('h', '') + i = PortOption('i', '') + j = NetworkOption('j', '') + k = NetmaskOption('k', '') + l = DomainnameOption('l', '') + m = OptionDescription('m', '', [a, b, c, d, e, g, h, i, j, k, l]) + a._requires = 'a' + b._requires = 'b' + c._requires = 'c' + d._requires = 'd' + e._requires = 'e' + g._requires = 'g' + h._requires = 'h' + i._requires = 'i' + j._requires = 'j' + k._requires = 'k' + l._requires = 'l' + m._requires = 'm' + Config(m) + raises(AttributeError, "a._requires = 'a'") + raises(AttributeError, "b._requires = 'b'") + raises(AttributeError, "c._requires = 'c'") + raises(AttributeError, "d._requires = 'd'") + raises(AttributeError, "e._requires = 'e'") + raises(AttributeError, "g._requires = 'g'") + raises(AttributeError, "h._requires = 'h'") + raises(AttributeError, "i._requires = 'i'") + raises(AttributeError, "j._requires = 'j'") + raises(AttributeError, "k._requires = 'k'") + raises(AttributeError, "l._requires = 'l'") + raises(AttributeError, "m._requires = 'm'") + + +def test_slots_option_readonly_name(): + a = ChoiceOption('a', '', ('a',)) + b = BoolOption('b', '') + c = IntOption('c', '') + d = FloatOption('d', '') + e = StrOption('e', '') + f = SymLinkOption('f', c) + g = UnicodeOption('g', '') + h = IPOption('h', '') + i = PortOption('i', '') + j = NetworkOption('j', '') + k = NetmaskOption('k', '') + l = DomainnameOption('l', '') + m = OptionDescription('m', '', [a, b, c, d, e, f, g, h, i, j, k, l]) + raises(AttributeError, "a._name = 'a'") + raises(AttributeError, "b._name = 'b'") + raises(AttributeError, "c._name = 'c'") + raises(AttributeError, "d._name = 'd'") + raises(AttributeError, "e._name = 'e'") + raises(AttributeError, "f._name = 'f'") + raises(AttributeError, "g._name = 'g'") + raises(AttributeError, "h._name = 'h'") + raises(AttributeError, "i._name = 'i'") + raises(AttributeError, "j._name = 'j'") + raises(AttributeError, "k._name = 'k'") + raises(AttributeError, "l._name = 'l'") + raises(AttributeError, "m._name = 'm'") + + +def test_slots_description(): + # __slots__ for OptionDescription must be complete + slots = set() + for subclass in OptionDescription.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + assert slots == set(OptionDescription.__slots__) + + def test_slots_config(): od1 = OptionDescription('a', '', []) od2 = OptionDescription('a', '', [od1]) diff --git a/tiramisu/option.py b/tiramisu/option.py index 031fb50..50625fb 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -91,7 +91,37 @@ class BaseInformation(object): self.__class__.__name__)) -class Option(BaseInformation): +class _ReadOnlyOption(BaseInformation): + __slots__ = ('_readonly',) + + def __setattr__(self, name, value): + is_readonly = False + # never change _name + if name == '_name': + try: + self._name + #so _name is already set + is_readonly = True + except: + pass + try: + if self._readonly is True: + if value is True: + # already readonly and try to re set readonly + # don't raise, just exit + return + is_readonly = True + except AttributeError: + pass + if is_readonly: + raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is" + " read-only").format( + self.__class__.__name__, self._name, + name)) + object.__setattr__(self, name, value) + + +class Option(_ReadOnlyOption): """ Abstract base class for configuration option's. @@ -450,10 +480,9 @@ else: raise ValueError(_('value must be an unicode')) -class SymLinkOption(object): +class SymLinkOption(_ReadOnlyOption): __slots__ = ('_name', '_opt') _opt_type = 'symlink' - _consistencies = None def __init__(self, name, opt): self._name = name @@ -462,9 +491,10 @@ class SymLinkOption(object): 'must be an option ' 'for symlink {0}').format(name)) self._opt = opt + self._readonly = True def __getattr__(self, name): - if name in ('_name', '_opt', '_consistencies'): + if name in ('_name', '_opt', '_opt_type', '_readonly'): return object.__getattr__(self, name) else: return getattr(self._opt, name) @@ -684,13 +714,13 @@ class DomainnameOption(Option): raise ValueError(_('invalid domainname')) -class OptionDescription(BaseInformation): +class OptionDescription(_ReadOnlyOption): """Config's schema (organisation, group) and container of Options The `OptionsDescription` objects lives in the `tiramisu.config.Config`. """ __slots__ = ('_name', '_requires', '_cache_paths', '_group_type', '_properties', '_children', '_consistencies', - '_calc_properties', '__weakref__') + '_calc_properties', '__weakref__', '_readonly', '_impl_informations') def __init__(self, name, doc, children, requires=None, properties=None): """ @@ -731,6 +761,8 @@ class OptionDescription(BaseInformation): return self.impl_get_information('doc') def __getattr__(self, name): + if name in self.__slots__: + return object.__getattribute__(self, name) try: return self._children[1][self._children[0].index(name)] except ValueError: @@ -769,6 +801,7 @@ class OptionDescription(BaseInformation): _currpath=None, _consistencies=None): if _currpath is None and self._cache_paths is not None: + # cache already set return if _currpath is None: save = True @@ -787,6 +820,7 @@ class OptionDescription(BaseInformation): raise ConflictError(_('duplicate option: {0}').format(option)) cache_option.append(option) + option._readonly = True cache_path.append(str('.'.join(_currpath + [attr]))) if not isinstance(option, OptionDescription): if option._consistencies is not None: @@ -807,6 +841,7 @@ class OptionDescription(BaseInformation): if save: self._cache_paths = (tuple(cache_option), tuple(cache_path)) self._consistencies = _consistencies + self._readonly = True def impl_get_opt_by_path(self, path): try: From 82b375ade5ca29e28659c1e1bc0eaf74b9628448 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 31 Aug 2013 09:54:23 +0200 Subject: [PATCH 03/50] - add "make build-pot" to build or update translations/tiramisu.pot files - corrections in error's message in tiramisu/option.py - update tiramisu.pot - update fr's translation --- Makefile | 6 +- tiramisu/option.py | 10 +- translations/fr/tiramisu.po | 287 +++++++++++++++++++++--------------- translations/tiramisu.pot | 244 ++++++++++++++++-------------- 4 files changed, 308 insertions(+), 239 deletions(-) diff --git a/Makefile b/Makefile index fce5f8d..203a6b7 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ clean: #test: clean # py.test +# Build or update Portable Object Base Translation for gettext +build-pot: + pygettext.py -p translations/ -o tiramisu.pot `find tiramisu/ -name "*.py"` + build-lang: $(call build_translation, $(TRADUC_DIR)) @@ -73,4 +77,4 @@ dist: version.in && tar --xform "s,\(.*\),$(PACKAGE)-$(VERSION)/\1," -f $(PACKAGE)-$(VERSION).tar -r version.in \ && gzip -9 $(PACKAGE)-$(VERSION).tar -.PHONY: all clean test install version.in dist +.PHONY: all clean test install version.in dist build-pot diff --git a/tiramisu/option.py b/tiramisu/option.py index 50625fb..d5c11a1 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -83,8 +83,8 @@ class BaseInformation(object): elif default is not None: return default else: - raise ValueError(_("Information's item" - "not found: {0}").format(key)) + raise ValueError(_("information's item" + " not found: {0}").format(key)) except AttributeError: raise AttributeError(_('{0} has no attribute ' 'impl_get_information').format( @@ -704,7 +704,7 @@ class DomainnameOption(Option): raise ValueError(_("invalid value for {0}, must have dot" "").format(self._name)) if len(value) > length: - raise ValueError(_("invalid domainname's length for " + raise ValueError(_("invalid domainname's length for" " {0} (max {1})").format(self._name, length)) if len(value) == 1: raise ValueError(_("invalid domainname's length for {0} (min 2)" @@ -728,7 +728,7 @@ class OptionDescription(_ReadOnlyOption): """ if not valid_name(name): - raise ValueError(_("invalid name: " + raise ValueError(_("invalid name:" " {0} for optiondescription").format(name)) self._name = name self._impl_informations = {} @@ -905,7 +905,7 @@ class OptionDescription(_ReadOnlyOption): raise ValueError(_("no child has same nom has master group" " for: {0}").format(self._name)) else: - raise ValueError(_('group_type : {0}' + raise ValueError(_('group_type: {0}' ' not allowed').format(group_type)) def impl_get_group_type(self): diff --git a/translations/fr/tiramisu.po b/translations/fr/tiramisu.po index 7f1c10d..fd00df1 100644 --- a/translations/fr/tiramisu.po +++ b/translations/fr/tiramisu.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-07-18 15:20+CEST\n" +"POT-Creation-Date: 2013-08-31 09:52+CEST\n" "PO-Revision-Date: \n" "Last-Translator: Emmanuel Garette \n" "Language-Team: LANGUAGE \n" @@ -11,18 +11,18 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.5.4\n" -#: tiramisu/autolib.py:49 +#: tiramisu/autolib.py:58 msgid "no config specified but needed" msgstr "aucune config spécifié alors que c'est nécessaire" -#: tiramisu/autolib.py:56 +#: tiramisu/autolib.py:65 msgid "" "unable to carry out a calculation, option {0} has properties: {1} for: {2}" msgstr "" "impossible d'effectuer le calcul, l'option {0} a les propriétés : {1} pour : " "{2}" -#: tiramisu/autolib.py:65 +#: tiramisu/autolib.py:74 msgid "" "unable to carry out a calculation, option value with multi types must have " "same length for: {0}" @@ -30,86 +30,79 @@ msgstr "" "impossible d'effectuer le calcul, valeur d'un option avec le type multi doit " "avoir la même longueur pour : {0}" -#: tiramisu/config.py:45 +#: tiramisu/config.py:47 msgid "descr must be an optiondescription, not {0}" msgstr "descr doit être une optiondescription pas un {0}" -#: tiramisu/config.py:118 +#: tiramisu/config.py:121 msgid "unknown group_type: {0}" msgstr "group_type inconnu: {0}" -#: tiramisu/config.py:154 -msgid "no optiondescription for this config (may be metaconfig without meta)" +#: tiramisu/config.py:157 +msgid "" +"no option description found for this config (may be metaconfig without meta)" msgstr "" -"pas d'optiondescription pour cette config (par exemple metaconfig sans meta)" +"pas d'option description pour cette config (peut être une metaconfig sans " +"meta)" -#: tiramisu/config.py:312 -msgid "unknown type_ type {0} for _find" +#: tiramisu/config.py:311 +msgid "unknown type_ type {0}for _find" msgstr "type_ type {0} pour _find inconnu" -#: tiramisu/config.py:351 +#: tiramisu/config.py:350 msgid "no option found in config with these criteria" msgstr "aucune option trouvée dans la config avec ces critères" -#: tiramisu/config.py:394 +#: tiramisu/config.py:400 msgid "make_dict can't filtering with value without option" msgstr "make_dict ne peut filtrer sur une valeur mais sans option" -#: tiramisu/config.py:414 +#: tiramisu/config.py:421 msgid "unexpected path {0}, should start with {1}" msgstr "chemin imprévu {0}, devrait commencer par {1}" -#: tiramisu/config.py:527 -msgid "metaconfig's children must be a list" -msgstr "enfants d'une metaconfig doit être une liste" +#: tiramisu/config.py:481 +msgid "opt in getowner must be an option not {0}" +msgstr "opt dans getowner doit être une option pas {0}" -#: tiramisu/config.py:532 -msgid "metaconfig's children must be config, not {0}" -msgstr "enfants d'une metaconfig doit être une config, pas {0}" - -#: tiramisu/config.py:537 -msgid "all config in metaconfig must have same optiondescription" -msgstr "" -"toutes les configs d'une metaconfig doivent avoir la même optiondescription" - -#: tiramisu/config.py:540 -msgid "child has already a metaconfig's" -msgstr "enfant a déjà une metaconfig" - -#: tiramisu/option.py:70 +#: tiramisu/option.py:71 msgid "{0} has no attribute impl_set_information" msgstr "{0} n'a pas d'attribut impl_set_information" -#: tiramisu/option.py:84 -msgid "Information's item not found: {0}" -msgstr "l'élément information non trouvé: {0}" - #: tiramisu/option.py:86 +msgid "information's item not found: {0}" +msgstr "aucune config spécifié alors que c'est nécessaire" + +#: tiramisu/option.py:89 msgid "{0} has no attribute impl_get_information" msgstr "{0} n'a pas d'attribut impl_get_information" -#: tiramisu/option.py:124 +#: tiramisu/option.py:117 +msgid "'{0}' ({1}) object attribute '{2}' is read-only" +msgstr "l'attribut {2} de l'objet '{0}' ({1}) est en lecture seul" + +#: tiramisu/option.py:159 msgid "invalid name: {0} for option" msgstr "nom invalide : {0} pour l'option" -#: tiramisu/option.py:134 +#: tiramisu/option.py:169 msgid "validator must be a function" msgstr "validator doit être une fonction" -#: tiramisu/option.py:141 +#: tiramisu/option.py:176 msgid "a default_multi is set whereas multi is False in option: {0}" msgstr "" "une default_multi est renseigné alors que multi est False dans l'option : {0}" -#: tiramisu/option.py:147 +#: tiramisu/option.py:182 msgid "invalid default_multi value {0} for option {1}: {2}" msgstr "la valeur default_multi est invalide {0} pour l'option {1} : {2}" -#: tiramisu/option.py:150 +#: tiramisu/option.py:187 msgid "default value not allowed if option: {0} is calculated" msgstr "la valeur par défaut n'est pas possible si l'option {0} est calculé" -#: tiramisu/option.py:153 +#: tiramisu/option.py:190 msgid "" "params defined for a callback function but no callback defined yet for " "option {0}" @@ -117,183 +110,183 @@ msgstr "" "params définit pour une fonction callback mais par de callback défini encore " "pour l'option {0}" -#: tiramisu/option.py:174 tiramisu/option.py:718 +#: tiramisu/option.py:212 tiramisu/option.py:753 msgid "invalid properties type {0} for {1}, must be a tuple" msgstr "type des properties invalide {0} pour {1}, doit être un tuple" -#: tiramisu/option.py:273 +#: tiramisu/option.py:285 msgid "invalid value {0} for option {1} for object {2}" msgstr "valeur invalide {0} pour l'option {1} pour l'objet {2}" -#: tiramisu/option.py:278 tiramisu/value.py:368 +#: tiramisu/option.py:293 tiramisu/value.py:468 msgid "invalid value {0} for option {1}: {2}" msgstr "valeur invalide {0} pour l'option {1} : {2}" -#: tiramisu/option.py:290 +#: tiramisu/option.py:305 msgid "invalid value {0} for option {1} which must be a list" msgstr "valeur invalide {0} pour l'option {1} qui doit être une liste" -#: tiramisu/option.py:354 +#: tiramisu/option.py:374 msgid "invalid value {0} for option {1} must be different as {2} option" msgstr "" "valeur invalide {0} pour l'option {1} doit être différent que l'option {2}" -#: tiramisu/option.py:376 +#: tiramisu/option.py:396 msgid "values must be a tuple for {0}" msgstr "values doit être un tuple pour {0}" -#: tiramisu/option.py:379 +#: tiramisu/option.py:399 msgid "open_values must be a boolean for {0}" msgstr "open_values doit être un booléen pour {0}" -#: tiramisu/option.py:400 +#: tiramisu/option.py:420 msgid "value {0} is not permitted, only {1} is allowed" msgstr "valeur {0} n'est pas permit, seules {1} sont autorisées" -#: tiramisu/option.py:411 +#: tiramisu/option.py:432 msgid "value must be a boolean" msgstr "valeur doit être un booléen" -#: tiramisu/option.py:421 +#: tiramisu/option.py:442 msgid "value must be an integer" msgstr "valeur doit être un numbre" -#: tiramisu/option.py:431 +#: tiramisu/option.py:452 msgid "value must be a float" msgstr "valeur doit être un nombre flottant" -#: tiramisu/option.py:441 -msgid "value must be a string" -msgstr "valeur doit être une chaîne" +#: tiramisu/option.py:462 +msgid "value must be a string, not {0}" +msgstr "valeur doit être une chaîne, pas {0}" -#: tiramisu/option.py:452 +#: tiramisu/option.py:480 msgid "value must be an unicode" msgstr "valeur doit être une valeur unicode" -#: tiramisu/option.py:463 +#: tiramisu/option.py:490 msgid "malformed symlinkoption must be an option for symlink {0}" msgstr "symlinkoption mal formé doit être une option pour symlink {0}" -#: tiramisu/option.py:497 +#: tiramisu/option.py:526 msgid "IP mustn't not be in reserved class" msgstr "IP ne doit pas être d'une classe reservée" -#: tiramisu/option.py:499 +#: tiramisu/option.py:528 msgid "IP must be in private class" msgstr "IP doit être dans la classe privée" -#: tiramisu/option.py:535 +#: tiramisu/option.py:566 msgid "inconsistency in allowed range" msgstr "inconsistence dans la plage autorisée" -#: tiramisu/option.py:540 +#: tiramisu/option.py:571 msgid "max value is empty" msgstr "valeur maximum est vide" -#: tiramisu/option.py:576 +#: tiramisu/option.py:608 msgid "network mustn't not be in reserved class" msgstr "réseau ne doit pas être dans la classe reservée" -#: tiramisu/option.py:608 +#: tiramisu/option.py:640 msgid "invalid network {0} ({1}) with netmask {2} ({3}), this network is an IP" msgstr "réseau invalide {0} ({1}) avec masque {2} ({3}), ce réseau est une IP" -#: tiramisu/option.py:612 +#: tiramisu/option.py:645 msgid "invalid IP {0} ({1}) with netmask {2} ({3}), this IP is a network" msgstr "IP invalide {0} ({1}) avec masque {2} ({3}), cette IP est un réseau" -#: tiramisu/option.py:617 +#: tiramisu/option.py:650 msgid "invalid IP {0} ({1}) with netmask {2} ({3})" msgstr "IP invalide {0} ({1}) avec masque {2} ({3})" -#: tiramisu/option.py:619 +#: tiramisu/option.py:652 msgid "invalid network {0} ({1}) with netmask {2} ({3})" msgstr "réseau invalide {0} ({1}) avec masque {2} ({3})" -#: tiramisu/option.py:639 +#: tiramisu/option.py:672 msgid "unknown type_ {0} for hostname" msgstr "type_ inconnu {0} pour le nom d'hôte" -#: tiramisu/option.py:642 +#: tiramisu/option.py:675 msgid "allow_ip must be a boolean" msgstr "allow_ip doit être un booléen" -#: tiramisu/option.py:671 +#: tiramisu/option.py:704 msgid "invalid value for {0}, must have dot" msgstr "valeur invalide pour {0}, doit avoir un point" -#: tiramisu/option.py:674 +#: tiramisu/option.py:707 msgid "invalid domainname's length for {0} (max {1})" msgstr "longueur du nom de domaine invalide pour {0} (maximum {1})" -#: tiramisu/option.py:676 +#: tiramisu/option.py:710 msgid "invalid domainname's length for {0} (min 2)" msgstr "longueur du nom de domaine invalide pour {0} (minimum 2)" -#: tiramisu/option.py:680 +#: tiramisu/option.py:714 msgid "invalid domainname" msgstr "nom de domaine invalide" -#: tiramisu/option.py:696 +#: tiramisu/option.py:731 msgid "invalid name: {0} for optiondescription" msgstr "nom invalide : {0} pour l'optiondescription" -#: tiramisu/option.py:707 +#: tiramisu/option.py:743 msgid "duplicate option name: {0}" msgstr "nom de l'option dupliqué : {0}" -#: tiramisu/option.py:731 +#: tiramisu/option.py:769 msgid "unknown Option {0} in OptionDescription {1}" msgstr "Option {} inconnue pour l'OptionDescription{}" -#: tiramisu/option.py:795 +#: tiramisu/option.py:820 msgid "duplicate option: {0}" msgstr "option dupliquée : {0}" -#: tiramisu/option.py:805 +#: tiramisu/option.py:850 msgid "no option for path {0}" msgstr "pas d'option pour le chemin {0}" -#: tiramisu/option.py:811 +#: tiramisu/option.py:856 msgid "no option {0} found" msgstr "pas d'option {0} trouvée" -#: tiramisu/option.py:821 +#: tiramisu/option.py:866 msgid "cannot change group_type if already set (old {0}, new {1})" msgstr "ne peut changer group_type si déjà spécifié (ancien {0}, nouveau {1})" -#: tiramisu/option.py:833 +#: tiramisu/option.py:879 msgid "master group {0} shall not have a subgroup" msgstr "groupe maître {0} ne doit pas avoir de sous-groupe" -#: tiramisu/option.py:836 +#: tiramisu/option.py:882 msgid "master group {0} shall not have a symlinkoption" msgstr "groupe maître {0} ne doit pas avoir de symlinkoption" -#: tiramisu/option.py:839 +#: tiramisu/option.py:885 msgid "not allowed option {0} in group {1}: this option is not a multi" msgstr "" "option non autorisée {0} dans le groupe {1} : cette option n'est pas une " "multi" -#: tiramisu/option.py:849 +#: tiramisu/option.py:896 msgid "master group with wrong master name for {0}" msgstr "le groupe maître avec un nom de maître éroné pour {0}" -#: tiramisu/option.py:857 +#: tiramisu/option.py:905 msgid "no child has same nom has master group for: {0}" msgstr "pas d'enfant avec le nom du groupe maître pour {0} " -#: tiramisu/option.py:860 -msgid "not allowed group_type : {0}" -msgstr "group_type non autorisé : {0}" +#: tiramisu/option.py:908 +msgid "group_type: {0} not allowed" +msgstr "group_type : {0} non autorisé" -#: tiramisu/option.py:889 +#: tiramisu/option.py:946 msgid "malformed requirements type for option: {0}, must be a dict" msgstr "" "type requirements malformé pour l'option : {0}, doit être un dictionnaire" -#: tiramisu/option.py:905 +#: tiramisu/option.py:962 msgid "" "malformed requirements for option: {0} require must have option, expected " "and action keys" @@ -301,68 +294,87 @@ msgstr "" "requirements malformé pour l'option : {0} l'exigence doit avoir les clefs " "option, exptected et action" -#: tiramisu/option.py:910 +#: tiramisu/option.py:967 msgid "malformed requirements for option: {0} inverse must be boolean" msgstr "requirements malformé pour l'option : {0} inverse doit être un booléen" -#: tiramisu/option.py:914 +#: tiramisu/option.py:971 msgid "malformed requirements for option: {0} transitive must be boolean" msgstr "requirements malformé pour l'option : {0} transitive doit être booléen" -#: tiramisu/option.py:918 +#: tiramisu/option.py:975 msgid "malformed requirements for option: {0} same_action must be boolean" msgstr "" "requirements malformé pour l'option : {0} same_action doit être un booléen" -#: tiramisu/option.py:923 +#: tiramisu/option.py:979 msgid "malformed requirements must be an option in option {0}" msgstr "requirements malformé doit être une option dans l'option {0}" -#: tiramisu/option.py:926 +#: tiramisu/option.py:982 msgid "malformed requirements option {0} should not be a multi" msgstr "requirements malformé l'option {0} ne doit pas être une multi" -#: tiramisu/option.py:932 +#: tiramisu/option.py:988 msgid "" "malformed requirements second argument must be valid for option {0}: {1}" msgstr "" "requirements malformé deuxième argument doit être valide pour l'option {0} : " "{1}" -#: tiramisu/option.py:936 +#: tiramisu/option.py:993 msgid "inconsistency in action types for option: {0} action: {1}" msgstr "incohérence dans les types action pour l'option : {0} action {1}" -#: tiramisu/setting.py:45 -msgid "can't rebind group ({})" -msgstr "ne peut reconsolider un groupe ({0})" +#: tiramisu/setting.py:47 +msgid "storage_type is already set, cannot rebind it" +msgstr "storage_type est déjà défini, impossible de le redéfinir" -#: tiramisu/setting.py:50 -msgid "can't unbind group ({})" -msgstr "ne peut délier un groupe ({0})" +#: tiramisu/setting.py:67 +msgid "can't rebind {0}" +msgstr "ne peut redéfinir ({0})" -#: tiramisu/setting.py:210 +#: tiramisu/setting.py:72 +msgid "can't unbind {0}" +msgstr "ne peut supprimer ({0})" + +#: tiramisu/setting.py:185 +msgid "cannot append {0} property for option {1}: this property is calculated" +msgstr "" +"ne peut ajouter la propriété {0} dans l'option {1}: cette propriété est " +"calculée" + +#: tiramisu/setting.py:215 +msgid "option {0} not already exists in storage {1}" +msgstr "option {0} n'existe pas dans l'espace de stockage {1}" + +#: tiramisu/setting.py:282 msgid "opt and all_properties must not be set together in reset" msgstr "opt et all_properties ne doit pas être renseigné ensemble dans reset" -#: tiramisu/setting.py:294 +#: tiramisu/setting.py:297 +msgid "if opt is not None, path should not be None in _getproperties" +msgstr "" +"si opt n'est pas None, path devrait ne pas être à None dans _getproperties" + +#: tiramisu/setting.py:391 msgid "cannot change the value for option {0} this option is frozen" msgstr "" "ne peut modifié la valeur de l'option {0} cette option n'est pas modifiable" -#: tiramisu/setting.py:298 +#: tiramisu/setting.py:397 msgid "trying to access to an option named: {0} with properties {1}" msgstr "tentative d'accès à une option nommée : {0} avec les propriétés {1}" -#: tiramisu/setting.py:307 +#: tiramisu/setting.py:415 msgid "permissive must be a tuple" msgstr "permissive doit être un tuple" -#: tiramisu/setting.py:314 tiramisu/value.py:208 +#: tiramisu/setting.py:422 tiramisu/value.py:277 msgid "invalid generic owner {0}" msgstr "invalide owner générique {0}" -#: tiramisu/setting.py:367 +#: tiramisu/setting.py:503 msgid "" "malformed requirements imbrication detected for option: '{0}' with " "requirement on: '{1}'" @@ -370,48 +382,81 @@ msgstr "" "imbrication de requirements malformé detectée pour l'option : '{0}' avec " "requirement sur : '{1}'" -#: tiramisu/setting.py:377 +#: tiramisu/setting.py:515 msgid "option '{0}' has requirement's property error: {1} {2}" msgstr "l'option '{0}' a une erreur de propriété pour le requirement : {1} {2}" -#: tiramisu/setting.py:383 -msgid "required option not found: {0}" -msgstr "option requise non trouvée : {0}" +#: tiramisu/storage/dictionary/storage.py:37 +msgid "dictionary storage cannot delete session" +msgstr "" +"impossible de supprimer une session dans un espace de stockage dictionary" -#: tiramisu/value.py:206 +#: tiramisu/storage/dictionary/storage.py:46 +msgid "session already used" +msgstr "session déjà utilisée" + +#: tiramisu/storage/dictionary/storage.py:48 +msgid "a dictionary cannot be persistent" +msgstr "un espace de stockage dictionary ne peut être persistant" + +#: tiramisu/value.py:284 msgid "no value for {0} cannot change owner to {1}" msgstr "pas de valeur pour {0} ne peut changer d'utilisateur pour {1}" -#: tiramisu/value.py:270 +#: tiramisu/value.py:356 msgid "invalid len for the slave: {0} which has {1} as master" msgstr "longueur invalide pour une esclave : {0} qui a {1} comme maître" -#: tiramisu/value.py:286 +#: tiramisu/value.py:373 msgid "invalid len for the master: {0} which has {1} as slave with greater len" msgstr "" "longueur invalide pour un maître : {0} qui a {1} une esclave avec une plus " "grande longueur" -#: tiramisu/value.py:306 +#: tiramisu/value.py:394 msgid "cannot append a value on a multi option {0} which is a slave" msgstr "ne peut ajouter une valeur sur l'option multi {0} qui est une esclave" -#: tiramisu/value.py:334 +#: tiramisu/value.py:429 msgid "cannot sort multi option {0} if master or slave" msgstr "ne peut trier une option multi {0} pour une maître ou une esclave" -#: tiramisu/value.py:342 +#: tiramisu/value.py:433 +msgid "cmp is not permitted in python v3 or greater" +msgstr "cmp n'est pas permis en python v3 ou supérieure" + +#: tiramisu/value.py:442 msgid "cannot reverse multi option {0} if master or slave" msgstr "ne peut inverser une option multi {0} pour une maître ou une esclave" -#: tiramisu/value.py:350 +#: tiramisu/value.py:450 msgid "cannot insert multi option {0} if master or slave" msgstr "ne peut insérer une option multi {0} pour une maître ou une esclave" -#: tiramisu/value.py:358 +#: tiramisu/value.py:458 msgid "cannot extend multi option {0} if master or slave" msgstr "ne peut étendre une option multi {0} pour une maître ou une esclave" -#: tiramisu/value.py:381 +#: tiramisu/value.py:482 msgid "cannot pop a value on a multi option {0} which is a slave" msgstr "ne peut supprimer une valeur dans l'option multi {0} qui est esclave" + +#~ msgid "metaconfig's children must be a list" +#~ msgstr "enfants d'une metaconfig doit être une liste" + +#~ msgid "metaconfig's children must be config, not {0}" +#~ msgstr "enfants d'une metaconfig doit être une config, pas {0}" + +#~ msgid "all config in metaconfig must have same optiondescription" +#~ msgstr "" +#~ "toutes les configs d'une metaconfig doivent avoir la même " +#~ "optiondescription" + +#~ msgid "child has already a metaconfig's" +#~ msgstr "enfant a déjà une metaconfig" + +#~ msgid "not allowed group_type : {0}" +#~ msgstr "group_type non autorisé : {0}" + +#~ msgid "required option not found: {0}" +#~ msgstr "option requise non trouvée : {0}" diff --git a/translations/tiramisu.pot b/translations/tiramisu.pot index faed4f3..d5c4ff6 100644 --- a/translations/tiramisu.pot +++ b/translations/tiramisu.pot @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2013-07-18 15:20+CEST\n" +"POT-Creation-Date: 2013-08-31 09:52+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -15,375 +15,395 @@ msgstr "" "Generated-By: pygettext.py 1.5\n" -#: tiramisu/autolib.py:49 +#: tiramisu/autolib.py:58 msgid "no config specified but needed" msgstr "" -#: tiramisu/autolib.py:56 +#: tiramisu/autolib.py:65 msgid "unable to carry out a calculation, option {0} has properties: {1} for: {2}" msgstr "" -#: tiramisu/autolib.py:65 +#: tiramisu/autolib.py:74 msgid "unable to carry out a calculation, option value with multi types must have same length for: {0}" msgstr "" -#: tiramisu/config.py:45 +#: tiramisu/config.py:47 msgid "descr must be an optiondescription, not {0}" msgstr "" -#: tiramisu/config.py:118 +#: tiramisu/config.py:121 msgid "unknown group_type: {0}" msgstr "" -#: tiramisu/config.py:154 -msgid "no optiondescription for this config (may be metaconfig without meta)" +#: tiramisu/config.py:157 +msgid "no option description found for this config (may be metaconfig without meta)" msgstr "" -#: tiramisu/config.py:312 -msgid "unknown type_ type {0} for _find" +#: tiramisu/config.py:311 +msgid "unknown type_ type {0}for _find" msgstr "" -#: tiramisu/config.py:351 +#: tiramisu/config.py:350 msgid "no option found in config with these criteria" msgstr "" -#: tiramisu/config.py:394 +#: tiramisu/config.py:400 msgid "make_dict can't filtering with value without option" msgstr "" -#: tiramisu/config.py:414 +#: tiramisu/config.py:421 msgid "unexpected path {0}, should start with {1}" msgstr "" -#: tiramisu/config.py:527 -msgid "metaconfig's children must be a list" +#: tiramisu/config.py:481 +msgid "opt in getowner must be an option not {0}" msgstr "" -#: tiramisu/config.py:532 -msgid "metaconfig's children must be config, not {0}" -msgstr "" - -#: tiramisu/config.py:537 -msgid "all config in metaconfig must have same optiondescription" -msgstr "" - -#: tiramisu/config.py:540 -msgid "child has already a metaconfig's" -msgstr "" - -#: tiramisu/option.py:70 +#: tiramisu/option.py:71 msgid "{0} has no attribute impl_set_information" msgstr "" -#: tiramisu/option.py:84 -msgid "Information's item not found: {0}" +#: tiramisu/option.py:86 +msgid "information's item not found: {0}" msgstr "" -#: tiramisu/option.py:86 +#: tiramisu/option.py:89 msgid "{0} has no attribute impl_get_information" msgstr "" -#: tiramisu/option.py:124 +#: tiramisu/option.py:117 +msgid "'{0}' ({1}) object attribute '{2}' is read-only" +msgstr "" + +#: tiramisu/option.py:159 msgid "invalid name: {0} for option" msgstr "" -#: tiramisu/option.py:134 +#: tiramisu/option.py:169 msgid "validator must be a function" msgstr "" -#: tiramisu/option.py:141 +#: tiramisu/option.py:176 msgid "a default_multi is set whereas multi is False in option: {0}" msgstr "" -#: tiramisu/option.py:147 +#: tiramisu/option.py:182 msgid "invalid default_multi value {0} for option {1}: {2}" msgstr "" -#: tiramisu/option.py:150 +#: tiramisu/option.py:187 msgid "default value not allowed if option: {0} is calculated" msgstr "" -#: tiramisu/option.py:153 +#: tiramisu/option.py:190 msgid "params defined for a callback function but no callback defined yet for option {0}" msgstr "" -#: tiramisu/option.py:174 tiramisu/option.py:718 +#: tiramisu/option.py:212 tiramisu/option.py:753 msgid "invalid properties type {0} for {1}, must be a tuple" msgstr "" -#: tiramisu/option.py:273 +#: tiramisu/option.py:285 msgid "invalid value {0} for option {1} for object {2}" msgstr "" -#: tiramisu/option.py:278 tiramisu/value.py:368 +#: tiramisu/option.py:293 tiramisu/value.py:468 msgid "invalid value {0} for option {1}: {2}" msgstr "" -#: tiramisu/option.py:290 +#: tiramisu/option.py:305 msgid "invalid value {0} for option {1} which must be a list" msgstr "" -#: tiramisu/option.py:354 +#: tiramisu/option.py:374 msgid "invalid value {0} for option {1} must be different as {2} option" msgstr "" -#: tiramisu/option.py:376 +#: tiramisu/option.py:396 msgid "values must be a tuple for {0}" msgstr "" -#: tiramisu/option.py:379 +#: tiramisu/option.py:399 msgid "open_values must be a boolean for {0}" msgstr "" -#: tiramisu/option.py:400 +#: tiramisu/option.py:420 msgid "value {0} is not permitted, only {1} is allowed" msgstr "" -#: tiramisu/option.py:411 +#: tiramisu/option.py:432 msgid "value must be a boolean" msgstr "" -#: tiramisu/option.py:421 +#: tiramisu/option.py:442 msgid "value must be an integer" msgstr "" -#: tiramisu/option.py:431 +#: tiramisu/option.py:452 msgid "value must be a float" msgstr "" -#: tiramisu/option.py:441 -msgid "value must be a string" +#: tiramisu/option.py:462 +msgid "value must be a string, not {0}" msgstr "" -#: tiramisu/option.py:452 +#: tiramisu/option.py:480 msgid "value must be an unicode" msgstr "" -#: tiramisu/option.py:463 +#: tiramisu/option.py:490 msgid "malformed symlinkoption must be an option for symlink {0}" msgstr "" -#: tiramisu/option.py:497 +#: tiramisu/option.py:526 msgid "IP mustn't not be in reserved class" msgstr "" -#: tiramisu/option.py:499 +#: tiramisu/option.py:528 msgid "IP must be in private class" msgstr "" -#: tiramisu/option.py:535 +#: tiramisu/option.py:566 msgid "inconsistency in allowed range" msgstr "" -#: tiramisu/option.py:540 +#: tiramisu/option.py:571 msgid "max value is empty" msgstr "" -#: tiramisu/option.py:576 +#: tiramisu/option.py:608 msgid "network mustn't not be in reserved class" msgstr "" -#: tiramisu/option.py:608 +#: tiramisu/option.py:640 msgid "invalid network {0} ({1}) with netmask {2} ({3}), this network is an IP" msgstr "" -#: tiramisu/option.py:612 +#: tiramisu/option.py:645 msgid "invalid IP {0} ({1}) with netmask {2} ({3}), this IP is a network" msgstr "" -#: tiramisu/option.py:617 +#: tiramisu/option.py:650 msgid "invalid IP {0} ({1}) with netmask {2} ({3})" msgstr "" -#: tiramisu/option.py:619 +#: tiramisu/option.py:652 msgid "invalid network {0} ({1}) with netmask {2} ({3})" msgstr "" -#: tiramisu/option.py:639 +#: tiramisu/option.py:672 msgid "unknown type_ {0} for hostname" msgstr "" -#: tiramisu/option.py:642 +#: tiramisu/option.py:675 msgid "allow_ip must be a boolean" msgstr "" -#: tiramisu/option.py:671 +#: tiramisu/option.py:704 msgid "invalid value for {0}, must have dot" msgstr "" -#: tiramisu/option.py:674 +#: tiramisu/option.py:707 msgid "invalid domainname's length for {0} (max {1})" msgstr "" -#: tiramisu/option.py:676 +#: tiramisu/option.py:710 msgid "invalid domainname's length for {0} (min 2)" msgstr "" -#: tiramisu/option.py:680 +#: tiramisu/option.py:714 msgid "invalid domainname" msgstr "" -#: tiramisu/option.py:696 +#: tiramisu/option.py:731 msgid "invalid name: {0} for optiondescription" msgstr "" -#: tiramisu/option.py:707 +#: tiramisu/option.py:743 msgid "duplicate option name: {0}" msgstr "" -#: tiramisu/option.py:731 +#: tiramisu/option.py:769 msgid "unknown Option {0} in OptionDescription {1}" msgstr "" -#: tiramisu/option.py:795 +#: tiramisu/option.py:820 msgid "duplicate option: {0}" msgstr "" -#: tiramisu/option.py:805 +#: tiramisu/option.py:850 msgid "no option for path {0}" msgstr "" -#: tiramisu/option.py:811 +#: tiramisu/option.py:856 msgid "no option {0} found" msgstr "" -#: tiramisu/option.py:821 +#: tiramisu/option.py:866 msgid "cannot change group_type if already set (old {0}, new {1})" msgstr "" -#: tiramisu/option.py:833 +#: tiramisu/option.py:879 msgid "master group {0} shall not have a subgroup" msgstr "" -#: tiramisu/option.py:836 +#: tiramisu/option.py:882 msgid "master group {0} shall not have a symlinkoption" msgstr "" -#: tiramisu/option.py:839 +#: tiramisu/option.py:885 msgid "not allowed option {0} in group {1}: this option is not a multi" msgstr "" -#: tiramisu/option.py:849 +#: tiramisu/option.py:896 msgid "master group with wrong master name for {0}" msgstr "" -#: tiramisu/option.py:857 +#: tiramisu/option.py:905 msgid "no child has same nom has master group for: {0}" msgstr "" -#: tiramisu/option.py:860 -msgid "not allowed group_type : {0}" +#: tiramisu/option.py:908 +msgid "group_type: {0} not allowed" msgstr "" -#: tiramisu/option.py:889 +#: tiramisu/option.py:946 msgid "malformed requirements type for option: {0}, must be a dict" msgstr "" -#: tiramisu/option.py:905 +#: tiramisu/option.py:962 msgid "malformed requirements for option: {0} require must have option, expected and action keys" msgstr "" -#: tiramisu/option.py:910 +#: tiramisu/option.py:967 msgid "malformed requirements for option: {0} inverse must be boolean" msgstr "" -#: tiramisu/option.py:914 +#: tiramisu/option.py:971 msgid "malformed requirements for option: {0} transitive must be boolean" msgstr "" -#: tiramisu/option.py:918 +#: tiramisu/option.py:975 msgid "malformed requirements for option: {0} same_action must be boolean" msgstr "" -#: tiramisu/option.py:923 +#: tiramisu/option.py:979 msgid "malformed requirements must be an option in option {0}" msgstr "" -#: tiramisu/option.py:926 +#: tiramisu/option.py:982 msgid "malformed requirements option {0} should not be a multi" msgstr "" -#: tiramisu/option.py:932 +#: tiramisu/option.py:988 msgid "malformed requirements second argument must be valid for option {0}: {1}" msgstr "" -#: tiramisu/option.py:936 +#: tiramisu/option.py:993 msgid "inconsistency in action types for option: {0} action: {1}" msgstr "" -#: tiramisu/setting.py:45 -msgid "can't rebind group ({})" +#: tiramisu/setting.py:47 +msgid "storage_type is already set, cannot rebind it" msgstr "" -#: tiramisu/setting.py:50 -msgid "can't unbind group ({})" +#: tiramisu/setting.py:67 +msgid "can't rebind {0}" msgstr "" -#: tiramisu/setting.py:210 +#: tiramisu/setting.py:72 +msgid "can't unbind {0}" +msgstr "" + +#: tiramisu/setting.py:185 +msgid "cannot append {0} property for option {1}: this property is calculated" +msgstr "" + +#: tiramisu/setting.py:215 +msgid "option {0} not already exists in storage {1}" +msgstr "" + +#: tiramisu/setting.py:282 msgid "opt and all_properties must not be set together in reset" msgstr "" -#: tiramisu/setting.py:294 +#: tiramisu/setting.py:297 +msgid "if opt is not None, path should not be None in _getproperties" +msgstr "" + +#: tiramisu/setting.py:391 msgid "cannot change the value for option {0} this option is frozen" msgstr "" -#: tiramisu/setting.py:298 +#: tiramisu/setting.py:397 msgid "trying to access to an option named: {0} with properties {1}" msgstr "" -#: tiramisu/setting.py:307 +#: tiramisu/setting.py:415 msgid "permissive must be a tuple" msgstr "" -#: tiramisu/setting.py:314 tiramisu/value.py:208 +#: tiramisu/setting.py:422 tiramisu/value.py:277 msgid "invalid generic owner {0}" msgstr "" -#: tiramisu/setting.py:367 +#: tiramisu/setting.py:503 msgid "malformed requirements imbrication detected for option: '{0}' with requirement on: '{1}'" msgstr "" -#: tiramisu/setting.py:377 +#: tiramisu/setting.py:515 msgid "option '{0}' has requirement's property error: {1} {2}" msgstr "" -#: tiramisu/setting.py:383 -msgid "required option not found: {0}" +#: tiramisu/storage/dictionary/storage.py:37 +msgid "dictionary storage cannot delete session" msgstr "" -#: tiramisu/value.py:206 +#: tiramisu/storage/dictionary/storage.py:46 +msgid "session already used" +msgstr "" + +#: tiramisu/storage/dictionary/storage.py:48 +msgid "a dictionary cannot be persistent" +msgstr "" + +#: tiramisu/value.py:284 msgid "no value for {0} cannot change owner to {1}" msgstr "" -#: tiramisu/value.py:270 +#: tiramisu/value.py:356 msgid "invalid len for the slave: {0} which has {1} as master" msgstr "" -#: tiramisu/value.py:286 +#: tiramisu/value.py:373 msgid "invalid len for the master: {0} which has {1} as slave with greater len" msgstr "" -#: tiramisu/value.py:306 +#: tiramisu/value.py:394 msgid "cannot append a value on a multi option {0} which is a slave" msgstr "" -#: tiramisu/value.py:334 +#: tiramisu/value.py:429 msgid "cannot sort multi option {0} if master or slave" msgstr "" -#: tiramisu/value.py:342 +#: tiramisu/value.py:433 +msgid "cmp is not permitted in python v3 or greater" +msgstr "" + +#: tiramisu/value.py:442 msgid "cannot reverse multi option {0} if master or slave" msgstr "" -#: tiramisu/value.py:350 +#: tiramisu/value.py:450 msgid "cannot insert multi option {0} if master or slave" msgstr "" -#: tiramisu/value.py:358 +#: tiramisu/value.py:458 msgid "cannot extend multi option {0} if master or slave" msgstr "" -#: tiramisu/value.py:381 +#: tiramisu/value.py:482 msgid "cannot pop a value on a multi option {0} which is a slave" msgstr "" From d3ee2acaab39f3f533e8071d8ca6b0072195f3dd Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 1 Sep 2013 22:20:11 +0200 Subject: [PATCH 04/50] can export options --- tiramisu/option.py | 84 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index d5c11a1..7f457a3 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -91,7 +91,7 @@ class BaseInformation(object): self.__class__.__name__)) -class _ReadOnlyOption(BaseInformation): +class BaseOption(BaseInformation): __slots__ = ('_readonly',) def __setattr__(self, name, value): @@ -120,8 +120,57 @@ class _ReadOnlyOption(BaseInformation): name)) object.__setattr__(self, name, value) + def _impl_convert_consistencies(self, value, cache): + # cache is a dico in import/not a dico in export + new_value = [] + for consistency in value: + if isinstance(cache, dict): + new_value = (consistency[0], cache[consistency[1]]) + else: + new_value = (consistency[0], cache.impl_get_path_by_opt( + consistency[1])) + return tuple(new_value) -class Option(_ReadOnlyOption): + def _impl_convert_requires(self, value, cache): + # cache is a dico in import/not a dico in export + new_value = [] + for requires in value: + new_requires = [] + for require in requires: + if isinstance(cache, dict): + new_require = [cache[require[0]]] + else: + new_require = [cache.impl_get_path_by_opt(require[0])] + new_require.extend(require[1:]) + new_requires.append(tuple(new_require)) + new_value.append(tuple(new_requires)) + return tuple(new_value) + + def impl_export(self, descr): + descr.impl_build_cache() + # add _opt_type (not in __slots__) + slots = set(['_opt_type']) + for subclass in self.__class__.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + slots -= frozenset(['_children', '_readonly', '_cache_paths', + '__weakref__']) + exported_object = {} + for attr in slots: + try: + value = getattr(self, attr) + if value is not None: + if attr == '_consistencies': + value = self._impl_convert_consistencies(value, descr) + elif attr == '_requires': + value = self._impl_convert_requires(value, descr) + exported_object[attr] = value + except AttributeError: + pass + return exported_object + + +class Option(BaseOption): """ Abstract base class for configuration option's. @@ -480,7 +529,7 @@ else: raise ValueError(_('value must be an unicode')) -class SymLinkOption(_ReadOnlyOption): +class SymLinkOption(BaseOption): __slots__ = ('_name', '_opt') _opt_type = 'symlink' @@ -499,6 +548,12 @@ class SymLinkOption(_ReadOnlyOption): else: return getattr(self._opt, name) + def impl_export(self, descr): + export = super(SymLinkOption, self).impl_export(descr) + export['_opt'] = descr.impl_get_path_by_opt(self._opt) + del(export['_impl_informations']) + return export + class IPOption(Option): "represents the choice of an ip" @@ -714,13 +769,14 @@ class DomainnameOption(Option): raise ValueError(_('invalid domainname')) -class OptionDescription(_ReadOnlyOption): +class OptionDescription(BaseOption): """Config's schema (organisation, group) and container of Options The `OptionsDescription` objects lives in the `tiramisu.config.Config`. """ __slots__ = ('_name', '_requires', '_cache_paths', '_group_type', '_properties', '_children', '_consistencies', '_calc_properties', '__weakref__', '_readonly', '_impl_informations') + _opt_type = 'optiondescription' def __init__(self, name, doc, children, requires=None, properties=None): """ @@ -814,8 +870,6 @@ class OptionDescription(_ReadOnlyOption): cache_option = [] for option in self.impl_getchildren(): attr = option._name - if attr.startswith('_cfgimpl'): - continue if option in cache_option: raise ConflictError(_('duplicate option: {0}').format(option)) @@ -926,6 +980,24 @@ class OptionDescription(_ReadOnlyOption): return False return True + def _impl_convert_group_type(self, value, cache): + if isinstance(cache, dict): + value = str(value) + else: + value = getattr(groups, value) + return value + + def impl_export(self, descr=None): + if descr is None: + descr = self + export = super(OptionDescription, self).impl_export(descr) + export['_group_type'] = self._impl_convert_group_type( + export['_group_type'], descr) + export['options'] = [] + for option in self.impl_getchildren(): + export['options'].append(option.impl_export(descr)) + return export + def validate_requires_arg(requires, name): """check malformed requirements From f379a0fc0ad7db9bd13c81616cf93ea154c5d2aa Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 1 Sep 2013 23:09:50 +0200 Subject: [PATCH 05/50] update Makefile and setup.py --- Makefile | 27 +++++++++-------------- setup.py | 65 +++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index 203a6b7..6edf2ee 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,7 @@ ifneq ($(DESTDIR),) PYTHON_OPTS += --root $(DESTDIR) endif -LAST_TAG := $(shell git describe --tags --abbrev=0) -VERSION := $(shell echo $(LAST_TAG) | awk -F'/' '{print $$2}' || true) +VERSION := 1.0rc VERSION_FILE := version.in # Build translation files @@ -39,11 +38,12 @@ define install_translation fi endef -all: +all: build-lang clean: $(RM) -r build - $(RM) -r tiramisu.egg-info/ + $(RM) -r $(PACKAGE).egg-info/ + $(RM) -r $(VERSION_FILE) $(RM) -r $(TRADUC_DIR)/*/*.mo #test: clean @@ -51,12 +51,12 @@ clean: # Build or update Portable Object Base Translation for gettext build-pot: - pygettext.py -p translations/ -o tiramisu.pot `find tiramisu/ -name "*.py"` + pygettext.py -p translations/ -o $(PACKAGE).pot `find $(PACKAGE)/ -name "*.py"` build-lang: $(call build_translation, $(TRADUC_DIR)) -install-lang: build-lang +install-lang: $(INSTALL_DIR) $(TRADUC_DEST) $(call install_translation, $(TRADUC_DIR)) @@ -65,16 +65,9 @@ install: version.in install-lang # List in .PHONY to force generation at each call version.in: - @if test -n $(VERSION) ; then \ - echo -n $(VERSION) > $(VERSION_FILE) ; \ - fi - @if test ! -s $(VERSION_FILE); then \ - echo -n '0.0-dev' > $(VERSION_FILE); \ - fi + echo -n $(VERSION) > $(VERSION_FILE) -dist: version.in - git archive --format=tar --prefix $(PACKAGE)-$(VERSION)/ -o $(PACKAGE)-$(VERSION).tar $(LAST_TAG) \ - && tar --xform "s,\(.*\),$(PACKAGE)-$(VERSION)/\1," -f $(PACKAGE)-$(VERSION).tar -r version.in \ - && gzip -9 $(PACKAGE)-$(VERSION).tar +dist: + git archive --format=tar --prefix $(PACKAGE)-$(VERSION)/ HEAD | gzip -9 > $(PACKAGE)-$(VERSION).tar.gz -.PHONY: all clean test install version.in dist build-pot +.PHONY: all clean build-pot build-lang install-lang install version.in dist diff --git a/setup.py b/setup.py index 1ca3915..b7d3ad3 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,55 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from distutils.core import setup +from os.path import isfile + + +version_file = 'version.in' -import os -import subprocess def fetch_version(): - """Get version from version.in or latest git tag""" - version_file='version.in' - version = "1.0" - git_last_tag_cmd = ['git', 'describe', '--tags', '--abbrev=0'] + """Get version from version.in""" - try: - if os.path.isfile(version_file): - version=file(version_file).readline().strip() - elif os.path.isdir('.git'): - popen = subprocess.Popen(git_last_tag_cmd, stdout=subprocess.PIPE) - out, ret = popen.communicate() - for line in out.split('\n'): - if line: - version = line.lstrip('release/') - break - except OSError: - pass # Failing is fine, we just can't print the version then - - return version + if not isfile(version_file): + raise Exception('Please use "make && make" install instead of ' + 'setup.py directly') + return file(version_file).readline().strip() setup( - author='cadoles team', + author="Tiramisu's team", author_email='contact@cadoles.com', name='tiramisu', version=fetch_version(), - description='configuration management tool', - url='http://labs.libre-entreprise.org/projects/tiramisu', - packages=['tiramisu'] + description='an options controller tool', + url='http://tiramisu.labs.libre-entreprise.org/', + packages=['tiramisu'], + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Developers", +# "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Linguistic" + ], + long_description="""\ +An options controller tool +------------------------------------- + +Due to more and more available options required to set up an operating system, +compiler options or whatever, it became quite annoying to hand the necessary +options to where they are actually used and even more annoying to add new +options. To circumvent these problems the configuration control was +introduced... + +Tiramisu is an options handler and an options controller, wich aims at +producing flexible and fast options access. + + +This version requires Python 2.6 or later. +""" ) From 2c1da6d72ed4b04a6db943d4f2cb436597935530 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 15:01:49 +0200 Subject: [PATCH 06/50] an OptionDescription can be serialized --- tiramisu/option.py | 187 ++++++++++++++++++++++++--------------------- 1 file changed, 102 insertions(+), 85 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index 7f457a3..4327938 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -92,82 +92,100 @@ class BaseInformation(object): class BaseOption(BaseInformation): - __slots__ = ('_readonly',) + __slots__ = ('_readonly', '_state_consistencies', '_state_requires') def __setattr__(self, name, value): - is_readonly = False - # never change _name - if name == '_name': + if not name.startswith('_state'): + is_readonly = False + # never change _name + if name == '_name': + try: + self._name + #so _name is already set + is_readonly = True + except: + pass try: - self._name - #so _name is already set - is_readonly = True - except: + if self._readonly is True: + if value is True: + # already readonly and try to re set readonly + # don't raise, just exit + return + is_readonly = True + except AttributeError: pass - try: - if self._readonly is True: - if value is True: - # already readonly and try to re set readonly - # don't raise, just exit - return - is_readonly = True - except AttributeError: - pass - if is_readonly: - raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is" - " read-only").format( - self.__class__.__name__, self._name, - name)) + if is_readonly: + raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is" + " read-only").format( + self.__class__.__name__, + self._name, + name)) object.__setattr__(self, name, value) - def _impl_convert_consistencies(self, value, cache): + def _impl_convert_consistencies(self, cache): # cache is a dico in import/not a dico in export - new_value = [] - for consistency in value: - if isinstance(cache, dict): - new_value = (consistency[0], cache[consistency[1]]) - else: - new_value = (consistency[0], cache.impl_get_path_by_opt( - consistency[1])) - return tuple(new_value) - - def _impl_convert_requires(self, value, cache): - # cache is a dico in import/not a dico in export - new_value = [] - for requires in value: - new_requires = [] - for require in requires: + if self._consistencies is None: + self._state_consistencies = None + else: + new_value = [] + for consistency in self._consistencies: if isinstance(cache, dict): - new_require = [cache[require[0]]] + new_value.append((consistency[0], cache[consistency[1]])) else: - new_require = [cache.impl_get_path_by_opt(require[0])] - new_require.extend(require[1:]) - new_requires.append(tuple(new_require)) - new_value.append(tuple(new_requires)) - return tuple(new_value) + new_value.append((consistency[0], + cache.impl_get_path_by_opt( + consistency[1]))) + if isinstance(cache, dict): + pass + else: + self._state_consistencies = tuple(new_value) - def impl_export(self, descr): - descr.impl_build_cache() - # add _opt_type (not in __slots__) - slots = set(['_opt_type']) + def _impl_convert_requires(self, cache): + # cache is a dico in import/not a dico in export + if self._requires is None: + self._state_requires = None + else: + new_value = [] + for requires in self._requires: + new_requires = [] + for require in requires: + if isinstance(cache, dict): + new_require = [cache[require[0]]] + else: + new_require = [cache.impl_get_path_by_opt(require[0])] + new_require.extend(require[1:]) + new_requires.append(tuple(new_require)) + new_value.append(tuple(new_requires)) + if isinstance(cache, dict): + pass + else: + self._state_requires = new_value + + def _impl_getstate(self, descr): + self._impl_convert_consistencies(descr) + self._impl_convert_requires(descr) + + def __getstate__(self, export=False): + slots = set() for subclass in self.__class__.__mro__: if subclass is not object: slots.update(subclass.__slots__) - slots -= frozenset(['_children', '_readonly', '_cache_paths', - '__weakref__']) - exported_object = {} - for attr in slots: - try: - value = getattr(self, attr) - if value is not None: - if attr == '_consistencies': - value = self._impl_convert_consistencies(value, descr) - elif attr == '_requires': - value = self._impl_convert_requires(value, descr) - exported_object[attr] = value - except AttributeError: - pass - return exported_object + slots -= frozenset(['_children', '_cache_paths', '__weakref__']) + states = {} + for slot in slots: + # remove variable if save variable converted in _state_xxxx variable + if '_state' + slot not in slots: + if slot.startswith('_state'): + # should exists + states[slot] = getattr(self, slot) + # remove _state_xxx variable + self.__delattr__(slot) + else: + try: + states[slot] = getattr(self, slot) + except AttributeError: + pass + return states class Option(BaseOption): @@ -530,7 +548,7 @@ else: class SymLinkOption(BaseOption): - __slots__ = ('_name', '_opt') + __slots__ = ('_name', '_opt', '_state_opt') _opt_type = 'symlink' def __init__(self, name, opt): @@ -548,11 +566,9 @@ class SymLinkOption(BaseOption): else: return getattr(self._opt, name) - def impl_export(self, descr): - export = super(SymLinkOption, self).impl_export(descr) - export['_opt'] = descr.impl_get_path_by_opt(self._opt) - del(export['_impl_informations']) - return export + def _impl_getstate(self, descr): + super(SymLinkOption, self)._impl_getstate(descr) + self._state_opt = descr.impl_get_path_by_opt(self._opt) class IPOption(Option): @@ -774,8 +790,9 @@ class OptionDescription(BaseOption): The `OptionsDescription` objects lives in the `tiramisu.config.Config`. """ __slots__ = ('_name', '_requires', '_cache_paths', '_group_type', - '_properties', '_children', '_consistencies', - '_calc_properties', '__weakref__', '_readonly', '_impl_informations') + '_state_group_type', '_properties', '_children', + '_consistencies', '_calc_properties', '__weakref__', + '_readonly', '_impl_informations') _opt_type = 'optiondescription' def __init__(self, name, doc, children, requires=None, properties=None): @@ -980,23 +997,23 @@ class OptionDescription(BaseOption): return False return True - def _impl_convert_group_type(self, value, cache): - if isinstance(cache, dict): - value = str(value) - else: - value = getattr(groups, value) - return value - - def impl_export(self, descr=None): + def _impl_getstate(self, descr=None): if descr is None: + self.impl_build_cache() descr = self - export = super(OptionDescription, self).impl_export(descr) - export['_group_type'] = self._impl_convert_group_type( - export['_group_type'], descr) - export['options'] = [] + super(OptionDescription, self)._impl_getstate(descr) + self._state_group_type = str(self._group_type) for option in self.impl_getchildren(): - export['options'].append(option.impl_export(descr)) - return export + option._impl_getstate(descr) + + def __getstate__(self, export=False): + if not export: + self._impl_getstate() + states = super(OptionDescription, self).__getstate__(True) + states['_state_children'] = [] + for option in self.impl_getchildren(): + states['_state_children'].append(option.__getstate__(True)) + return states def validate_requires_arg(requires, name): From 7dd9394b846c6e72144c964749020cf1f78fddf1 Mon Sep 17 00:00:00 2001 From: gwen Date: Mon, 2 Sep 2013 15:06:55 +0200 Subject: [PATCH 07/50] makefile and docstrings --- Makefile | 27 +++++---- setup.py | 11 +--- tiramisu/option.py | 17 +++++- translations/tiramisu.pot | 116 +++++++++++++++++++------------------- 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/Makefile b/Makefile index 6edf2ee..ca436a5 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,16 @@ ifneq ($(DESTDIR),) PYTHON_OPTS += --root $(DESTDIR) endif -VERSION := 1.0rc -VERSION_FILE := version.in +VERSION := `cat VERSION` + +define gettext + if command -v pygettext >/dev/null 2>&1 ; then \ + P="pygettext" ; \ + else \ + P="pygettext.py" ; \ + fi ; \ + $$P -p translations/ -o $(PACKAGE).pot `find $(PACKAGE)/ -name "*.py"` +endef # Build translation files define build_translation @@ -27,6 +35,8 @@ define build_translation fi endef + + # Install Traduction define install_translation if [ -d ${1} ]; then \ @@ -43,15 +53,15 @@ all: build-lang clean: $(RM) -r build $(RM) -r $(PACKAGE).egg-info/ - $(RM) -r $(VERSION_FILE) $(RM) -r $(TRADUC_DIR)/*/*.mo #test: clean # py.test # Build or update Portable Object Base Translation for gettext + build-pot: - pygettext.py -p translations/ -o $(PACKAGE).pot `find $(PACKAGE)/ -name "*.py"` + $(call gettext) build-lang: $(call build_translation, $(TRADUC_DIR)) @@ -60,14 +70,11 @@ install-lang: $(INSTALL_DIR) $(TRADUC_DEST) $(call install_translation, $(TRADUC_DIR)) -install: version.in install-lang +install: install-lang python setup.py install --no-compile $(PYTHON_OPTS) -# List in .PHONY to force generation at each call -version.in: - echo -n $(VERSION) > $(VERSION_FILE) - dist: git archive --format=tar --prefix $(PACKAGE)-$(VERSION)/ HEAD | gzip -9 > $(PACKAGE)-$(VERSION).tar.gz -.PHONY: all clean build-pot build-lang install-lang install version.in dist +# List in .PHONY to force generation at each call +.PHONY: all clean build-pot build-lang install-lang install dist diff --git a/setup.py b/setup.py index b7d3ad3..f320843 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from distutils.core import setup -from os.path import isfile - - -version_file = 'version.in' def fetch_version(): """Get version from version.in""" - - if not isfile(version_file): - raise Exception('Please use "make && make" install instead of ' - 'setup.py directly') - return file(version_file).readline().strip() + return file('VERSION', 'r').readline().strip() setup( @@ -26,6 +18,7 @@ setup( packages=['tiramisu'], classifiers=[ "Programming Language :: Python", + "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", "Environment :: Other Environment", diff --git a/tiramisu/option.py b/tiramisu/option.py index 7f457a3..9648877 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -92,9 +92,21 @@ class BaseInformation(object): class BaseOption(BaseInformation): + """This abstract base class stands for attribute access + in options that have to be set only once, it is of course done in the + __setattr__ method + """ __slots__ = ('_readonly',) def __setattr__(self, name, value): + """set once and only once some attributes in the option, + like `_name`. `_name` cannot be changed one the option and + pushed in the :class:`tiramisu.option.OptionDescription`. + + if the attribute `_readonly` is set to `True`, the option is + "frozen" (which has noting to do with the high level "freeze" + propertie or "read_only" property) + """ is_readonly = False # never change _name if name == '_name': @@ -102,7 +114,7 @@ class BaseOption(BaseInformation): self._name #so _name is already set is_readonly = True - except: + except AttributeError: pass try: if self._readonly is True: @@ -988,6 +1000,9 @@ class OptionDescription(BaseOption): return value def impl_export(self, descr=None): + """enables us to export into a dict + :param descr: parent :class:`tiramisu.option.OptionDescription` + """ if descr is None: descr = self export = super(OptionDescription, self).impl_export(descr) diff --git a/translations/tiramisu.pot b/translations/tiramisu.pot index d5c4ff6..3b7b989 100644 --- a/translations/tiramisu.pot +++ b/translations/tiramisu.pot @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2013-08-31 09:52+CEST\n" +"POT-Creation-Date: 2013-09-02 11:30+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -75,231 +75,231 @@ msgstr "" msgid "'{0}' ({1}) object attribute '{2}' is read-only" msgstr "" -#: tiramisu/option.py:159 +#: tiramisu/option.py:208 msgid "invalid name: {0} for option" msgstr "" -#: tiramisu/option.py:169 +#: tiramisu/option.py:218 msgid "validator must be a function" msgstr "" -#: tiramisu/option.py:176 +#: tiramisu/option.py:225 msgid "a default_multi is set whereas multi is False in option: {0}" msgstr "" -#: tiramisu/option.py:182 +#: tiramisu/option.py:231 msgid "invalid default_multi value {0} for option {1}: {2}" msgstr "" -#: tiramisu/option.py:187 +#: tiramisu/option.py:236 msgid "default value not allowed if option: {0} is calculated" msgstr "" -#: tiramisu/option.py:190 +#: tiramisu/option.py:239 msgid "params defined for a callback function but no callback defined yet for option {0}" msgstr "" -#: tiramisu/option.py:212 tiramisu/option.py:753 +#: tiramisu/option.py:261 tiramisu/option.py:809 msgid "invalid properties type {0} for {1}, must be a tuple" msgstr "" -#: tiramisu/option.py:285 +#: tiramisu/option.py:334 msgid "invalid value {0} for option {1} for object {2}" msgstr "" -#: tiramisu/option.py:293 tiramisu/value.py:468 +#: tiramisu/option.py:342 tiramisu/value.py:468 msgid "invalid value {0} for option {1}: {2}" msgstr "" -#: tiramisu/option.py:305 +#: tiramisu/option.py:354 msgid "invalid value {0} for option {1} which must be a list" msgstr "" -#: tiramisu/option.py:374 +#: tiramisu/option.py:423 msgid "invalid value {0} for option {1} must be different as {2} option" msgstr "" -#: tiramisu/option.py:396 +#: tiramisu/option.py:445 msgid "values must be a tuple for {0}" msgstr "" -#: tiramisu/option.py:399 +#: tiramisu/option.py:448 msgid "open_values must be a boolean for {0}" msgstr "" -#: tiramisu/option.py:420 +#: tiramisu/option.py:469 msgid "value {0} is not permitted, only {1} is allowed" msgstr "" -#: tiramisu/option.py:432 +#: tiramisu/option.py:481 msgid "value must be a boolean" msgstr "" -#: tiramisu/option.py:442 +#: tiramisu/option.py:491 msgid "value must be an integer" msgstr "" -#: tiramisu/option.py:452 +#: tiramisu/option.py:501 msgid "value must be a float" msgstr "" -#: tiramisu/option.py:462 +#: tiramisu/option.py:511 msgid "value must be a string, not {0}" msgstr "" -#: tiramisu/option.py:480 +#: tiramisu/option.py:529 msgid "value must be an unicode" msgstr "" -#: tiramisu/option.py:490 +#: tiramisu/option.py:539 msgid "malformed symlinkoption must be an option for symlink {0}" msgstr "" -#: tiramisu/option.py:526 +#: tiramisu/option.py:581 msgid "IP mustn't not be in reserved class" msgstr "" -#: tiramisu/option.py:528 +#: tiramisu/option.py:583 msgid "IP must be in private class" msgstr "" -#: tiramisu/option.py:566 +#: tiramisu/option.py:621 msgid "inconsistency in allowed range" msgstr "" -#: tiramisu/option.py:571 +#: tiramisu/option.py:626 msgid "max value is empty" msgstr "" -#: tiramisu/option.py:608 +#: tiramisu/option.py:663 msgid "network mustn't not be in reserved class" msgstr "" -#: tiramisu/option.py:640 +#: tiramisu/option.py:695 msgid "invalid network {0} ({1}) with netmask {2} ({3}), this network is an IP" msgstr "" -#: tiramisu/option.py:645 +#: tiramisu/option.py:700 msgid "invalid IP {0} ({1}) with netmask {2} ({3}), this IP is a network" msgstr "" -#: tiramisu/option.py:650 +#: tiramisu/option.py:705 msgid "invalid IP {0} ({1}) with netmask {2} ({3})" msgstr "" -#: tiramisu/option.py:652 +#: tiramisu/option.py:707 msgid "invalid network {0} ({1}) with netmask {2} ({3})" msgstr "" -#: tiramisu/option.py:672 +#: tiramisu/option.py:727 msgid "unknown type_ {0} for hostname" msgstr "" -#: tiramisu/option.py:675 +#: tiramisu/option.py:730 msgid "allow_ip must be a boolean" msgstr "" -#: tiramisu/option.py:704 +#: tiramisu/option.py:759 msgid "invalid value for {0}, must have dot" msgstr "" -#: tiramisu/option.py:707 +#: tiramisu/option.py:762 msgid "invalid domainname's length for {0} (max {1})" msgstr "" -#: tiramisu/option.py:710 +#: tiramisu/option.py:765 msgid "invalid domainname's length for {0} (min 2)" msgstr "" -#: tiramisu/option.py:714 +#: tiramisu/option.py:769 msgid "invalid domainname" msgstr "" -#: tiramisu/option.py:731 +#: tiramisu/option.py:787 msgid "invalid name: {0} for optiondescription" msgstr "" -#: tiramisu/option.py:743 +#: tiramisu/option.py:799 msgid "duplicate option name: {0}" msgstr "" -#: tiramisu/option.py:769 +#: tiramisu/option.py:825 msgid "unknown Option {0} in OptionDescription {1}" msgstr "" -#: tiramisu/option.py:820 +#: tiramisu/option.py:874 msgid "duplicate option: {0}" msgstr "" -#: tiramisu/option.py:850 +#: tiramisu/option.py:904 msgid "no option for path {0}" msgstr "" -#: tiramisu/option.py:856 +#: tiramisu/option.py:910 msgid "no option {0} found" msgstr "" -#: tiramisu/option.py:866 +#: tiramisu/option.py:920 msgid "cannot change group_type if already set (old {0}, new {1})" msgstr "" -#: tiramisu/option.py:879 +#: tiramisu/option.py:933 msgid "master group {0} shall not have a subgroup" msgstr "" -#: tiramisu/option.py:882 +#: tiramisu/option.py:936 msgid "master group {0} shall not have a symlinkoption" msgstr "" -#: tiramisu/option.py:885 +#: tiramisu/option.py:939 msgid "not allowed option {0} in group {1}: this option is not a multi" msgstr "" -#: tiramisu/option.py:896 +#: tiramisu/option.py:950 msgid "master group with wrong master name for {0}" msgstr "" -#: tiramisu/option.py:905 +#: tiramisu/option.py:959 msgid "no child has same nom has master group for: {0}" msgstr "" -#: tiramisu/option.py:908 +#: tiramisu/option.py:962 msgid "group_type: {0} not allowed" msgstr "" -#: tiramisu/option.py:946 +#: tiramisu/option.py:1021 msgid "malformed requirements type for option: {0}, must be a dict" msgstr "" -#: tiramisu/option.py:962 +#: tiramisu/option.py:1037 msgid "malformed requirements for option: {0} require must have option, expected and action keys" msgstr "" -#: tiramisu/option.py:967 +#: tiramisu/option.py:1042 msgid "malformed requirements for option: {0} inverse must be boolean" msgstr "" -#: tiramisu/option.py:971 +#: tiramisu/option.py:1046 msgid "malformed requirements for option: {0} transitive must be boolean" msgstr "" -#: tiramisu/option.py:975 +#: tiramisu/option.py:1050 msgid "malformed requirements for option: {0} same_action must be boolean" msgstr "" -#: tiramisu/option.py:979 +#: tiramisu/option.py:1054 msgid "malformed requirements must be an option in option {0}" msgstr "" -#: tiramisu/option.py:982 +#: tiramisu/option.py:1057 msgid "malformed requirements option {0} should not be a multi" msgstr "" -#: tiramisu/option.py:988 +#: tiramisu/option.py:1063 msgid "malformed requirements second argument must be valid for option {0}: {1}" msgstr "" -#: tiramisu/option.py:993 +#: tiramisu/option.py:1068 msgid "inconsistency in action types for option: {0} action: {1}" msgstr "" From 52a6705fbf99ff34d0917c2c92ca2e368f235cbb Mon Sep 17 00:00:00 2001 From: gwen Date: Mon, 2 Sep 2013 16:27:22 +0200 Subject: [PATCH 08/50] new logo for the tiramisu project --- AUTHORS | 2 ++ VERSION | 1 + doc/config.txt | 43 ++++++++-------------------------------- doc/index.txt | 2 +- doc/logo.png | Bin 0 -> 151818 bytes doc/tiramisu.jpeg | Bin 29275 -> 0 bytes test/test_config_api.py | 1 - 7 files changed, 12 insertions(+), 37 deletions(-) create mode 100644 VERSION create mode 100644 doc/logo.png delete mode 100644 doc/tiramisu.jpeg diff --git a/AUTHORS b/AUTHORS index f6e3bb8..1819d02 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,3 +6,5 @@ Emmanuel Garette developer Daniel Dehennin contributor Philippe Caseiro contributor + +Gerald Schwartzmann tiramisu's logo (made with The Gimp) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..0f82de4 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0rc1 diff --git a/doc/config.txt b/doc/config.txt index c833172..ed1a3ca 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -67,17 +67,6 @@ But there are special exceptions. We will see later on that an option can be a :term:`mandatory option`. A mandatory option is an option that must have a value defined. -Appart from this case, if no value have been set yet, the value is `None`. When -the option is called to retrieve a value, an exception is raised. - -What if a value has been set and `None` is to be returned again ? Don't worry, -an option value can be reseted:: -:: - - >>> cfg.cfgimpl_get_values().reset(gcdummy) - >>> cfg.dummy - False - Setting the values of the options ---------------------------------------- @@ -110,7 +99,7 @@ adhere to the option description). Common manipulations ------------------------ -Let's perform some common manipulation on some options: +Let's perform some common manipulation on some options >>> from tiramisu.config import Config >>> from tiramisu.option import UnicodeOption, OptionDescription @@ -141,9 +130,7 @@ value let's modify a value (careful to the value's type...) >>> c.od1.var1 = 'value' -Traceback (most recent call last): -[...] -ValueError: invalid value value for option var1 +Traceback (most recent call last): ValueError: invalid value value for option var1 >>> c.od1.var1 = u'value' >>> print c.od1.var1 value @@ -161,16 +148,13 @@ The value is saved in a :class:`~tiramisu.value.Value` object. It is on this object that we have to trigger the `reset`, wich take the option itself (`var2`) as a parameter. -On the other side, in the `read_only` mode, it is not possible to modify the value:: +On the other side, in the `read_only` mode, it is not possible to modify the value + +>>> c.read_only() +>>> c.od1.var2 = u'value2' +Traceback (most recent call last): +tiramisu.error.PropertiesOptionError: cannot change the value to var2 for option ['frozen'] this option is frozen - >>> c.read_only() - >>> c.od1.var2 = u'value2' - Traceback (most recent call last): - [...] - tiramisu.error.PropertiesOptionError: - cannot change the value to var2 - for option ['frozen'] this option is frozen - let's retrieve the option `var1` description >>> var1.impl_get_information('doc') @@ -314,7 +298,6 @@ with a hidden option:: >>> print c.od1.var1 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var1 with properties ['hidden'] >>> c.read_only() @@ -331,7 +314,6 @@ mode. But in read only mode, an error is raised if no value has been defined:: >>> c.read_only() >>> print c.od1.var2 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var2 with properties ['mandatory'] >>> c.read_write() @@ -348,7 +330,6 @@ Let's try to modify a frozen option:: value >>> c.od1.var3 = u'value2' Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: cannot change the value for option var3 this option is frozen >>> c.read_only() >>> print c.od1.var3 @@ -360,7 +341,6 @@ read/write or read only mode:: >>> c.cfgimpl_get_settings().append('inconnu') >>> print c.od1.var3 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var3 with properties ['inconnu'] >>> c.cfgimpl_get_settings().remove('inconnu') @@ -373,7 +353,6 @@ Properties can also be defined on an option group, (that is, on an >>> c.read_write() >>> print c.od2.var4 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: od2 with properties ['hidden'] >>> c.read_only() @@ -387,7 +366,6 @@ Furthermore, let's retrieve the properties, delete and add the `hidden` property ['hidden'] >>> print c.od1.var1 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var1 with properties ['hidden'] >>> c.cfgimpl_get_settings()[rootod.od1.var1].remove('hidden') @@ -400,7 +378,6 @@ Furthermore, let's retrieve the properties, delete and add the `hidden` property ['hidden'] >>> print c.od1.var1 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var1 with properties ['hidden'] @@ -442,7 +419,6 @@ But it is not possible to set a value to a multi-option wich is not a list:: >>> c.od1.var1 = u'error' Traceback (most recent call last): - [...] ValueError: invalid value error for option var1 which must be a list @@ -514,17 +490,14 @@ But it is forbidden to change the lenght of a slave:: slave2 = [u'default', u'default'] >>> c.master.slave1 = [u'new1'] Traceback (most recent call last): - [...] tiramisu.error.SlaveError: invalid len for the slave: slave1 which has master.master as master >>> c.master.slave1 = [u'new1', u'new2', u'new3'] - [...] tiramisu.error.SlaveError: invalid len for the slave: slave1 which has master.master as master you have to call the `pop` function on the master:: >>> c.master.master = [u'oui'] Traceback (most recent call last): - [...] tiramisu.error.SlaveError: invalid len for the master: master which has slave1 as slave with greater len >>> c.master.master.pop(0) u'oui' diff --git a/doc/index.txt b/doc/index.txt index 41c2ce3..21cf692 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -10,7 +10,7 @@ The tasting of `Tiramisu` ========================= -.. image:: tiramisu.jpeg +.. image:: logo.png :height: 150px `Tiramisu` diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..08e33cd02becab34052bd0c4a058f4e2b9f7aa1f GIT binary patch literal 151818 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 Date: Mon, 2 Sep 2013 17:13:07 +0200 Subject: [PATCH 09/50] requirements: calculate all requirements for an option --- test/test_requires.py | 24 ++++++++++++++++++++++++ tiramisu/option.py | 9 ++++++--- tiramisu/setting.py | 10 +++++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/test/test_requires.py b/test/test_requires.py index 0f9af61..3d386d7 100644 --- a/test/test_requires.py +++ b/test/test_requires.py @@ -88,6 +88,30 @@ def test_multiple_requires(): c.ip_address_service +def test_multiple_requires_cumulative(): + a = StrOption('activate_service', '') + b = IPOption('ip_address_service', '', + requires=[{'option': a, 'expected': 'yes', 'action': 'disabled'}, + {'option': a, 'expected': 'yes', 'action': 'hidden'}]) + od = OptionDescription('service', '', [a, b]) + c = Config(od) + c.read_write() + c.ip_address_service + c.activate_service = 'yes' + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert set(props) == set(['hidden', 'disabled']) + + c.activate_service = 'ok' + c.ip_address_service + + c.activate_service = 'no' + c.ip_address_service + + def test_multiple_requires_inverse(): a = StrOption('activate_service', '') b = IPOption('ip_address_service', '', diff --git a/tiramisu/option.py b/tiramisu/option.py index c1c949f..ff50ea2 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -1045,6 +1045,8 @@ def validate_requires_arg(requires, name): ret_requires = {} config_action = {} + # start parsing all requires given by user (has dict) + # transforme it to a tuple for require in requires: if not type(require) == dict: raise ValueError(_("malformed requirements type for option:" @@ -1058,6 +1060,7 @@ def validate_requires_arg(requires, name): '{2}'.format(name, unknown_keys, valid_keys)) + # prepare all attributes try: option = require['option'] expected = require['expected'] @@ -1106,12 +1109,12 @@ 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(): - req = (require[0], tuple(require[1]), require[2], require[3], - require[4], require[5]) - ret_action.append(req) + 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) diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 879656c..46741b5 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -402,11 +402,11 @@ class Settings(object): def setpermissive(self, permissive, opt=None, path=None): """ enables us to put the permissives in the storage - - :param path: the option's path + + :param path: the option's path :param type: str - :param opt: if an option object is set, the path is extracted. - it is better (faster) to set the path parameter + :param opt: if an option object is set, the path is extracted. + it is better (faster) to set the path parameter instead of passing a :class:`tiramisu.option.Option()` object. """ if opt is not None and path is None: @@ -527,7 +527,7 @@ class Settings(object): calc_properties.add(action) # the calculation cannot be carried out break - return calc_properties + return calc_properties def _get_opt_path(self, opt): return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt) From f106f3ced7ece0fb56da4ec35e29d90dee47cd33 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 19:47:00 +0200 Subject: [PATCH 10/50] cannot set properties if those properties are in requirement --- test/test_requires.py | 40 ++++++++++++++++++++++++++++++++++++++++ test/test_slots.py | 2 +- tiramisu/option.py | 15 ++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/test/test_requires.py b/test/test_requires.py index 3d386d7..ba6cf3f 100644 --- a/test/test_requires.py +++ b/test/test_requires.py @@ -112,6 +112,40 @@ def test_multiple_requires_cumulative(): c.ip_address_service +def test_multiple_requires_cumulative_inverse(): + a = StrOption('activate_service', '') + b = IPOption('ip_address_service', '', + requires=[{'option': a, 'expected': 'yes', 'action': 'disabled', 'inverse': True}, + {'option': a, 'expected': 'yes', 'action': 'hidden', 'inverse': True}]) + od = OptionDescription('service', '', [a, b]) + c = Config(od) + c.read_write() + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert set(props) == set(['hidden', 'disabled']) + c.activate_service = 'yes' + c.ip_address_service + + c.activate_service = 'ok' + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert set(props) == set(['hidden', 'disabled']) + + c.activate_service = 'no' + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert set(props) == set(['hidden', 'disabled']) + + def test_multiple_requires_inverse(): a = StrOption('activate_service', '') b = IPOption('ip_address_service', '', @@ -500,3 +534,9 @@ def test_set_item(): c = Config(od) c.read_write() raises(ValueError, 'c.cfgimpl_get_settings()[a] = ("test",)') + + +def test_properties_conflict(): + a = BoolOption('activate_service', '', True) + raises(ValueError, "IPOption('ip_address_service', '', properties=('disabled',), requires=[{'option': a, 'expected': False, 'action': 'disabled'}])") + raises(ValueError, "od1 = OptionDescription('service', '', [a], properties=('disabled',), requires=[{'option': a, 'expected': False, 'action': 'disabled'}])") diff --git a/test/test_slots.py b/test/test_slots.py index f99484f..1f2aee6 100644 --- a/test/test_slots.py +++ b/test/test_slots.py @@ -107,7 +107,7 @@ def test_slots_option_readonly_name(): def test_slots_description(): - # __slots__ for OptionDescription must be complete + # __slots__ for OptionDescription should be complete for __getattr__ slots = set() for subclass in OptionDescription.__mro__: if subclass is not object: diff --git a/tiramisu/option.py b/tiramisu/option.py index ff50ea2..9a2dd5d 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -292,6 +292,12 @@ class Option(BaseOption): ' must be a tuple').format( type(properties), self._name)) + if self._calc_properties is not None and properties is not tuple(): + set_forbidden_properties = set(properties) & self._calc_properties + if set_forbidden_properties != frozenset(): + raise ValueError('conflict: properties already set in ' + 'requirement {0}'.format( + list(set_forbidden_properties))) self._properties = properties # 'hidden', 'disabled'... def _launch_consistency(self, func, opt, vals, context, index, opt_): @@ -804,7 +810,8 @@ class OptionDescription(BaseOption): __slots__ = ('_name', '_requires', '_cache_paths', '_group_type', '_state_group_type', '_properties', '_children', '_consistencies', '_calc_properties', '__weakref__', - '_readonly', '_impl_informations') + '_readonly', '_impl_informations', '_state_requires', + '_state_consistencies') _opt_type = 'optiondescription' def __init__(self, name, doc, children, requires=None, properties=None): @@ -838,6 +845,12 @@ class OptionDescription(BaseOption): raise TypeError(_('invalid properties type {0} for {1},' ' must be a tuple').format(type(properties), self._name)) + if self._calc_properties is not None and properties is not tuple(): + set_forbidden_properties = set(properties) & self._calc_properties + if set_forbidden_properties != frozenset(): + raise ValueError('conflict: properties already set in ' + 'requirement {0}'.format( + list(set_forbidden_properties))) self._properties = properties # 'hidden', 'disabled'... # the group_type is useful for filtering OptionDescriptions in a config self._group_type = groups.default From 8ccfba167183cb949847ac1cd616ddb6fcf99e4f Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 20:37:23 +0200 Subject: [PATCH 11/50] factorise Option and OptionDescription init --- tiramisu/option.py | 69 +++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index 9a2dd5d..f606536 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -98,6 +98,30 @@ class BaseOption(BaseInformation): """ __slots__ = ('_readonly', '_state_consistencies', '_state_requires') + def __init__(self, name, doc, requires, properties): + if not valid_name(name): + raise ValueError(_("invalid name: {0} for option").format(name)) + self._name = name + self._impl_informations = {} + self.impl_set_information('doc', doc) + self._calc_properties, self._requires = validate_requires_arg( + requires, self._name) + self._consistencies = None + if properties is None: + properties = tuple() + if not isinstance(properties, tuple): + raise TypeError(_('invalid properties type {0} for {1},' + ' must be a tuple').format( + type(properties), + self._name)) + if self._calc_properties is not None and properties is not tuple(): + set_forbidden_properties = set(properties) & self._calc_properties + if set_forbidden_properties != frozenset(): + raise ValueError('conflict: properties already set in ' + 'requirement {0}'.format( + list(set_forbidden_properties))) + self._properties = properties # 'hidden', 'disabled'... + def __setattr__(self, name, value): """set once and only once some attributes in the option, like `_name`. `_name` cannot be changed one the option and @@ -234,15 +258,8 @@ class Option(BaseOption): :param validator_args: the validator's parameters """ - if not valid_name(name): - raise ValueError(_("invalid name: {0} for option").format(name)) - self._name = name - self._impl_informations = {} - self.impl_set_information('doc', doc) - self._calc_properties, self._requires = validate_requires_arg( - requires, self._name) + super(Option, self).__init__(name, doc, requires, properties) self._multi = multi - self._consistencies = None if validator is not None: if type(validator) != FunctionType: raise TypeError(_("validator must be a function")) @@ -285,20 +302,6 @@ class Option(BaseOption): self._default_multi = default_multi self.impl_validate(default) self._default = default - if properties is None: - properties = tuple() - if not isinstance(properties, tuple): - raise TypeError(_('invalid properties type {0} for {1},' - ' must be a tuple').format( - type(properties), - self._name)) - if self._calc_properties is not None and properties is not tuple(): - set_forbidden_properties = set(properties) & self._calc_properties - if set_forbidden_properties != frozenset(): - raise ValueError('conflict: properties already set in ' - 'requirement {0}'.format( - list(set_forbidden_properties))) - self._properties = properties # 'hidden', 'disabled'... def _launch_consistency(self, func, opt, vals, context, index, opt_): if context is not None: @@ -819,12 +822,7 @@ class OptionDescription(BaseOption): :param children: a list of options (including optiondescriptions) """ - if not valid_name(name): - raise ValueError(_("invalid name:" - " {0} for optiondescription").format(name)) - self._name = name - self._impl_informations = {} - self.impl_set_information('doc', doc) + super(OptionDescription, self).__init__(name, doc, requires, properties) child_names = [child._name for child in children] #better performance like this valid_child = copy(child_names) @@ -836,22 +834,7 @@ class OptionDescription(BaseOption): '{0}').format(child)) old = child self._children = (tuple(child_names), tuple(children)) - self._calc_properties, self._requires = validate_requires_arg(requires, self._name) self._cache_paths = None - self._consistencies = None - if properties is None: - properties = tuple() - if not isinstance(properties, tuple): - raise TypeError(_('invalid properties type {0} for {1},' - ' must be a tuple').format(type(properties), - self._name)) - if self._calc_properties is not None and properties is not tuple(): - set_forbidden_properties = set(properties) & self._calc_properties - if set_forbidden_properties != frozenset(): - raise ValueError('conflict: properties already set in ' - 'requirement {0}'.format( - list(set_forbidden_properties))) - self._properties = properties # 'hidden', 'disabled'... # the group_type is useful for filtering OptionDescriptions in a config self._group_type = groups.default From 84b7ec7b379bc7a41aa120e446f045c07954ec63 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 20:46:51 +0200 Subject: [PATCH 12/50] update __slots__ for Option/BaseOption --- tiramisu/option.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index f606536..67b2fa5 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -96,7 +96,9 @@ class BaseOption(BaseInformation): in options that have to be set only once, it is of course done in the __setattr__ method """ - __slots__ = ('_readonly', '_state_consistencies', '_state_requires') + __slots__ = ('_name', '_requires', '_properties', '_readonly', + '_consistencies', '_calc_properties', '_state_consistencies', + '_state_requires') def __init__(self, name, doc, requires, properties): if not valid_name(name): @@ -230,10 +232,8 @@ class Option(BaseOption): Reminder: an Option object is **not** a container for the value """ - __slots__ = ('_name', '_requires', '_multi', '_validator', - '_default_multi', '_default', '_properties', '_callback', - '_multitype', '_master_slaves', '_consistencies', - '_calc_properties', '__weakref__') + __slots__ = ('_multi', '_validator', '_default_multi', '_default', '_callback', + '_multitype', '_master_slaves', '__weakref__') _empty = '' def __init__(self, name, doc, default=None, default_multi=None, From cc3a33ef4f4559b119f1da5be3be7349212e9f82 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 21:29:41 +0200 Subject: [PATCH 13/50] true serialize for _children --- tiramisu/option.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index 67b2fa5..f5b171c 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -98,7 +98,7 @@ class BaseOption(BaseInformation): """ __slots__ = ('_name', '_requires', '_properties', '_readonly', '_consistencies', '_calc_properties', '_state_consistencies', - '_state_requires') + '_state_requires', '_stated') def __init__(self, name, doc, requires, properties): if not valid_name(name): @@ -200,15 +200,20 @@ class BaseOption(BaseInformation): self._state_requires = new_value def _impl_getstate(self, descr): + self._stated = True self._impl_convert_consistencies(descr) self._impl_convert_requires(descr) def __getstate__(self, export=False): + try: + self._stated + except AttributeError: + raise SystemError(_('cannot serialize Option, only in OptionDescription')) slots = set() for subclass in self.__class__.__mro__: if subclass is not object: slots.update(subclass.__slots__) - slots -= frozenset(['_children', '_cache_paths', '__weakref__']) + slots -= frozenset(['_cache_paths', '__weakref__']) states = {} for slot in slots: # remove variable if save variable converted in _state_xxxx variable @@ -814,7 +819,7 @@ class OptionDescription(BaseOption): '_state_group_type', '_properties', '_children', '_consistencies', '_calc_properties', '__weakref__', '_readonly', '_impl_informations', '_state_requires', - '_state_consistencies') + '_state_consistencies', '_stated') _opt_type = 'optiondescription' def __init__(self, name, doc, children, requires=None, properties=None): @@ -1017,14 +1022,16 @@ class OptionDescription(BaseOption): for option in self.impl_getchildren(): option._impl_getstate(descr) - def __getstate__(self, export=False): - if not export: + def __getstate__(self): + try: + del(self._stated) + except AttributeError: + # if cannot delete, _impl_getstate never launch + # launch it recursivement + # _stated prevent __getstate__ launch more than one time + # _stated is delete, if re-serialize, re-lauch _impl_getstate self._impl_getstate() - states = super(OptionDescription, self).__getstate__(True) - states['_state_children'] = [] - for option in self.impl_getchildren(): - states['_state_children'].append(option.__getstate__(True)) - return states + return super(OptionDescription, self).__getstate__() def validate_requires_arg(requires, name): From 0212a1538740f9fedac68bfe86bc57f61dd8ffc1 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 2 Sep 2013 23:04:37 +0200 Subject: [PATCH 14/50] add __setstate__ to loads from a serialised object --- tiramisu/option.py | 130 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 30 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index f5b171c..eca61e2 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -98,7 +98,7 @@ class BaseOption(BaseInformation): """ __slots__ = ('_name', '_requires', '_properties', '_readonly', '_consistencies', '_calc_properties', '_state_consistencies', - '_state_requires', '_stated') + '_state_readonly', '_state_requires', '_stated') def __init__(self, name, doc, requires, properties): if not valid_name(name): @@ -133,7 +133,7 @@ class BaseOption(BaseInformation): "frozen" (which has noting to do with the high level "freeze" propertie or "read_only" property) """ - if not name.startswith('_state'): + if not name.startswith('_state') and name not in ('_cache_paths', '_consistencies'): is_readonly = False # never change _name if name == '_name': @@ -160,42 +160,58 @@ class BaseOption(BaseInformation): name)) object.__setattr__(self, name, value) - def _impl_convert_consistencies(self, cache): - # cache is a dico in import/not a dico in export - if self._consistencies is None: + def _impl_convert_consistencies(self, descr, load=False): + if not load and self._consistencies is None: self._state_consistencies = None + elif load and self._state_consistencies is None: + self._consistencies = None + del(self._state_consistencies) else: + if load: + consistencies = self._state_consistencies + else: + consistencies = self._consistencies new_value = [] - for consistency in self._consistencies: - if isinstance(cache, dict): - new_value.append((consistency[0], cache[consistency[1]])) + for consistency in consistencies: + if load: + new_value.append((consistency[0], + descr.impl_get_opt_by_path( + consistency[1]))) else: new_value.append((consistency[0], - cache.impl_get_path_by_opt( + descr.impl_get_path_by_opt( consistency[1]))) - if isinstance(cache, dict): - pass + if load: + del(self._state_consistencies) + self._consistencies = tuple(new_value) else: self._state_consistencies = tuple(new_value) - def _impl_convert_requires(self, cache): - # cache is a dico in import/not a dico in export - if self._requires is None: + def _impl_convert_requires(self, descr, load=False): + if not load and self._requires is None: self._state_requires = None + elif load and self._state_requires is None: + self._requires = None + del(self._state_requires) else: + if load: + _requires = self._state_requires + else: + _requires = self._requires new_value = [] - for requires in self._requires: + for requires in _requires: new_requires = [] for require in requires: - if isinstance(cache, dict): - new_require = [cache[require[0]]] + if load: + new_require = [descr.impl_get_opt_by_path(require[0])] else: - new_require = [cache.impl_get_path_by_opt(require[0])] + new_require = [descr.impl_get_path_by_opt(require[0])] new_require.extend(require[1:]) new_requires.append(tuple(new_require)) new_value.append(tuple(new_requires)) - if isinstance(cache, dict): - pass + if load: + del(self._state_requires) + self._requires = new_value else: self._state_requires = new_value @@ -203,8 +219,12 @@ class BaseOption(BaseInformation): self._stated = True self._impl_convert_consistencies(descr) self._impl_convert_requires(descr) + try: + self._state_readonly = self._readonly + except AttributeError: + pass - def __getstate__(self, export=False): + def __getstate__(self, stated=True): try: self._stated except AttributeError: @@ -228,8 +248,24 @@ class BaseOption(BaseInformation): states[slot] = getattr(self, slot) except AttributeError: pass + if not stated: + del(states['_stated']) return states + def _impl_setstate(self, descr): + self._impl_convert_consistencies(descr, load=True) + self._impl_convert_requires(descr, load=True) + try: + self._readonly = self._state_readonly + del(self._state_readonly) + del(self._stated) + except AttributeError: + pass + + def __setstate__(self, state): + for key, value in state.items(): + setattr(self, key, value) + class Option(BaseOption): """ @@ -596,6 +632,11 @@ class SymLinkOption(BaseOption): super(SymLinkOption, self)._impl_getstate(descr) self._state_opt = descr.impl_get_path_by_opt(self._opt) + def _impl_setstate(self, descr): + self._opt = descr.impl_get_opt_by_path(self._state_opt) + del(self._state_opt) + super(SymLinkOption, self)._impl_setstate(descr) + class IPOption(Option): "represents the choice of an ip" @@ -885,14 +926,16 @@ class OptionDescription(BaseOption): cache_path=None, cache_option=None, _currpath=None, - _consistencies=None): + _consistencies=None, + force_no_consistencies=False): if _currpath is None and self._cache_paths is not None: # cache already set return if _currpath is None: save = True _currpath = [] - _consistencies = {} + if not force_no_consistencies: + _consistencies = {} else: save = False if cache_path is None: @@ -904,10 +947,12 @@ class OptionDescription(BaseOption): raise ConflictError(_('duplicate option: {0}').format(option)) cache_option.append(option) - option._readonly = True + if not force_no_consistencies: + option._readonly = True cache_path.append(str('.'.join(_currpath + [attr]))) if not isinstance(option, OptionDescription): - if option._consistencies is not None: + if not force_no_consistencies and \ + option._consistencies is not None: for consistency in option._consistencies: func, opt = consistency opts = (option, opt) @@ -920,12 +965,14 @@ class OptionDescription(BaseOption): option.impl_build_cache(cache_path, cache_option, _currpath, - _consistencies) + _consistencies, + force_no_consistencies) _currpath.pop() if save: self._cache_paths = (tuple(cache_option), tuple(cache_path)) - self._consistencies = _consistencies - self._readonly = True + if not force_no_consistencies: + self._consistencies = _consistencies + self._readonly = True def impl_get_opt_by_path(self, path): try: @@ -1023,15 +1070,38 @@ class OptionDescription(BaseOption): option._impl_getstate(descr) def __getstate__(self): + stated = True try: - del(self._stated) + self._stated except AttributeError: # if cannot delete, _impl_getstate never launch # launch it recursivement # _stated prevent __getstate__ launch more than one time # _stated is delete, if re-serialize, re-lauch _impl_getstate self._impl_getstate() - return super(OptionDescription, self).__getstate__() + stated = False + return super(OptionDescription, self).__getstate__(stated) + + def _impl_setstate(self, descr=None): + """enables us to import from a dict + :param descr: parent :class:`tiramisu.option.OptionDescription` + """ + if descr is None: + self._cache_paths = None + self.impl_build_cache(force_no_consistencies=True) + descr = self + self._group_type = getattr(groups, self._state_group_type) + del(self._state_group_type) + super(OptionDescription, self)._impl_setstate(descr) + for option in self.impl_getchildren(): + option._impl_setstate(descr) + + def __setstate__(self, state): + super(OptionDescription, self).__setstate__(state) + try: + self._stated + except AttributeError: + self._impl_setstate() def validate_requires_arg(requires, name): From dcd6efd06316a561657e628590276e175f4222f8 Mon Sep 17 00:00:00 2001 From: gwen Date: Tue, 3 Sep 2013 09:34:53 +0200 Subject: [PATCH 15/50] resise logo --- doc/logo.png | Bin 151818 -> 2591 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/logo.png b/doc/logo.png index 08e33cd02becab34052bd0c4a058f4e2b9f7aa1f..084a5346d00ddbdad245dd3706d8128ebf47fdf0 100644 GIT binary patch delta 2533 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 literal 151818 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 Date: Tue, 3 Sep 2013 10:38:28 +0200 Subject: [PATCH 16/50] impl_get_information and impl_set_information are, now, persistent in storage --- test/test_config.py | 1 + test/test_storage.py | 18 ++++++- tiramisu/config.py | 31 ++++++++---- tiramisu/option.py | 72 ++++++++++++---------------- tiramisu/storage/dictionary/value.py | 22 ++++++++- tiramisu/storage/sqlite3/value.py | 29 ++++++++++- tiramisu/value.py | 24 ++++++++++ 7 files changed, 143 insertions(+), 54 deletions(-) diff --git a/test/test_config.py b/test/test_config.py index 4e38f19..17e863a 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -141,6 +141,7 @@ def test_information_config(): string = 'some informations' config.impl_set_information('info', string) assert config.impl_get_information('info') == string + raises(ValueError, "config.impl_get_information('noinfo')") def test_config_impl_get_path_by_opt(): diff --git a/test/test_storage.py b/test/test_storage.py index 6dbb721..56c44e5 100644 --- a/test/test_storage.py +++ b/test/test_storage.py @@ -18,7 +18,6 @@ def test_list(): b = BoolOption('b', '') o = OptionDescription('od', '', [b]) c = Config(o, session_id='test_non_persistent') - from tiramisu.setting import list_sessions assert 'test_non_persistent' in list_sessions() del(c) assert 'test_non_persistent' not in list_sessions() @@ -43,7 +42,6 @@ def test_list_sessions_persistent(): # storage is not persistent pass else: - from tiramisu.setting import list_sessions assert 'test_persistent' in list_sessions() @@ -124,3 +122,19 @@ def test_two_persistent_owner(): assert c.getowner(b) == owners.persistent assert c2.getowner(b) == owners.persistent delete_session('test_persistent') + + +def test_two_persistent_information(): + b = BoolOption('b', '') + o = OptionDescription('od', '', [b]) + try: + c = Config(o, session_id='test_persistent', persistent=True) + except ValueError: + # storage is not persistent + pass + else: + c.impl_set_information('info', 'string') + assert c.impl_get_information('info') == 'string' + c2 = Config(o, session_id='test_persistent', persistent=True) + assert c2.impl_get_information('info') == 'string' + delete_session('test_persistent') diff --git a/tiramisu/config.py b/tiramisu/config.py index ae68e98..8537c0d 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -22,14 +22,13 @@ # ____________________________________________________________ import weakref from tiramisu.error import PropertiesOptionError, ConfigError -from tiramisu.option import OptionDescription, Option, SymLinkOption, \ - BaseInformation +from tiramisu.option import OptionDescription, Option, SymLinkOption from tiramisu.setting import groups, Settings, default_encoding, get_storage from tiramisu.value import Values from tiramisu.i18n import _ -class SubConfig(BaseInformation): +class SubConfig(object): "sub configuration management entry" __slots__ = ('_impl_context', '_impl_descr', '_impl_path') @@ -252,10 +251,10 @@ class SubConfig(BaseInformation): :returns: list of matching Option objects """ return self._cfgimpl_get_context()._find(bytype, byname, byvalue, - first=False, - type_=type_, - _subpath=self.cfgimpl_get_path() - ) + first=False, + type_=type_, + _subpath=self.cfgimpl_get_path() + ) def find_first(self, bytype=None, byname=None, byvalue=None, type_='option', display_error=True): @@ -501,6 +500,22 @@ class CommonConfig(SubConfig): def cfgimpl_get_meta(self): return self._impl_meta + # information + def impl_set_information(self, key, value): + """updates the information's attribute + + :param key: information's key (ex: "help", "doc" + :param value: information's value (ex: "the help string") + """ + self._impl_values.set_information(key, value) + + def impl_get_information(self, key, default=None): + """retrieves one information's item + + :param key: the item string (ex: "help") + """ + return self._impl_values.get_information(key, default) + # ____________________________________________________________ class Config(CommonConfig): @@ -526,7 +541,6 @@ class Config(CommonConfig): super(Config, self).__init__(descr, weakref.ref(self)) self._impl_build_all_paths() self._impl_meta = None - self._impl_informations = {} def cfgimpl_reset_cache(self, only_expired=False, @@ -565,7 +579,6 @@ class Config(CommonConfig): # self._impl_settings = Settings(self, storage) # self._impl_values = Values(self, storage) # self._impl_meta = None -# self._impl_informations = {} # def cfgimpl_get_children(self): # return self._impl_children diff --git a/tiramisu/option.py b/tiramisu/option.py index eca61e2..b8bcc0a 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -54,51 +54,15 @@ def valid_name(name): # -class BaseInformation(object): - "interface for an option's information attribute" - __slots__ = ('_impl_informations',) - - 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") - """ - try: - self._impl_informations[key] = value - except AttributeError: - raise AttributeError(_('{0} has no attribute ' - 'impl_set_information').format( - self.__class__.__name__)) - - def impl_get_information(self, key, default=None): - """retrieves one information's item - - :param key: the item string (ex: "help") - """ - try: - if key in self._impl_informations: - return self._impl_informations[key] - elif default is not None: - return default - else: - raise ValueError(_("information's item" - " not found: {0}").format(key)) - except AttributeError: - raise AttributeError(_('{0} has no attribute ' - 'impl_get_information').format( - self.__class__.__name__)) - - -class BaseOption(BaseInformation): +class BaseOption(object): """This abstract base class stands for attribute access in options that have to be set only once, it is of course done in the __setattr__ method """ __slots__ = ('_name', '_requires', '_properties', '_readonly', - '_consistencies', '_calc_properties', '_state_consistencies', - '_state_readonly', '_state_requires', '_stated') + '_consistencies', '_calc_properties', '_impl_informations', + '_state_consistencies', '_state_readonly', '_state_requires', + '_stated') def __init__(self, name, doc, requires, properties): if not valid_name(name): @@ -160,6 +124,30 @@ class BaseOption(BaseInformation): name)) object.__setattr__(self, name, value) + # 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._impl_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._impl_informations: + return self._impl_informations[key] + elif default is not None: + return default + else: + raise ValueError(_("information's item not found: {0}").format( + key)) + + # serialize/unserialize def _impl_convert_consistencies(self, descr, load=False): if not load and self._consistencies is None: self._state_consistencies = None @@ -215,6 +203,7 @@ class BaseOption(BaseInformation): else: self._state_requires = new_value + # serialize def _impl_getstate(self, descr): self._stated = True self._impl_convert_consistencies(descr) @@ -252,6 +241,7 @@ class BaseOption(BaseInformation): del(states['_stated']) return states + # unserialize def _impl_setstate(self, descr): self._impl_convert_consistencies(descr, load=True) self._impl_convert_requires(descr, load=True) @@ -860,7 +850,7 @@ class OptionDescription(BaseOption): '_state_group_type', '_properties', '_children', '_consistencies', '_calc_properties', '__weakref__', '_readonly', '_impl_informations', '_state_requires', - '_state_consistencies', '_stated') + '_state_consistencies', '_stated', '_state_readonly') _opt_type = 'optiondescription' def __init__(self, name, doc, children, requires=None, properties=None): diff --git a/tiramisu/storage/dictionary/value.py b/tiramisu/storage/dictionary/value.py index 9c3e1f9..c1bf2eb 100644 --- a/tiramisu/storage/dictionary/value.py +++ b/tiramisu/storage/dictionary/value.py @@ -22,12 +22,13 @@ from tiramisu.storage.dictionary.storage import Cache class Values(Cache): - __slots__ = ('_values', '__weakref__') + __slots__ = ('_values', '_informations', '__weakref__') def __init__(self, storage): """init plugin means create values storage """ self._values = {} + self._informations = {} # should init cache too super(Values, self).__init__(storage) @@ -72,3 +73,22 @@ class Values(Cache): return: owner object """ return self._values.get(path, (default, None))[0] + + def 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 get_information(self, key): + """retrieves one information's item + + :param key: the item string (ex: "help") + """ + if key in self._informations: + return self._informations[key] + else: + raise ValueError("not found") diff --git a/tiramisu/storage/sqlite3/value.py b/tiramisu/storage/sqlite3/value.py index 4207c43..2b9ddc7 100644 --- a/tiramisu/storage/sqlite3/value.py +++ b/tiramisu/storage/sqlite3/value.py @@ -32,7 +32,10 @@ class Values(Cache): super(Values, self).__init__('value', storage) values_table = 'CREATE TABLE IF NOT EXISTS value(path text primary ' values_table += 'key, value text, owner text)' - self.storage.execute(values_table) + self.storage.execute(values_table, commit=False) + informations_table = 'CREATE TABLE IF NOT EXISTS information(key text primary ' + informations_table += 'key, value text)' + self.storage.execute(informations_table) for owner in self.storage.select("SELECT DISTINCT owner FROM value", tuple(), False): try: getattr(owners, owner[0]) @@ -114,3 +117,27 @@ class Values(Cache): except AttributeError: owners.addowner(owner) return getattr(owners, owner) + + def 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.storage.execute("DELETE FROM information WHERE key = ?", (key,), + False) + self.storage.execute("INSERT INTO information(key, value) VALUES " + "(?, ?)", (key, self._sqlite_encode(value))) + + def get_information(self, key): + """retrieves one information's item + + :param key: the item string (ex: "help") + """ + value = self.storage.select("SELECT value FROM information WHERE key = ?", + (key,)) + if value is None: + raise ValueError("not found") + else: + return self._sqlite_decode(value[0]) diff --git a/tiramisu/value.py b/tiramisu/value.py index d846bc4..b401634 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -315,6 +315,30 @@ class Values(object): """ return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt) + # information + def set_information(self, key, value): + """updates the information's attribute + + :param key: information's key (ex: "help", "doc" + :param value: information's value (ex: "the help string") + """ + self._p_.set_information(key, value) + + def get_information(self, key, default=None): + """retrieves one information's item + + :param key: the item string (ex: "help") + """ + try: + return self._p_.get_information(key) + except ValueError: + if default is not None: + return default + else: + raise ValueError(_("information's item" + " not found: {0}").format(key)) + + # ____________________________________________________________ # multi types From f9fde44b3bf5f67df66df84b6199bbac15badc0a Mon Sep 17 00:00:00 2001 From: gwen Date: Tue, 3 Sep 2013 11:01:07 +0200 Subject: [PATCH 17/50] docstrings --- tiramisu/option.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tiramisu/option.py b/tiramisu/option.py index eca61e2..df0ef2c 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -161,6 +161,14 @@ class BaseOption(BaseInformation): object.__setattr__(self, name, value) def _impl_convert_consistencies(self, descr, load=False): + """during serialization process, many things have to be done. + one of them is the localisation of the options. + The paths are set once for all. + + :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 self._consistencies is None: self._state_consistencies = None elif load and self._state_consistencies is None: @@ -216,6 +224,11 @@ class BaseOption(BaseInformation): self._state_requires = new_value def _impl_getstate(self, descr): + """the under the hood stuff that need to be done + before the serialization. + + :param descr: the parent :class:`tiramisu.option.OptionDescription` + """ self._stated = True self._impl_convert_consistencies(descr) self._impl_convert_requires(descr) @@ -225,6 +238,14 @@ class BaseOption(BaseInformation): pass def __getstate__(self, stated=True): + """special method to enable the serialization with pickle + Usualy, a `__getstate__` method does'nt need any parameter, + but somme under the hood stuff need to be done before this action + + :parameter stated: if stated is `True`, the serialization protocol + can be performed, not ready yet otherwise + :parameter type: bool + """ try: self._stated except AttributeError: @@ -1070,8 +1091,12 @@ class OptionDescription(BaseOption): option._impl_getstate(descr) def __getstate__(self): + """special method to enable the serialization with pickle + """ stated = True try: + # the `_state` attribute is a flag that which tells us if + # the serialization can be performed self._stated except AttributeError: # if cannot delete, _impl_getstate never launch From 3b733d1b4f9a9b82ce994eb9931cfdc11c787553 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Tue, 3 Sep 2013 22:41:18 +0200 Subject: [PATCH 18/50] support cache consistencies + no consistencies for a symlink + test --- test/test_option_consistency.py | 12 ++++++++- tiramisu/option.py | 48 ++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/test/test_option_consistency.py b/test/test_option_consistency.py index 8dde2a8..5cf53cd 100644 --- a/test/test_option_consistency.py +++ b/test/test_option_consistency.py @@ -4,7 +4,7 @@ from py.test import raises from tiramisu.setting import owners, groups from tiramisu.config import Config from tiramisu.option import IPOption, NetworkOption, NetmaskOption, IntOption,\ - OptionDescription + SymLinkOption, OptionDescription def test_consistency_not_equal(): @@ -22,6 +22,16 @@ def test_consistency_not_equal(): c.b = 2 +def test_consistency_not_equal_symlink(): + a = IntOption('a', '') + b = IntOption('b', '') + c = SymLinkOption('c', a) + od = OptionDescription('od', '', [a, b, c]) + a.impl_add_consistency('not_equal', b) + c = Config(od) + assert set(od._consistencies.keys()) == set([a, b]) + + def test_consistency_not_equal_multi(): a = IntOption('a', '', multi=True) b = IntOption('b', '', multi=True) diff --git a/tiramisu/option.py b/tiramisu/option.py index 4fb5434..b380817 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -167,21 +167,35 @@ class BaseOption(object): consistencies = self._state_consistencies else: consistencies = self._consistencies - new_value = [] - for consistency in consistencies: - if load: - new_value.append((consistency[0], - descr.impl_get_opt_by_path( - consistency[1]))) - else: - new_value.append((consistency[0], - descr.impl_get_path_by_opt( - consistency[1]))) + if isinstance(consistencies, list): + new_value = [] + for consistency in consistencies: + if load: + new_value.append((consistency[0], + descr.impl_get_opt_by_path( + consistency[1]))) + else: + new_value.append((consistency[0], + descr.impl_get_path_by_opt( + consistency[1]))) + + else: + new_value = {} + for key, _consistencies in consistencies.items(): + new_value[key] = [] + for key_cons, _cons in _consistencies: + _list_cons = [] + for _con in _cons: + if load: + _list_cons.append(descr.impl_get_opt_by_path(_con)) + else: + _list_cons.append(descr.impl_get_path_by_opt(_con)) + new_value[key].append((key_cons, tuple(_list_cons))) if load: del(self._state_consistencies) - self._consistencies = tuple(new_value) + self._consistencies = new_value else: - self._state_consistencies = tuple(new_value) + self._state_consistencies = new_value def _impl_convert_requires(self, descr, load=False): if not load and self._requires is None: @@ -621,8 +635,10 @@ else: class SymLinkOption(BaseOption): - __slots__ = ('_name', '_opt', '_state_opt') + __slots__ = ('_name', '_opt', '_state_opt', '_consistencies') _opt_type = 'symlink' + #not return _opt consistencies + _consistencies = {} def __init__(self, name, opt): self._name = name @@ -648,6 +664,12 @@ class SymLinkOption(BaseOption): del(self._state_opt) super(SymLinkOption, self)._impl_setstate(descr) + def _impl_convert_consistencies(self, descr, load=False): + if load: + del(self._state_consistencies) + else: + self._state_consistencies = None + class IPOption(Option): "represents the choice of an ip" From 9983739b2b4a5589f63f860b88b7efe8cd8a5896 Mon Sep 17 00:00:00 2001 From: gwen Date: Wed, 4 Sep 2013 09:05:12 +0200 Subject: [PATCH 19/50] pep8 line too long --- doc/doctest.txt | 9 --------- tiramisu/option.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/doc/doctest.txt b/doc/doctest.txt index be73752..d717c29 100644 --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -23,9 +23,6 @@ option APIs others ---------- -.. automodule:: test.test_config_api - :members: - .. automodule:: test.test_mandatory :members: @@ -38,18 +35,12 @@ others .. automodule:: test.test_option_consistency :members: -.. automodule:: test.test_option - :members: - .. automodule:: test.test_cache :members: .. automodule:: test.test_option_setting :members: -.. automodule:: test.test_config - :members: - .. automodule:: test.test_freeze :members: diff --git a/tiramisu/option.py b/tiramisu/option.py index 4fb5434..d483d9e 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -97,7 +97,8 @@ class BaseOption(object): "frozen" (which has noting to do with the high level "freeze" propertie or "read_only" property) """ - if not name.startswith('_state') and name not in ('_cache_paths', '_consistencies'): + if not name.startswith('_state') and \ + name not in ('_cache_paths', '_consistencies'): is_readonly = False # never change _name if name == '_name': @@ -184,6 +185,12 @@ class BaseOption(object): self._state_consistencies = tuple(new_value) def _impl_convert_requires(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 self._requires is None: self._state_requires = None elif load and self._state_requires is None: @@ -238,7 +245,8 @@ class BaseOption(object): try: self._stated except AttributeError: - raise SystemError(_('cannot serialize Option, only in OptionDescription')) + raise SystemError(_('cannot serialize Option, ' + 'only in OptionDescription')) slots = set() for subclass in self.__class__.__mro__: if subclass is not object: @@ -246,7 +254,8 @@ class BaseOption(object): slots -= frozenset(['_cache_paths', '__weakref__']) states = {} for slot in slots: - # remove variable if save variable converted in _state_xxxx variable + # remove variable if save variable converted + # in _state_xxxx variable if '_state' + slot not in slots: if slot.startswith('_state'): # should exists @@ -264,6 +273,11 @@ class BaseOption(object): # unserialize def _impl_setstate(self, descr): + """the under the hood stuff that need to be done + before the serialization. + + :type descr: :class:`tiramisu.option.OptionDescription` + """ self._impl_convert_consistencies(descr, load=True) self._impl_convert_requires(descr, load=True) try: @@ -274,6 +288,15 @@ class BaseOption(object): pass def __setstate__(self, state): + """special method that enables us to serialize (pickle) + + Usualy, a `__setstate__` method does'nt need any parameter, + but somme under the hood stuff need to be done before this action + + :parameter state: a dict is passed to the loads, it is the attributes + of the options object + :type state: dict + """ for key, value in state.items(): setattr(self, key, value) @@ -284,8 +307,8 @@ class Option(BaseOption): Reminder: an Option object is **not** a container for the value """ - __slots__ = ('_multi', '_validator', '_default_multi', '_default', '_callback', - '_multitype', '_master_slaves', '__weakref__') + __slots__ = ('_multi', '_validator', '_default_multi', '_default', + '_callback', '_multitype', '_master_slaves', '__weakref__') _empty = '' def __init__(self, name, doc, default=None, default_multi=None, From dc688ad644d22f261ec067954e6d8ec953d53ca2 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 4 Sep 2013 09:09:37 +0200 Subject: [PATCH 20/50] ro/rw_append/remove are now 'set' type --- tiramisu/setting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 46741b5..c2ca92f 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -30,11 +30,11 @@ from tiramisu.i18n import _ default_encoding = 'utf-8' expires_time = 5 -ro_remove = ('permissive', 'hidden') -ro_append = ('frozen', 'disabled', 'validator', 'everything_frozen', - 'mandatory') -rw_remove = ('permissive', 'everything_frozen', 'mandatory') -rw_append = ('frozen', 'disabled', 'validator', 'hidden') +ro_remove = set('permissive', 'hidden') +ro_append = set('frozen', 'disabled', 'validator', 'everything_frozen', + 'mandatory') +rw_remove = set('permissive', 'everything_frozen', 'mandatory') +rw_append = set('frozen', 'disabled', 'validator', 'hidden') default_properties = ('expire', 'validator') From e73f3e0e6c36ab72c161344224a7a06d56ed23de Mon Sep 17 00:00:00 2001 From: gwen Date: Thu, 5 Sep 2013 16:56:02 +0200 Subject: [PATCH 21/50] doc --- doc/config.txt | 10 +++++----- doc/getting-started.txt | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/config.txt b/doc/config.txt index ed1a3ca..2118f4c 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -145,7 +145,7 @@ let's come back to the default value value The value is saved in a :class:`~tiramisu.value.Value` object. It is on this -object that we have to trigger the `reset`, wich take the option itself +object that we have to trigger the `reset`, which take the option itself (`var2`) as a parameter. On the other side, in the `read_only` mode, it is not possible to modify the value @@ -415,7 +415,7 @@ A multi-option's value can be manipulated like a list:: >>> print c.od1.var1 [u'var1'] -But it is not possible to set a value to a multi-option wich is not a list:: +But it is not possible to set a value to a multi-option which is not a list:: >>> c.od1.var1 = u'error' Traceback (most recent call last): @@ -543,6 +543,6 @@ Here are the (useful) methods on ``Config`` (or `SubConfig`). A :class:`~config.CommonConfig` is a abstract base class. A :class:`~config.SubConfig` is an just in time created objects that wraps an ::class:`~option.OptionDescription`. A SubConfig differs from a Config in the -::fact that a config is a root object and has an environnement, a context wich -::defines the different properties, access rules, vs... There is generally only -::one Config, and many SubConfigs. +fact that a config is a root object and has an environnement, a context which +defines the different properties, access rules, vs... There is generally only +one Config, and many SubConfigs. diff --git a/doc/getting-started.txt b/doc/getting-started.txt index 13cc8f2..a2f6a74 100644 --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,7 +14,7 @@ introduced... What is Tiramisu ? =================== -Tiramisu is an options handler and an options controller, wich aims at +Tiramisu is an options handler and an options controller, which aims at producing flexible and fast options access. The main advantages are its access rules and the fact that the whole consistency is preserved at any time, see :doc:`consistency`. There is of course type and structure validations, but also @@ -65,7 +65,7 @@ So by now, we have: - a namespace (which is `c` here) - the access of an option's value by the - attribute access way (here `bool`, wich is a boolean option + attribute access way (here `bool`, which is a boolean option :class:`~tiramisu.option.BoolOption()`. So, option objects are produced at the entry point `c` and then handed down to From 18fc5db4ac2478beeca96f763bf33add9d2f92e1 Mon Sep 17 00:00:00 2001 From: gwen Date: Fri, 6 Sep 2013 09:05:19 +0200 Subject: [PATCH 22/50] lists in sets --- tiramisu/setting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tiramisu/setting.py b/tiramisu/setting.py index c2ca92f..99fda6a 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -30,11 +30,11 @@ from tiramisu.i18n import _ default_encoding = 'utf-8' expires_time = 5 -ro_remove = set('permissive', 'hidden') -ro_append = set('frozen', 'disabled', 'validator', 'everything_frozen', - 'mandatory') -rw_remove = set('permissive', 'everything_frozen', 'mandatory') -rw_append = set('frozen', 'disabled', 'validator', 'hidden') +ro_remove = set(['permissive', 'hidden']) +ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen', + 'mandatory']) +rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) +rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) default_properties = ('expire', 'validator') From 22bfbb9fa4449d4fb8acc19da8b619c05710e61e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 6 Sep 2013 23:15:28 +0200 Subject: [PATCH 23/50] storage no more in setting.py, code is now in storage/__init__.py --- test/test_state.py | 121 ++++++++++++++++++++++++ test/test_storage.py | 2 +- tiramisu/config.py | 15 +-- tiramisu/option.py | 2 +- tiramisu/setting.py | 59 +----------- tiramisu/storage/__init__.py | 90 ++++++++++++++++++ tiramisu/storage/dictionary/__init__.py | 24 +++++ tiramisu/storage/dictionary/cache.py | 61 ++++++++++++ tiramisu/storage/dictionary/setting.py | 2 +- tiramisu/storage/dictionary/storage.py | 46 +-------- tiramisu/storage/dictionary/value.py | 2 +- tiramisu/storage/sqlite3/__init__.py | 24 +++++ tiramisu/storage/sqlite3/cache.py | 98 +++++++++++++++++++ tiramisu/storage/sqlite3/setting.py | 2 +- tiramisu/storage/sqlite3/storage.py | 79 ---------------- tiramisu/storage/sqlite3/value.py | 2 +- tiramisu/value.py | 4 +- 17 files changed, 437 insertions(+), 196 deletions(-) create mode 100644 test/test_state.py create mode 100644 tiramisu/storage/dictionary/cache.py create mode 100644 tiramisu/storage/sqlite3/cache.py diff --git a/test/test_state.py b/test/test_state.py new file mode 100644 index 0000000..03ab670 --- /dev/null +++ b/test/test_state.py @@ -0,0 +1,121 @@ +from tiramisu.option import BoolOption, UnicodeOption, SymLinkOption, \ + OptionDescription +from pickle import dumps, loads + + +def _get_slots(opt): + slots = set() + for subclass in opt.__class__.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + return slots + + +def _no_state(opt): + for attr in _get_slots(opt): + if 'state' in attr: + try: + getattr(opt, attr) + except: + pass + else: + raise Exception('opt should have already attribute {0}'.format(attr)) + + +def _diff_opt(opt1, opt2): + attr1 = set(_get_slots(opt1)) + attr2 = set(_get_slots(opt2)) + diff1 = attr1 - attr2 + diff2 = attr2 - attr1 + if diff1 != set(): + raise Exception('more attribute in opt1 {0}'.format(list(diff1))) + if diff2 != set(): + raise Exception('more attribute in opt2 {0}'.format(list(diff2))) + for attr in attr1: + if attr in ['_cache_paths']: + continue + err1 = False + err2 = False + val1 = None + val2 = None + try: + val1 = getattr(opt1, attr) + except: + err1 = True + + try: + val2 = getattr(opt2, attr) + except: + err2 = True + assert err1 == err2 + if val1 is None: + assert val1 == val2 + elif attr == '_children': + assert val1[0] == val2[0] + for index, _opt in enumerate(val1[1]): + assert _opt._name == val2[1][index]._name + elif attr == '_requires': + assert val1[0][0][0]._name == val2[0][0][0]._name + assert val1[0][0][1:] == val2[0][0][1:] + elif attr == '_opt': + assert val1._name == val2._name + elif attr == '_consistencies': + # dict is only a cache + if isinstance(val1, list): + for index, consistency in enumerate(val1): + assert consistency[0] == val2[index][0] + assert consistency[1]._name == val2[index][1]._name + else: + assert val1 == val2 + + +def test_diff_opt(): + b = BoolOption('b', '') + u = UnicodeOption('u', '', requires=[{'option': b, 'expected': True, 'action': 'disabled', 'inverse': True}]) + #u.impl_add_consistency('not_equal', b) + s = SymLinkOption('s', u) + o = OptionDescription('o', '', [b, u, s]) + o1 = OptionDescription('o1', '', [o]) + + a = dumps(o1) + q = loads(a) + _diff_opt(o1, q) + _diff_opt(o1.o, q.o) + _diff_opt(o1.o.b, q.o.b) + _diff_opt(o1.o.u, q.o.u) + _diff_opt(o1.o.s, q.o.s) + + +def test_diff_opt_cache(): + b = BoolOption('b', '') + u = UnicodeOption('u', '', requires=[{'option': b, 'expected': True, 'action': 'disabled', 'inverse': True}]) + u.impl_add_consistency('not_equal', b) + s = SymLinkOption('s', u) + o = OptionDescription('o', '', [b, u, s]) + o1 = OptionDescription('o1', '', [o]) + o1.impl_build_cache() + + a = dumps(o1) + q = loads(a) + _diff_opt(o1, q) + _diff_opt(o1.o, q.o) + _diff_opt(o1.o.b, q.o.b) + _diff_opt(o1.o.u, q.o.u) + _diff_opt(o1.o.s, q.o.s) + + +def test_no_state_attr(): + # all _state_xxx attributes should be deleted + b = BoolOption('b', '') + u = UnicodeOption('u', '', requires=[{'option': b, 'expected': True, 'action': 'disabled', 'inverse': True}]) + s = SymLinkOption('s', u) + o = OptionDescription('o', '', [b, u, s]) + o1 = OptionDescription('o1', '', [o]) + + a = dumps(o1) + q = loads(a) + _no_state(q) + _no_state(q.o) + _no_state(q.o.b) + _no_state(q.o.u) + _no_state(q.o.s) diff --git a/test/test_storage.py b/test/test_storage.py index 56c44e5..1527f5f 100644 --- a/test/test_storage.py +++ b/test/test_storage.py @@ -5,7 +5,7 @@ import autopath from tiramisu.config import Config from tiramisu.option import BoolOption, OptionDescription from tiramisu.setting import owners -from tiramisu.setting import list_sessions, delete_session +from tiramisu.storage import list_sessions, delete_session def test_non_persistent(): diff --git a/tiramisu/config.py b/tiramisu/config.py index 8537c0d..4659b51 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -23,7 +23,8 @@ import weakref from tiramisu.error import PropertiesOptionError, ConfigError from tiramisu.option import OptionDescription, Option, SymLinkOption -from tiramisu.setting import groups, Settings, default_encoding, get_storage +from tiramisu.setting import groups, Settings, default_encoding +from tiramisu.storage import get_storage from tiramisu.value import Values from tiramisu.i18n import _ @@ -535,9 +536,9 @@ class Config(CommonConfig): :param persistent: if persistent, don't delete storage when leaving :type persistent: `boolean` """ - storage = get_storage(self, session_id, persistent) - self._impl_settings = Settings(self, storage) - self._impl_values = Values(self, storage) + settings, values = get_storage(self, session_id, persistent) + self._impl_settings = Settings(self, settings) + self._impl_values = Values(self, values) super(Config, self).__init__(descr, weakref.ref(self)) self._impl_build_all_paths() self._impl_meta = None @@ -575,9 +576,9 @@ class Config(CommonConfig): # child._impl_meta = self # self._impl_children = children -# storage = get_storage(self, session_id, persistent) -# self._impl_settings = Settings(self, storage) -# self._impl_values = Values(self, storage) +# settings, values = get_storage(self, session_id, persistent) +# self._impl_settings = Settings(self, settings) +# self._impl_values = Values(self, values) # self._impl_meta = None # def cfgimpl_get_children(self): diff --git a/tiramisu/option.py b/tiramisu/option.py index 6e4d9da..6d7f3b3 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -658,7 +658,7 @@ else: class SymLinkOption(BaseOption): - __slots__ = ('_name', '_opt', '_state_opt', '_consistencies') + __slots__ = ('_name', '_opt', '_state_opt') _opt_type = 'symlink' #not return _opt consistencies _consistencies = {} diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 99fda6a..3c88c1c 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -24,7 +24,7 @@ from time import time from copy import copy import weakref from tiramisu.error import (RequirementError, PropertiesOptionError, - ConstError, ConfigError) + ConstError) from tiramisu.i18n import _ @@ -38,26 +38,6 @@ rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) default_properties = ('expire', 'validator') -class StorageType: - default_storage = 'dictionary' - storage_type = None - - def set_storage(self, name): - if self.storage_type is not None: - raise ConfigError(_('storage_type is already set, cannot rebind it')) - self.storage_type = name - - def get_storage(self): - if self.storage_type is None: - self.storage_type = self.default_storage - storage = self.storage_type - return 'tiramisu.storage.{0}.storage'.format( - storage) - - -storage_type = StorageType() - - class _NameSpace: """convenient class that emulates a module and builds constants (that is, unique names)""" @@ -203,39 +183,6 @@ class Property(object): return str(list(self._properties)) -def set_storage(name, **args): - storage_type.set_storage(name) - settings = __import__(storage_type.get_storage(), globals(), locals(), - ['Setting']).Setting() - for option, value in args.items(): - try: - getattr(settings, option) - setattr(settings, option, value) - except AttributeError: - raise ValueError(_('option {0} not already exists in storage {1}' - '').format(option, name)) - - -def get_storage(context, session_id, persistent): - def gen_id(config): - return str(id(config)) + str(time()) - - if session_id is None: - session_id = gen_id(context) - return __import__(storage_type.get_storage(), globals(), locals(), - ['Storage']).Storage(session_id, persistent) - - -def list_sessions(): - return __import__(storage_type.get_storage(), globals(), locals(), - ['list_sessions']).list_sessions() - - -def delete_session(session_id): - return __import__(storage_type.get_storage(), globals(), locals(), - ['delete_session']).delete_session(session_id) - - #____________________________________________________________ class Settings(object): "``Config()``'s configuration options" @@ -254,9 +201,7 @@ class Settings(object): # generic owner self._owner = owners.user self.context = weakref.ref(context) - import_lib = 'tiramisu.storage.{0}.setting'.format(storage.storage) - self._p_ = __import__(import_lib, globals(), locals(), ['Settings'] - ).Settings(storage) + self._p_ = storage #____________________________________________________________ # properties methods diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index e69de29..5476f26 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -0,0 +1,90 @@ +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# The original `Config` design model is unproudly borrowed from +# the rough gus of pypy: pypy: http://codespeak.net/svn/pypy/dist/pypy/config/ +# the whole pypy projet is under MIT licence +# ____________________________________________________________ + +"""Storage connections, executions and managements. + +Storage is basic components used to set informations in DB +""" + + +from time import time +from tiramisu.error import ConfigError +from tiramisu.i18n import _ + + +class StorageType(object): + default_storage = 'dictionary' + storage_type = None + mod = None + + def set(self, name): + if self.storage_type is not None: + raise ConfigError(_('storage_type is already set, cannot rebind it')) + self.storage_type = name + + def get(self): + if self.storage_type is None: + self.storage_type = self.default_storage + storage = self.storage_type + if self.mod is None: + modulepath = 'tiramisu.storage.{0}'.format(storage) + mod = __import__(modulepath) + for token in modulepath.split(".")[1:]: + mod = getattr(mod, token) + self.mod = mod + return self.mod + + +storage_type = StorageType() + + +def set_storage(name, **args): + storage_type.set(name) + settings = storage_type.get().Setting() + for option, value in args.items(): + try: + getattr(settings, option) + setattr(settings, option, value) + except AttributeError: + raise ValueError(_('option {0} not already exists in storage {1}' + '').format(option, name)) + + +def get_storage(context, session_id, persistent): + def gen_id(config): + return str(id(config)) + str(time()) + + if session_id is None: + session_id = gen_id(context) + imp = storage_type.get() + storage = imp.Storage(session_id, persistent) + return imp.Settings(storage), imp.Values(storage) + + +def list_sessions(): + return storage_type.get().list_sessions() + + +def delete_session(session_id): + return storage_type.get().delete_session(session_id) + + +#__all__ = (,) diff --git a/tiramisu/storage/dictionary/__init__.py b/tiramisu/storage/dictionary/__init__.py index e69de29..a53e505 100644 --- a/tiramisu/storage/dictionary/__init__.py +++ b/tiramisu/storage/dictionary/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"default plugin for storage: set it in a simple dictionary" +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# ____________________________________________________________ +from .value import Values +from .setting import Settings +from .storage import Storage, list_sessions, delete_session + +__all__ = (Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/dictionary/cache.py b/tiramisu/storage/dictionary/cache.py new file mode 100644 index 0000000..664990d --- /dev/null +++ b/tiramisu/storage/dictionary/cache.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"default plugin for cache: set it in a simple dictionary" +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# ____________________________________________________________ + + +class Cache(object): + __slots__ = ('_cache', 'storage') + key_is_path = False + + def __init__(self, storage): + self._cache = {} + self.storage = storage + + def setcache(self, cache_type, path, val, time): + self._cache[path] = (val, time) + + def getcache(self, cache_type, path, exp): + value, created = self._cache[path] + if exp < created: + return True, value + return False, None + + def hascache(self, cache_type, path): + """ path is in the cache + + :param cache_type: value | property + :param path: the path's option + """ + return path in self._cache + + def reset_expired_cache(self, cache_type, exp): + for key in tuple(self._cache.keys()): + val, created = self._cache[key] + if exp > created: + del(self._cache[key]) + + def reset_all_cache(self, cache_type): + "empty the cache" + self._cache.clear() + + def get_cached(self, cache_type, context): + """return all values in a dictionary + example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} + """ + return self._cache diff --git a/tiramisu/storage/dictionary/setting.py b/tiramisu/storage/dictionary/setting.py index 580cba3..ea59513 100644 --- a/tiramisu/storage/dictionary/setting.py +++ b/tiramisu/storage/dictionary/setting.py @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from tiramisu.storage.dictionary.storage import Cache +from .cache import Cache class Settings(Cache): diff --git a/tiramisu/storage/dictionary/storage.py b/tiramisu/storage/dictionary/storage.py index d4904c6..5442c5d 100644 --- a/tiramisu/storage/dictionary/storage.py +++ b/tiramisu/storage/dictionary/storage.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"default plugin for cache: set it in a simple dictionary" +"default plugin for storage: set it in a simple dictionary" # Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) # # This program is free software; you can redistribute it and/or modify @@ -38,7 +38,7 @@ def delete_session(session_id): class Storage(object): - __slots__ = ('session_id',) + __slots__ = ('session_id', 'values', 'settings') storage = 'dictionary' def __init__(self, session_id, persistent): @@ -54,45 +54,3 @@ class Storage(object): _list_sessions.remove(self.session_id) except AttributeError: pass - - -class Cache(object): - __slots__ = ('_cache', 'storage') - key_is_path = False - - def __init__(self, storage): - self._cache = {} - self.storage = storage - - def setcache(self, cache_type, path, val, time): - self._cache[path] = (val, time) - - def getcache(self, cache_type, path, exp): - value, created = self._cache[path] - if exp < created: - return True, value - return False, None - - def hascache(self, cache_type, path): - """ path is in the cache - - :param cache_type: value | property - :param path: the path's option - """ - return path in self._cache - - def reset_expired_cache(self, cache_type, exp): - for key in tuple(self._cache.keys()): - val, created = self._cache[key] - if exp > created: - del(self._cache[key]) - - def reset_all_cache(self, cache_type): - "empty the cache" - self._cache.clear() - - def get_cached(self, cache_type, context): - """return all values in a dictionary - example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} - """ - return self._cache diff --git a/tiramisu/storage/dictionary/value.py b/tiramisu/storage/dictionary/value.py index c1bf2eb..ed1017e 100644 --- a/tiramisu/storage/dictionary/value.py +++ b/tiramisu/storage/dictionary/value.py @@ -18,7 +18,7 @@ # # ____________________________________________________________ -from tiramisu.storage.dictionary.storage import Cache +from .cache import Cache class Values(Cache): diff --git a/tiramisu/storage/sqlite3/__init__.py b/tiramisu/storage/sqlite3/__init__.py index e69de29..adcdfdc 100644 --- a/tiramisu/storage/sqlite3/__init__.py +++ b/tiramisu/storage/sqlite3/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"set storage in sqlite3" +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# ____________________________________________________________ +from .value import Values +from .setting import Settings +from .storage import Storage, list_sessions, delete_session + +__all__ = (Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/sqlite3/cache.py b/tiramisu/storage/sqlite3/cache.py new file mode 100644 index 0000000..183c4d0 --- /dev/null +++ b/tiramisu/storage/sqlite3/cache.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +"sqlite3 cache" +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# ____________________________________________________________ +from pickle import dumps, loads + + +class Cache(object): + __slots__ = ('storage',) + key_is_path = True + + def __init__(self, cache_type, storage): + self.storage = storage + cache_table = 'CREATE TABLE IF NOT EXISTS cache_{0}(path '.format( + cache_type) + cache_table += 'text primary key, value text, time real)' + self.storage.execute(cache_table) + + # value + def _sqlite_decode_path(self, path): + if path == '_none': + return None + else: + return path + + def _sqlite_encode_path(self, path): + if path is None: + return '_none' + else: + return path + + def _sqlite_decode(self, value): + return loads(value) + + def _sqlite_encode(self, value): + if isinstance(value, list): + value = list(value) + return dumps(value) + + def setcache(self, cache_type, path, val, time): + convert_value = self._sqlite_encode(val) + path = self._sqlite_encode_path(path) + self.storage.execute("DELETE FROM cache_{0} WHERE path = ?".format( + cache_type), (path,), False) + self.storage.execute("INSERT INTO cache_{0}(path, value, time) " + "VALUES (?, ?, ?)".format(cache_type), + (path, convert_value, time)) + + def getcache(self, cache_type, path, exp): + path = self._sqlite_encode_path(path) + cached = self.storage.select("SELECT value FROM cache_{0} WHERE " + "path = ? AND time >= ?".format( + cache_type), (path, exp)) + if cached is None: + return False, None + else: + return True, self._sqlite_decode(cached[0]) + + def hascache(self, cache_type, path): + path = self._sqlite_encode_path(path) + return self.storage.select("SELECT value FROM cache_{0} WHERE " + "path = ?".format(cache_type), + (path,)) is not None + + def reset_expired_cache(self, cache_type, exp): + self.storage.execute("DELETE FROM cache_{0} WHERE time < ?".format( + cache_type), (exp,)) + + def reset_all_cache(self, cache_type): + self.storage.execute("DELETE FROM cache_{0}".format(cache_type)) + + def get_cached(self, cache_type, context): + """return all values in a dictionary + example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} + """ + ret = {} + for path, value, time in self.storage.select("SELECT * FROM cache_{0}" + "".format(cache_type), + only_one=False): + path = self._sqlite_decode_path(path) + value = self._sqlite_decode(value) + ret[path] = (value, time) + return ret diff --git a/tiramisu/storage/sqlite3/setting.py b/tiramisu/storage/sqlite3/setting.py index f91f9fd..11506bb 100644 --- a/tiramisu/storage/sqlite3/setting.py +++ b/tiramisu/storage/sqlite3/setting.py @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from tiramisu.storage.sqlite3.storage import Cache +from .cache import Cache class Settings(Cache): diff --git a/tiramisu/storage/sqlite3/storage.py b/tiramisu/storage/sqlite3/storage.py index d855c88..ed5abd9 100644 --- a/tiramisu/storage/sqlite3/storage.py +++ b/tiramisu/storage/sqlite3/storage.py @@ -18,7 +18,6 @@ # # ____________________________________________________________ -from pickle import dumps, loads from os import unlink from os.path import basename, splitext, join import sqlite3 @@ -79,81 +78,3 @@ class Storage(object): self._conn.close() if not self.persistent: delete_session(self._session_id) - - -class Cache(object): - __slots__ = ('storage',) - key_is_path = True - - def __init__(self, cache_type, storage): - self.storage = storage - cache_table = 'CREATE TABLE IF NOT EXISTS cache_{0}(path '.format( - cache_type) - cache_table += 'text primary key, value text, time real)' - self.storage.execute(cache_table) - - # value - def _sqlite_decode_path(self, path): - if path == '_none': - return None - else: - return path - - def _sqlite_encode_path(self, path): - if path is None: - return '_none' - else: - return path - - def _sqlite_decode(self, value): - return loads(value) - - def _sqlite_encode(self, value): - if isinstance(value, list): - value = list(value) - return dumps(value) - - def setcache(self, cache_type, path, val, time): - convert_value = self._sqlite_encode(val) - path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM cache_{0} WHERE path = ?".format( - cache_type), (path,), False) - self.storage.execute("INSERT INTO cache_{0}(path, value, time) " - "VALUES (?, ?, ?)".format(cache_type), - (path, convert_value, time)) - - def getcache(self, cache_type, path, exp): - path = self._sqlite_encode_path(path) - cached = self.storage.select("SELECT value FROM cache_{0} WHERE " - "path = ? AND time >= ?".format( - cache_type), (path, exp)) - if cached is None: - return False, None - else: - return True, self._sqlite_decode(cached[0]) - - def hascache(self, cache_type, path): - path = self._sqlite_encode_path(path) - return self.storage.select("SELECT value FROM cache_{0} WHERE " - "path = ?".format(cache_type), - (path,)) is not None - - def reset_expired_cache(self, cache_type, exp): - self.storage.execute("DELETE FROM cache_{0} WHERE time < ?".format( - cache_type), (exp,)) - - def reset_all_cache(self, cache_type): - self.storage.execute("DELETE FROM cache_{0}".format(cache_type)) - - def get_cached(self, cache_type, context): - """return all values in a dictionary - example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} - """ - ret = {} - for path, value, time in self.storage.select("SELECT * FROM cache_{0}" - "".format(cache_type), - only_one=False): - path = self._sqlite_decode_path(path) - value = self._sqlite_decode(value) - ret[path] = (value, time) - return ret diff --git a/tiramisu/storage/sqlite3/value.py b/tiramisu/storage/sqlite3/value.py index 2b9ddc7..687e7dd 100644 --- a/tiramisu/storage/sqlite3/value.py +++ b/tiramisu/storage/sqlite3/value.py @@ -18,7 +18,7 @@ # # ____________________________________________________________ -from tiramisu.storage.sqlite3.storage import Cache +from .cache import Cache from tiramisu.setting import owners diff --git a/tiramisu/value.py b/tiramisu/value.py index b401634..f35c16d 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -44,9 +44,7 @@ class Values(object): """ self.context = weakref.ref(context) # the storage type is dictionary or sqlite3 - import_lib = 'tiramisu.storage.{0}.value'.format(storage.storage) - self._p_ = __import__(import_lib, globals(), locals(), ['Values'], - ).Values(storage) + self._p_ = storage def _getdefault(self, opt): """ From c8876ab18481d40ec4d788aefcea12ed1d3868da Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 6 Sep 2013 23:53:19 +0200 Subject: [PATCH 24/50] comment storage --- tiramisu/storage/__init__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index 5476f26..ee74c16 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -21,7 +21,9 @@ """Storage connections, executions and managements. -Storage is basic components used to set informations in DB +Storage is basic components used to set Config informations in DB. +The primary "entry point" class into this package is the StorageType and it's +public configurator ``set_storage()``. """ @@ -31,6 +33,10 @@ from tiramisu.i18n import _ class StorageType(object): + """Object to store storage's type. If a Config is already set, + default storage is store as selected storage. You cannot change it + after. + """ default_storage = 'dictionary' storage_type = None mod = None @@ -57,6 +63,13 @@ storage_type = StorageType() def set_storage(name, **args): + """Change storage's configuration + + :params name: is the storage name. If storage is already set, cannot + reset storage name + + Other attributes are differents according to the selected storage's name + """ storage_type.set(name) settings = storage_type.get().Setting() for option, value in args.items(): @@ -80,11 +93,16 @@ def get_storage(context, session_id, persistent): def list_sessions(): + """List all available session (persistent or not persistent) + """ return storage_type.get().list_sessions() def delete_session(session_id): + """Delete a selected session, be careful, you can deleted a session + use by an other instance + """ return storage_type.get().delete_session(session_id) -#__all__ = (,) +__all__ = (set_storage, list_sessions, delete_session) From f8b0a53c3f3e4a24a464db239640add018343099 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 7 Sep 2013 10:31:39 +0200 Subject: [PATCH 25/50] cache is always a dictionary in memory --- test/test_storage.py | 2 + tiramisu/config.py | 6 +- tiramisu/storage/__init__.py | 3 +- tiramisu/storage/{dictionary => }/cache.py | 0 tiramisu/storage/dictionary/setting.py | 2 +- tiramisu/storage/dictionary/value.py | 2 +- tiramisu/storage/sqlite3/cache.py | 98 ---------------------- tiramisu/storage/sqlite3/setting.py | 6 +- tiramisu/storage/sqlite3/sqlite3db.py | 44 ++++++++++ tiramisu/storage/sqlite3/value.py | 6 +- 10 files changed, 59 insertions(+), 110 deletions(-) rename tiramisu/storage/{dictionary => }/cache.py (100%) delete mode 100644 tiramisu/storage/sqlite3/cache.py create mode 100644 tiramisu/storage/sqlite3/sqlite3db.py diff --git a/test/test_storage.py b/test/test_storage.py index 1527f5f..c3acc70 100644 --- a/test/test_storage.py +++ b/test/test_storage.py @@ -6,7 +6,9 @@ from tiramisu.config import Config from tiramisu.option import BoolOption, OptionDescription from tiramisu.setting import owners from tiramisu.storage import list_sessions, delete_session +from tiramisu import setting +setting.expires_time = 0 def test_non_persistent(): b = BoolOption('b', '') diff --git a/tiramisu/config.py b/tiramisu/config.py index 4659b51..fac3caf 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -24,7 +24,7 @@ import weakref from tiramisu.error import PropertiesOptionError, ConfigError from tiramisu.option import OptionDescription, Option, SymLinkOption from tiramisu.setting import groups, Settings, default_encoding -from tiramisu.storage import get_storage +from tiramisu.storage import get_storages from tiramisu.value import Values from tiramisu.i18n import _ @@ -536,7 +536,7 @@ class Config(CommonConfig): :param persistent: if persistent, don't delete storage when leaving :type persistent: `boolean` """ - settings, values = get_storage(self, session_id, persistent) + settings, values = get_storages(self, session_id, persistent) self._impl_settings = Settings(self, settings) self._impl_values = Values(self, values) super(Config, self).__init__(descr, weakref.ref(self)) @@ -576,7 +576,7 @@ class Config(CommonConfig): # child._impl_meta = self # self._impl_children = children -# settings, values = get_storage(self, session_id, persistent) +# settings, values = get_storages(self, session_id, persistent) # self._impl_settings = Settings(self, settings) # self._impl_values = Values(self, values) # self._impl_meta = None diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index ee74c16..562aad5 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -81,7 +81,7 @@ def set_storage(name, **args): '').format(option, name)) -def get_storage(context, session_id, persistent): +def get_storages(context, session_id, persistent): def gen_id(config): return str(id(config)) + str(time()) @@ -101,6 +101,7 @@ def list_sessions(): def delete_session(session_id): """Delete a selected session, be careful, you can deleted a session use by an other instance + :params session_id: id of session to delete """ return storage_type.get().delete_session(session_id) diff --git a/tiramisu/storage/dictionary/cache.py b/tiramisu/storage/cache.py similarity index 100% rename from tiramisu/storage/dictionary/cache.py rename to tiramisu/storage/cache.py diff --git a/tiramisu/storage/dictionary/setting.py b/tiramisu/storage/dictionary/setting.py index ea59513..706ab2a 100644 --- a/tiramisu/storage/dictionary/setting.py +++ b/tiramisu/storage/dictionary/setting.py @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from .cache import Cache +from ..cache import Cache class Settings(Cache): diff --git a/tiramisu/storage/dictionary/value.py b/tiramisu/storage/dictionary/value.py index ed1017e..c435d06 100644 --- a/tiramisu/storage/dictionary/value.py +++ b/tiramisu/storage/dictionary/value.py @@ -18,7 +18,7 @@ # # ____________________________________________________________ -from .cache import Cache +from ..cache import Cache class Values(Cache): diff --git a/tiramisu/storage/sqlite3/cache.py b/tiramisu/storage/sqlite3/cache.py deleted file mode 100644 index 183c4d0..0000000 --- a/tiramisu/storage/sqlite3/cache.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -"sqlite3 cache" -# Copyright (C) 2013 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 General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# ____________________________________________________________ -from pickle import dumps, loads - - -class Cache(object): - __slots__ = ('storage',) - key_is_path = True - - def __init__(self, cache_type, storage): - self.storage = storage - cache_table = 'CREATE TABLE IF NOT EXISTS cache_{0}(path '.format( - cache_type) - cache_table += 'text primary key, value text, time real)' - self.storage.execute(cache_table) - - # value - def _sqlite_decode_path(self, path): - if path == '_none': - return None - else: - return path - - def _sqlite_encode_path(self, path): - if path is None: - return '_none' - else: - return path - - def _sqlite_decode(self, value): - return loads(value) - - def _sqlite_encode(self, value): - if isinstance(value, list): - value = list(value) - return dumps(value) - - def setcache(self, cache_type, path, val, time): - convert_value = self._sqlite_encode(val) - path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM cache_{0} WHERE path = ?".format( - cache_type), (path,), False) - self.storage.execute("INSERT INTO cache_{0}(path, value, time) " - "VALUES (?, ?, ?)".format(cache_type), - (path, convert_value, time)) - - def getcache(self, cache_type, path, exp): - path = self._sqlite_encode_path(path) - cached = self.storage.select("SELECT value FROM cache_{0} WHERE " - "path = ? AND time >= ?".format( - cache_type), (path, exp)) - if cached is None: - return False, None - else: - return True, self._sqlite_decode(cached[0]) - - def hascache(self, cache_type, path): - path = self._sqlite_encode_path(path) - return self.storage.select("SELECT value FROM cache_{0} WHERE " - "path = ?".format(cache_type), - (path,)) is not None - - def reset_expired_cache(self, cache_type, exp): - self.storage.execute("DELETE FROM cache_{0} WHERE time < ?".format( - cache_type), (exp,)) - - def reset_all_cache(self, cache_type): - self.storage.execute("DELETE FROM cache_{0}".format(cache_type)) - - def get_cached(self, cache_type, context): - """return all values in a dictionary - example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} - """ - ret = {} - for path, value, time in self.storage.select("SELECT * FROM cache_{0}" - "".format(cache_type), - only_one=False): - path = self._sqlite_decode_path(path) - value = self._sqlite_decode(value) - ret[path] = (value, time) - return ret diff --git a/tiramisu/storage/sqlite3/setting.py b/tiramisu/storage/sqlite3/setting.py index 11506bb..720849b 100644 --- a/tiramisu/storage/sqlite3/setting.py +++ b/tiramisu/storage/sqlite3/setting.py @@ -17,10 +17,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from .cache import Cache +from .sqlite3db import Sqlite3DB -class Settings(Cache): +class Settings(Sqlite3DB): __slots__ = tuple() def __init__(self, storage): @@ -29,7 +29,7 @@ class Settings(Cache): permissives_table = 'CREATE TABLE IF NOT EXISTS permissive(path text ' permissives_table += 'primary key, permissives text)' # should init cache too - super(Settings, self).__init__('property', storage) + super(Settings, self).__init__(storage) self.storage.execute(settings_table, commit=False) self.storage.execute(permissives_table) diff --git a/tiramisu/storage/sqlite3/sqlite3db.py b/tiramisu/storage/sqlite3/sqlite3db.py new file mode 100644 index 0000000..9a967cd --- /dev/null +++ b/tiramisu/storage/sqlite3/sqlite3db.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"sqlite3 cache" +# Copyright (C) 2013 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 General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# ____________________________________________________________ +from cPickle import loads, dumps +from ..cache import Cache + + +class Sqlite3DB(Cache): + __slots__ = tuple() + def _sqlite_decode_path(self, path): + if path == '_none': + return None + else: + return path + + def _sqlite_encode_path(self, path): + if path is None: + return '_none' + else: + return path + + def _sqlite_decode(self, value): + return loads(value) + + def _sqlite_encode(self, value): + if isinstance(value, list): + value = list(value) + return dumps(value) diff --git a/tiramisu/storage/sqlite3/value.py b/tiramisu/storage/sqlite3/value.py index 687e7dd..3f76e2c 100644 --- a/tiramisu/storage/sqlite3/value.py +++ b/tiramisu/storage/sqlite3/value.py @@ -18,18 +18,18 @@ # # ____________________________________________________________ -from .cache import Cache +from .sqlite3db import Sqlite3DB from tiramisu.setting import owners -class Values(Cache): +class Values(Sqlite3DB): __slots__ = ('__weakref__',) def __init__(self, storage): """init plugin means create values storage """ # should init cache too - super(Values, self).__init__('value', storage) + super(Values, self).__init__(storage) values_table = 'CREATE TABLE IF NOT EXISTS value(path text primary ' values_table += 'key, value text, owner text)' self.storage.execute(values_table, commit=False) From 77c1ccf40bd8d611a0af997d8599115991d9bf70 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 7 Sep 2013 17:25:22 +0200 Subject: [PATCH 26/50] add 'cache' property --- test/test_cache.py | 215 ++++++++++++++++++++++-------------- test/test_option_setting.py | 9 +- test/test_storage.py | 13 ++- tiramisu/setting.py | 23 ++-- tiramisu/storage/cache.py | 17 ++- tiramisu/value.py | 31 +++--- 6 files changed, 187 insertions(+), 121 deletions(-) diff --git a/test/test_cache.py b/test/test_cache.py index 47270ee..b65e117 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -4,7 +4,7 @@ from tiramisu import setting setting.expires_time = 1 from tiramisu.option import IntOption, OptionDescription from tiramisu.config import Config -from time import sleep +from time import sleep, time def make_description(): @@ -20,13 +20,26 @@ def test_cache(): values = c.cfgimpl_get_values() settings = c.cfgimpl_get_settings() c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u2 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) - assert 'u2' in values._p_.get_cached('value', c) - assert 'u2' in settings._p_.get_cached('property', c) + 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) + + +def test_get_cache(): + # force a value in cache, try if reget corrupted value + od1 = make_description() + c = Config(od1) + values = c.cfgimpl_get_values() + settings = c.cfgimpl_get_settings() + ntime = time() + 1 + settings._p_.setcache('u1', set(['inject']), ntime) + assert 'inject' in settings[od1.u1] + values._p_.setcache('u1', 100, ntime) + assert c.u1 == [100] def test_cache_reset(): @@ -36,44 +49,44 @@ def test_cache_reset(): settings = c.cfgimpl_get_settings() #when change a value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u2 = 1 - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when remove a value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + 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('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when add/del property c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + 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('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + 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('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when enable/disabled property c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_get_settings().append('test') - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_get_settings().remove('test') - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) def test_cache_reset_multi(): @@ -83,32 +96,32 @@ def test_cache_reset_multi(): settings = c.cfgimpl_get_settings() #when change a value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u3 = [1] - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when append value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u3.append(1) - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when pop value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u3.pop(1) - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) #when remove a value c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + 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('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) def test_reset_cache(): @@ -117,23 +130,25 @@ def test_reset_cache(): values = c.cfgimpl_get_values() settings = c.cfgimpl_get_settings() c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_reset_cache() - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) + c.u1 + sleep(1) c.u1 sleep(1) c.u2 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) - assert 'u2' in values._p_.get_cached('value', c) - assert 'u2' in settings._p_.get_cached('property', c) + 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.cfgimpl_reset_cache() - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) - assert 'u2' not in values._p_.get_cached('value', c) - assert 'u2' not in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) + assert 'u2' not in values._p_.get_cached(c) + assert 'u2' not in settings._p_.get_cached(c) def test_reset_cache_subconfig(): @@ -142,9 +157,9 @@ def test_reset_cache_subconfig(): c = Config(od2) values = c.cfgimpl_get_values() c.od1.u1 - assert 'od1.u1' in values._p_.get_cached('value', c) + assert 'od1.u1' in values._p_.get_cached(c) c.od1.cfgimpl_reset_cache() - assert 'od1.u1' not in values._p_.get_cached('value', c) + assert 'od1.u1' not in values._p_.get_cached(c) def test_reset_cache_only_expired(): @@ -153,22 +168,60 @@ def test_reset_cache_only_expired(): values = c.cfgimpl_get_values() settings = c.cfgimpl_get_settings() c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_reset_cache(True) - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + sleep(1) + c.u1 sleep(1) c.u2 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) - assert 'u2' in values._p_.get_cached('value', c) - assert 'u2' in settings._p_.get_cached('property', c) + 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.cfgimpl_reset_cache(True) - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) - assert 'u2' in values._p_.get_cached('value', c) - assert 'u2' in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) + assert 'u2' in values._p_.get_cached(c) + assert 'u2' in settings._p_.get_cached(c) + + +def test_cache_not_expire(): + od1 = make_description() + c = Config(od1) + values = c.cfgimpl_get_values() + settings = c.cfgimpl_get_settings() + settings.remove('expire') + c.u1 + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + c.cfgimpl_reset_cache(True) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) + sleep(1) + 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.cfgimpl_reset_cache(True) + 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) + + +def test_cache_not_cache(): + od1 = make_description() + c = Config(od1) + values = c.cfgimpl_get_values() + settings = c.cfgimpl_get_settings() + settings.remove('cache') + c.u1 + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) def test_reset_cache_only(): @@ -177,14 +230,14 @@ def test_reset_cache_only(): values = c.cfgimpl_get_values() settings = c.cfgimpl_get_settings() c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_reset_cache(only=('values',)) - assert 'u1' not in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' not in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.u1 - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' in settings._p_.get_cached(c) c.cfgimpl_reset_cache(only=('settings',)) - assert 'u1' in values._p_.get_cached('value', c) - assert 'u1' not in settings._p_.get_cached('property', c) + assert 'u1' in values._p_.get_cached(c) + assert 'u1' not in settings._p_.get_cached(c) diff --git a/test/test_option_setting.py b/test/test_option_setting.py index a841b9b..2dc76ab 100644 --- a/test/test_option_setting.py +++ b/test/test_option_setting.py @@ -329,7 +329,7 @@ def test_reset_properties(): option = cfg.cfgimpl_get_description().gc.dummy assert setting._p_.get_properties(cfg) == {} setting.append('frozen') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator'))} + assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'cache', 'validator'))} setting.reset() assert setting._p_.get_properties(cfg) == {} setting[option].append('test') @@ -337,11 +337,11 @@ def test_reset_properties(): setting.reset() assert setting._p_.get_properties(cfg) == {'gc.dummy': set(('test',))} setting.append('frozen') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator')), 'gc.dummy': set(('test',))} + assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} setting.reset(option) - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator'))} + assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache'))} setting[option].append('test') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator')), 'gc.dummy': set(('test',))} + assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} setting.reset(all_properties=True) assert setting._p_.get_properties(cfg) == {} raises(ValueError, 'setting.reset(all_properties=True, opt=option)') @@ -350,7 +350,6 @@ def test_reset_properties(): setting[a].reset() - def test_reset_multiple(): descr = make_description() cfg = Config(descr) diff --git a/test/test_storage.py b/test/test_storage.py index c3acc70..0b2e3ac 100644 --- a/test/test_storage.py +++ b/test/test_storage.py @@ -6,9 +6,7 @@ from tiramisu.config import Config from tiramisu.option import BoolOption, OptionDescription from tiramisu.setting import owners from tiramisu.storage import list_sessions, delete_session -from tiramisu import setting -setting.expires_time = 0 def test_non_persistent(): b = BoolOption('b', '') @@ -20,6 +18,7 @@ def test_list(): b = BoolOption('b', '') o = OptionDescription('od', '', [b]) c = Config(o, session_id='test_non_persistent') + c.cfgimpl_get_settings().remove('cache') assert 'test_non_persistent' in list_sessions() del(c) assert 'test_non_persistent' not in list_sessions() @@ -66,6 +65,7 @@ def test_create_persistent_retrieve(): o = OptionDescription('od', '', [b]) try: c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') except ValueError: # storage is not persistent pass @@ -75,10 +75,12 @@ def test_create_persistent_retrieve(): assert c.b is True del(c) c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') assert c.b is True assert 'test_persistent' in list_sessions() delete_session('test_persistent') c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') assert c.b is None delete_session('test_persistent') @@ -88,11 +90,13 @@ def test_two_persistent(): o = OptionDescription('od', '', [b]) try: c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') except ValueError: # storage is not persistent pass else: c2 = Config(o, session_id='test_persistent', persistent=True) + c2.cfgimpl_get_settings().remove('cache') assert c.b is None assert c2.b is None c.b = False @@ -109,11 +113,13 @@ def test_two_persistent_owner(): o = OptionDescription('od', '', [b]) try: c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') except ValueError: # storage is not persistent pass else: c2 = Config(o, session_id='test_persistent', persistent=True) + c2.cfgimpl_get_settings().remove('cache') owners.addowner('persistent') assert c.getowner(b) == owners.default assert c2.getowner(b) == owners.default @@ -131,6 +137,7 @@ def test_two_persistent_information(): o = OptionDescription('od', '', [b]) try: c = Config(o, session_id='test_persistent', persistent=True) + c.cfgimpl_get_settings().remove('cache') except ValueError: # storage is not persistent pass @@ -138,5 +145,7 @@ def test_two_persistent_information(): c.impl_set_information('info', 'string') assert c.impl_get_information('info') == 'string' c2 = Config(o, session_id='test_persistent', persistent=True) + c2.cfgimpl_get_settings().remove('cache') + c2.cfgimpl_get_settings().remove('cache') assert c2.impl_get_information('info') == 'string' delete_session('test_persistent') diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 3c88c1c..7946212 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -35,7 +35,7 @@ ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen', 'mandatory']) rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) -default_properties = ('expire', 'validator') +default_properties = ('cache', 'expire', 'validator') class _NameSpace: @@ -242,18 +242,21 @@ class Settings(object): raise ValueError(_('if opt is not None, path should not be' ' None in _getproperties')) ntime = None - if self._p_.hascache('property', path): - ntime = time() - is_cached, props = self._p_.getcache('property', path, ntime) + if 'cache' in self and self._p_.hascache(path): + if 'expire' in self: + ntime = int(time()) + is_cached, props = self._p_.getcache(path, ntime) if is_cached: return props props = self._p_.getproperties(path, opt._properties) if is_apply_req: props |= self.apply_requires(opt, path) - if 'expire' in self: - if ntime is None: - ntime = time() - self._p_.setcache('property', path, props, ntime + expires_time) + if 'cache' in self: + if 'expire' in self: + if ntime is None: + ntime = int(time()) + ntime = ntime + expires_time + self._p_.setcache(path, props, ntime) return props def append(self, propname): @@ -387,9 +390,9 @@ class Settings(object): def reset_cache(self, only_expired): if only_expired: - self._p_.reset_expired_cache('property', time()) + self._p_.reset_expired_cache(int(time())) else: - self._p_.reset_all_cache('property') + self._p_.reset_all_cache() def apply_requires(self, opt, path): """carries out the jit (just in time) requirements between options diff --git a/tiramisu/storage/cache.py b/tiramisu/storage/cache.py index 664990d..98db066 100644 --- a/tiramisu/storage/cache.py +++ b/tiramisu/storage/cache.py @@ -27,34 +27,33 @@ class Cache(object): self._cache = {} self.storage = storage - def setcache(self, cache_type, path, val, time): + def setcache(self, path, val, time): self._cache[path] = (val, time) - def getcache(self, cache_type, path, exp): + def getcache(self, path, exp): value, created = self._cache[path] - if exp < created: + if exp <= created: return True, value return False, None - def hascache(self, cache_type, path): + def hascache(self, path): """ path is in the cache - :param cache_type: value | property :param path: the path's option """ return path in self._cache - def reset_expired_cache(self, cache_type, exp): + def reset_expired_cache(self, exp): for key in tuple(self._cache.keys()): val, created = self._cache[key] - if exp > created: + if created is not None and exp > created: del(self._cache[key]) - def reset_all_cache(self, cache_type): + def reset_all_cache(self): "empty the cache" self._cache.clear() - def get_cached(self, cache_type, context): + def get_cached(self, context): """return all values in a dictionary example: {'path1': ('value1', 'time1'), 'path2': ('value2', 'time2')} """ diff --git a/tiramisu/value.py b/tiramisu/value.py index f35c16d..ffd34d6 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -147,25 +147,28 @@ class Values(object): def getitem(self, opt, path=None, validate=True, force_permissive=False, force_properties=None, validate_properties=True): - ntime = None if path is None: path = self._get_opt_path(opt) - if self._p_.hascache('value', path): - ntime = time() - is_cached, value = self._p_.getcache('value', path, ntime) + ntime = None + setting = self.context().cfgimpl_get_settings() + if 'cache' in setting and self._p_.hascache(path): + if 'expire' in setting: + ntime = int(time()) + is_cached, value = self._p_.getcache(path, ntime) if is_cached: if opt.impl_is_multi() and not isinstance(value, Multi): #load value so don't need to validate if is not a Multi value = Multi(value, self.context, opt, path, validate=False) return value - val = self._getitem(opt, path, validate, force_permissive, force_properties, - validate_properties) - if 'expire' in self.context().cfgimpl_get_settings() and validate and \ - validate_properties and force_permissive is False and \ - force_properties is None: - if ntime is None: - ntime = time() - self._p_.setcache('value', path, val, ntime + expires_time) + val = self._getitem(opt, path, validate, force_permissive, + force_properties, validate_properties) + if 'cache' in setting and validate and validate_properties and \ + force_permissive is False and force_properties is None: + if 'expire' in setting: + if ntime is None: + ntime = int(time()) + ntime = ntime + expires_time + self._p_.setcache(path, val, ntime) return val @@ -300,9 +303,9 @@ class Values(object): clears the cache if necessary """ if only_expired: - self._p_.reset_expired_cache('value', time()) + self._p_.reset_expired_cache(int(time())) else: - self._p_.reset_all_cache('value') + self._p_.reset_all_cache() def _get_opt_path(self, opt): """ From 371f094dcbb8da76b5a2a1c902127a79ba3261c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 7 Sep 2013 21:47:17 +0200 Subject: [PATCH 27/50] comment tiramisu/setting.py --- tiramisu/setting.py | 189 +++++++++++++++++++++++++++++++++----------- 1 file changed, 142 insertions(+), 47 deletions(-) diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 7946212..1dba144 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -28,19 +28,88 @@ from tiramisu.error import (RequirementError, PropertiesOptionError, from tiramisu.i18n import _ +"Default encoding for display a Config if raise UnicodeEncodeError" default_encoding = 'utf-8' + +"""If cache and expire is enable, time before cache is expired. +This delay start first time value/setting is set in cache, even if +user access several time to value/setting +""" expires_time = 5 -ro_remove = set(['permissive', 'hidden']) -ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen', - 'mandatory']) -rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) -rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) +"""List of default properties (you can add new one if needed). + +For common properties and personalise properties, if a propery is set for +an Option and for the Config together, Setting raise a PropertiesOptionError + +* Common properties: + +hidden + option with this property can only get value in read only mode. This + option is not available in read write mode. + +disabled + option with this property cannot be set/get + +frozen + cannot set value for option with this properties if 'frozen' is set in + config + +mandatory + should set value for option with this properties if 'mandatory' is set in + config + + +* Special property: + +permissive + option with 'permissive' cannot raise PropertiesOptionError for properties + set in permissive + config with 'permissive', whole option in this config cannot raise + PropertiesOptionError for properties set in permissive + +* Special Config properties: + +cache + if set, enable cache settings and values + +expire + if set, settings and values in cache expire after ``expires_time`` + +everything_frozen + whole option in config are frozen (even if option have not frozen + property) + +validator + launch validator set by user in option (this property has no effect + for internal validator) +""" default_properties = ('cache', 'expire', 'validator') +"""Config can be in two defaut mode: +read_only + you can get all variables not disabled but you cannot set any variables + if a value has a callback without any value, callback is launch and value + of this variable can change + you cannot access to mandatory variable without values + +read_write + you can get all variables not disabled and not hidden + you can set all variables not frozen +""" +ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen', + 'mandatory']) +ro_remove = set(['permissive', 'hidden']) +rw_append = set(['frozen', 'disabled', 'validator', 'hidden']) +rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) + + +# ____________________________________________________________ class _NameSpace: """convenient class that emulates a module - and builds constants (that is, unique names)""" + and builds constants (that is, unique names) + when attribute is added, we cannot delete it + """ def __setattr__(self, name, value): if name in self.__dict__: @@ -53,7 +122,6 @@ class _NameSpace: raise ValueError(name) -# ____________________________________________________________ class GroupModule(_NameSpace): "emulates a module to manage unique group (OptionDescription) names" class GroupType(str): @@ -71,21 +139,8 @@ class GroupModule(_NameSpace): *master* means : groups that have the 'master' attribute set """ pass -# setting.groups (emulates a module) -groups = GroupModule() -def populate_groups(): - "populates the available groups in the appropriate namespaces" - groups.master = groups.MasterGroupType('master') - groups.default = groups.DefaultGroupType('default') - groups.family = groups.GroupType('family') - -# names are in the module now -populate_groups() - - -# ____________________________________________________________ class OwnerModule(_NameSpace): """emulates a module to manage unique owner names. @@ -99,28 +154,6 @@ class OwnerModule(_NameSpace): class DefaultOwner(Owner): """groups that are default (typically 'default')""" pass -# setting.owners (emulates a module) -owners = OwnerModule() - - -def populate_owners(): - """populates the available owners in the appropriate namespaces - - - 'user' is the generic is the generic owner. - - 'default' is the config owner after init time - """ - setattr(owners, 'default', owners.DefaultOwner('default')) - setattr(owners, 'user', owners.Owner('user')) - - def addowner(name): - """ - :param name: the name of the new owner - """ - setattr(owners, name, owners.Owner(name)) - setattr(owners, 'addowner', addowner) - -# names are in the module now -populate_owners() class MultiTypeModule(_NameSpace): @@ -137,18 +170,79 @@ class MultiTypeModule(_NameSpace): class SlaveMultiType(MultiType): pass -multitypes = MultiTypeModule() + +# ____________________________________________________________ +def populate_groups(): + """populates the available groups in the appropriate namespaces + + groups.default + default group set when creating a new optiondescription + + groups.master + master group is a special optiondescription, all suboptions should be + multi option and all values should have same length, to find master's + option, the optiondescription's name should be same than de master's + option + + groups.family + example of group, no special behavior with this group's type + """ + groups.default = groups.DefaultGroupType('default') + groups.master = groups.MasterGroupType('master') + groups.family = groups.GroupType('family') + + +def populate_owners(): + """populates the available owners in the appropriate namespaces + + default + is the config owner after init time + + user + is the generic is the generic owner + """ + setattr(owners, 'default', owners.DefaultOwner('default')) + setattr(owners, 'user', owners.Owner('user')) + + def addowner(name): + """ + :param name: the name of the new owner + """ + setattr(owners, name, owners.Owner(name)) + setattr(owners, 'addowner', addowner) def populate_multitypes(): - "populates the master/slave namespace" + """all multi option should have a type, this type is automaticly set do + not touch this + + default + default's multi option set if not master or slave + + master + master's option in a group with master's type, name of this option + should be the same name of the optiondescription + + slave + slave's option in a group with master's type + + """ setattr(multitypes, 'default', multitypes.DefaultMultiType('default')) setattr(multitypes, 'master', multitypes.MasterMultiType('master')) setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave')) + +# ____________________________________________________________ +# populate groups, owners and multitypes with default attributes +groups = GroupModule() +populate_groups() +owners = OwnerModule() +populate_owners() +multitypes = MultiTypeModule() populate_multitypes() +# ____________________________________________________________ class Property(object): "a property is responsible of the option's value access rules" __slots__ = ('_setting', '_properties', '_opt', '_path') @@ -171,7 +265,8 @@ class Property(object): def remove(self, propname): if propname in self._properties: self._properties.remove(propname) - self._setting._setproperties(self._properties, self._opt, self._path) + self._setting._setproperties(self._properties, self._opt, + self._path) def reset(self): self._setting.reset(_path=self._path) @@ -381,11 +476,11 @@ class Settings(object): self.append(prop) def read_only(self): - "convenience method to freeze, hidde and disable" + "convenience method to freeze, hide and disable" self._read(ro_remove, ro_append) def read_write(self): - "convenience method to freeze, hidde and disable" + "convenience method to freeze, hide and disable" self._read(rw_remove, rw_append) def reset_cache(self, only_expired): From 632de1cffb74959ce57146fd2e9e3d2205798c24 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 7 Sep 2013 22:16:50 +0200 Subject: [PATCH 28/50] comment tiramisu/setting.py --- tiramisu/setting.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 1dba144..249af37 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -308,7 +308,7 @@ class Settings(object): return str(list(self._getproperties())) def __getitem__(self, opt): - path = self._get_opt_path(opt) + path = self._get_path_by_opt(opt) return self._getitem(opt, path) def _getitem(self, opt, path): @@ -325,7 +325,7 @@ class Settings(object): self._p_.reset_all_propertives() else: if opt is not None and _path is None: - _path = self._get_opt_path(opt) + _path = self._get_path_by_opt(opt) self._p_.reset_properties(_path) self.context().cfgimpl_reset_cache() @@ -453,7 +453,7 @@ class Settings(object): instead of passing a :class:`tiramisu.option.Option()` object. """ if opt is not None and path is None: - path = self._get_opt_path(opt) + path = self._get_path_by_opt(opt) if not isinstance(permissive, tuple): raise TypeError(_('permissive must be a tuple')) self._p_.setpermissive(path, permissive) @@ -484,6 +484,11 @@ class Settings(object): 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: @@ -499,12 +504,13 @@ class Settings(object): let's have a look at all the tuple's items: - - **option** is the target option's name or path + - **option** is the target option's - - **expected** is the target option's value that is going to trigger an action + - **expected** is the target option's value that is going to trigger + an action - - **action** is the (property) action to be accomplished if the target option - happens to have the expected value + - **action** is the (property) action to be accomplished if the target + option happens to have the expected value - if **inverse** is `True` and if the target option's value does not apply, then the property action must be removed from the option's @@ -541,7 +547,7 @@ class Settings(object): for require in requires: option, expected, action, inverse, \ transitive, same_action = require - reqpath = self._get_opt_path(option) + reqpath = self._get_path_by_opt(option) if reqpath == path or reqpath.startswith(path + '.'): raise RequirementError(_("malformed requirements " "imbrication detected for option:" @@ -572,5 +578,10 @@ class Settings(object): break return calc_properties - def _get_opt_path(self, opt): + def _get_path_by_opt(self, opt): + """just a wrapper to get path in optiondescription's cache + + :param opt: `Option`'s object + :returns: path + """ return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt) From 3dc72c505c45a2a90e9aeed946dc6312564a2787 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 7 Sep 2013 22:37:13 +0200 Subject: [PATCH 29/50] support no expire in getcache --- test/test_cache.py | 12 ++++++++++++ tiramisu/storage/cache.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/test_cache.py b/test/test_cache.py index b65e117..f483c76 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -42,6 +42,18 @@ def test_get_cache(): assert c.u1 == [100] +def test_get_cache_no_expire(): + # force a value in cache, try if reget corrupted value + od1 = make_description() + c = Config(od1) + values = c.cfgimpl_get_values() + settings = c.cfgimpl_get_settings() + settings._p_.setcache('u1', set(['inject2']), None) + assert 'inject2' in settings[od1.u1] + values._p_.setcache('u1', 200, None) + assert c.u1 == [200] + + def test_cache_reset(): od1 = make_description() c = Config(od1) diff --git a/tiramisu/storage/cache.py b/tiramisu/storage/cache.py index 98db066..347d270 100644 --- a/tiramisu/storage/cache.py +++ b/tiramisu/storage/cache.py @@ -32,7 +32,7 @@ class Cache(object): def getcache(self, path, exp): value, created = self._cache[path] - if exp <= created: + if created is None or exp <= created: return True, value return False, None From b070fa8a83f20a2ede8bb2302d1c50c1b4dba258 Mon Sep 17 00:00:00 2001 From: Daniel Dehennin Date: Fri, 6 Sep 2013 16:17:53 +0200 Subject: [PATCH 30/50] Storages are not installed * setup.py: Add storages to packages. --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f320843..2e3a3df 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,9 @@ from distutils.core import setup +PACKAGES = ['tiramisu', 'tiramisu.storage', 'tiramisu.storage.dictionary', + 'tiramisu.storage.sqlite3'] + def fetch_version(): """Get version from version.in""" return file('VERSION', 'r').readline().strip() @@ -15,7 +18,7 @@ setup( version=fetch_version(), description='an options controller tool', url='http://tiramisu.labs.libre-entreprise.org/', - packages=['tiramisu'], + packages=PACKAGES, classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", From 28ea4f0e9006278c66ba211459c2923a4453e367 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Tue, 10 Sep 2013 21:04:12 +0200 Subject: [PATCH 31/50] update doc --- doc/api.txt | 1 + doc/consistency.txt | 23 +- doc/index.txt | 1 + doc/storage.png | Bin 0 -> 15867 bytes doc/storage.svg | 265 ++++++++++++++++++++++++ doc/storage.txt | 52 +++++ tiramisu/storage/__init__.py | 10 +- tiramisu/storage/dictionary/__init__.py | 13 +- tiramisu/storage/dictionary/storage.py | 3 +- tiramisu/storage/sqlite3/__init__.py | 11 +- tiramisu/storage/sqlite3/storage.py | 3 + 11 files changed, 355 insertions(+), 27 deletions(-) create mode 100644 doc/storage.png create mode 100644 doc/storage.svg create mode 100644 doc/storage.txt diff --git a/doc/api.txt b/doc/api.txt index 1498f14..7fb526f 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -11,4 +11,5 @@ Auto generated library's API tiramisu.value tiramisu.autolib tiramisu.error + tiramisu.storage diff --git a/doc/consistency.txt b/doc/consistency.txt index 4b268ed..9de8748 100644 --- a/doc/consistency.txt +++ b/doc/consistency.txt @@ -57,7 +57,7 @@ A requirement is a list of dictionaries that have fairly this form:: 'transitive':True, 'same_action': True}] Actually a transformation is made to this dictionary during the validation of -this requires at the :class:`~option.Option()`'s init. The dictionairy becomes +this requires at the :class:`~option.Option()`'s init. The dictionary becomes a tuple, wich is passed to the :meth:`~setting.Settings.apply_requires()` method. Take a look at the code to fully understand the exact meaning of the requirements: @@ -103,7 +103,6 @@ hidden any more:: ['hidden'] >>> print c.od1.var1 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: var1 with properties ['hidden'] >>> c.od1.var2 = u'oui' @@ -123,7 +122,6 @@ document.):: >>> c.od1.var2 = u'non' >>> print c.od2.var4 Traceback (most recent call last): - [...] tiramisu.error.PropertiesOptionError: trying to access to an option named: od2 with properties ['hidden'] >>> c.od1.var2 = u'oui' >>> print c.od2.var4 @@ -144,21 +142,20 @@ Requirements can be accumulated for different or identical properties (inverted or not):: >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, - 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', - 'action':'hidden'}]) + ... 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', + ... 'action':'hidden'}]) >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, - 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'excepted':'oui', - 'action':'disabled', 'inverse':True}]) + ... 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'excepted':'oui', + ... 'action':'disabled', 'inverse':True}]) But it is not possible to have inverted requirements on the same property. Here is an impossible situation:: >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, - 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', - 'hidden', True}]) + ... 'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', + ... 'hidden', True}]) Traceback (most recent call last): - [...] ValueError: inconsistency in action types for option: var3 action: hidden Validation upon a whole configuration object @@ -184,11 +181,8 @@ Let's define validator (wich is a normal python function):: Here is an option wich uses this validator:: >>> var1 = UnicodeOption('var1', '', u'oui', validator=valid_a, validator_args={'letter': 'o'}) - >>> >>> od1 = OptionDescription('od1', '', [var1]) - >>> >>> rootod = OptionDescription('rootod', '', [od1]) - >>> >>> c = Config(rootod) >>> c.read_write() @@ -196,11 +190,10 @@ The validation is applied at the modification time:: >>> c.od1.var1 = u'non' Traceback (most recent call last): - [...] ValueError: invalid value non for option var1 >>> c.od1.var1 = u'oh non' - Il est possible de désactiver la validation : +You can disabled this validation:: >>> c.cfgimpl_get_settings().remove('validator') >>> c.od1.var1 = u'non' diff --git a/doc/index.txt b/doc/index.txt index 21cf692..c073d99 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -33,6 +33,7 @@ controlling options explanations getting-started config + storage option status consistency diff --git a/doc/storage.png b/doc/storage.png new file mode 100644 index 0000000000000000000000000000000000000000..9bef2b37a157f83657bad9613ee7f9f8df0dd38a GIT binary patch literal 15867 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 literal 0 HcmV?d00001 diff --git a/doc/storage.svg b/doc/storage.svg new file mode 100644 index 0000000..d710cbc --- /dev/null +++ b/doc/storage.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + Config + + + + + Values + + + + Settings + + + + + + + + + + + + Storage + + + Option + + + + diff --git a/doc/storage.txt b/doc/storage.txt new file mode 100644 index 0000000..8ff5d93 --- /dev/null +++ b/doc/storage.txt @@ -0,0 +1,52 @@ +Storage +======= + +Config's informations are, by default, volatiles. This means, all values and +settings changes will be lost. + +The storage is the system Tiramisu uses to communicate with various DB. +You can specified a persistent storage. + +.. image:: storage.png + +.. automodule:: tiramisu.storage + +.. automethod:: tiramisu.storage.set_storage + +Dictionary +~~~~~~~~~~ + +.. automodule:: tiramisu.storage.dictionary + +Dictionary settings: + +.. automethod:: tiramisu.storage.dictionary.storage.Setting + +Sqlite3 +~~~~~~~ + +.. automodule:: tiramisu.storage.sqlite3 + +Sqlite3 settings: + +.. automethod:: tiramisu.storage.sqlite3.storage.Setting + +Example +~~~~~~~ + +>>> from tiramisu.option import StrOption, OptionDescription +>>> from tiramisu.config import Config +>>> from tiramisu.storage import set_storage +>>> set_storage('sqlite3', dir_database='/tmp/tiramisu') +>>> s = StrOption('str', '') +>>> o = OptionDescription('od', '', [s]) +>>> c1 = Config(o, persistent=True, session_id='xxxx') +>>> c1.str +>>> c1.str = 'yes' +>>> c1.str +'yes' +>>> del(c1) +>>> c2 = Config(o, persistent=True, session_id='xxxx') +>>> c2.str +'yes' + diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index 562aad5..147d8a8 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -19,11 +19,9 @@ # the whole pypy projet is under MIT licence # ____________________________________________________________ -"""Storage connections, executions and managements. - -Storage is basic components used to set Config informations in DB. -The primary "entry point" class into this package is the StorageType and it's -public configurator ``set_storage()``. +"""Storage is basic components used to set Config informations in DB. +The primary "entry point" class is the StorageType and it's public +configurator ``set_storage()``. """ @@ -43,6 +41,8 @@ class StorageType(object): def set(self, name): if self.storage_type is not None: + if self.storage_type == name: + return raise ConfigError(_('storage_type is already set, cannot rebind it')) self.storage_type = name diff --git a/tiramisu/storage/dictionary/__init__.py b/tiramisu/storage/dictionary/__init__.py index a53e505..dadce23 100644 --- a/tiramisu/storage/dictionary/__init__.py +++ b/tiramisu/storage/dictionary/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"default plugin for storage: set it in a simple dictionary" # Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) # # This program is free software; you can redistribute it and/or modify @@ -17,8 +16,16 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ +"""Default plugin for storage. All informations are store in a simple +dictionary in memory. + +You cannot have persistente informations with this kind of storage. + +The advantage of this solution is that you can easily create a Config and +use it. But if something goes wrong, you will lost your modifications. +""" from .value import Values from .setting import Settings -from .storage import Storage, list_sessions, delete_session +from .storage import Setting, Storage, list_sessions, delete_session -__all__ = (Values, Settings, Storage, list_sessions, delete_session) +__all__ = (Setting, Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/dictionary/storage.py b/tiramisu/storage/dictionary/storage.py index 5442c5d..6e15c1b 100644 --- a/tiramisu/storage/dictionary/storage.py +++ b/tiramisu/storage/dictionary/storage.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"default plugin for storage: set it in a simple dictionary" # Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) # # This program is free software; you can redistribute it and/or modify @@ -22,6 +21,8 @@ from tiramisu.error import ConfigError class Setting(object): + """Dictionary storage has no particular setting. + """ pass diff --git a/tiramisu/storage/sqlite3/__init__.py b/tiramisu/storage/sqlite3/__init__.py index adcdfdc..dc6c14b 100644 --- a/tiramisu/storage/sqlite3/__init__.py +++ b/tiramisu/storage/sqlite3/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"set storage in sqlite3" # Copyright (C) 2013 Team tiramisu (see AUTHORS for all contributors) # # This program is free software; you can redistribute it and/or modify @@ -17,8 +16,14 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ +"""Sqlite3 plugin for storage. This storage is not made to be used in productive +environment. It was developing as proof of concept. + +You should not configure differents Configs with same session_id. + +""" from .value import Values from .setting import Settings -from .storage import Storage, list_sessions, delete_session +from .storage import Setting, Storage, list_sessions, delete_session -__all__ = (Values, Settings, Storage, list_sessions, delete_session) +__all__ = (Setting, Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/sqlite3/storage.py b/tiramisu/storage/sqlite3/storage.py index ed5abd9..2ab8e08 100644 --- a/tiramisu/storage/sqlite3/storage.py +++ b/tiramisu/storage/sqlite3/storage.py @@ -25,6 +25,9 @@ from glob import glob class Setting(object): + """:param extension: database file extension (by default: db) + :param dir_database: root database directory (by default: /tmp) + """ extension = 'db' dir_database = '/tmp' From b492874cbe0d9e6033252498a0381c2d434f205c Mon Sep 17 00:00:00 2001 From: gwen Date: Thu, 29 Aug 2013 16:38:23 +0200 Subject: [PATCH 32/50] version for setup.py --- setup.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f320843..1b73eea 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,27 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from distutils.core import setup +from os.path import dirname, abspath, join, normpath, isdir, basename +from os import listdir def fetch_version(): """Get version from version.in""" return file('VERSION', 'r').readline().strip() +def return_storages(): + "returns all the storage plugins that are living in tiramisu/storage" + here = dirname(abspath(__file__)) + storages_path = normpath(join(here, 'tiramisu', 'storage')) + dir_content = [ content for content in listdir(storages_path) \ + if not content =='__pycache__'] + storages = filter(isdir, [join(storages_path, content) \ + for content in dir_content]) + storage_list = [basename(storage) for storage in storages] + return storage_list + +packages = ['tiramisu', 'tiramisu.storage'] +packages.extend(return_storages()) setup( author="Tiramisu's team", @@ -15,7 +30,6 @@ setup( version=fetch_version(), description='an options controller tool', url='http://tiramisu.labs.libre-entreprise.org/', - packages=['tiramisu'], classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", @@ -45,4 +59,5 @@ producing flexible and fast options access. This version requires Python 2.6 or later. """ + packages=packages ) From abbb7a274eba723f51a3c20f6e0c55ceacc66de3 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 14 Sep 2013 14:44:33 +0200 Subject: [PATCH 33/50] update doc --- doc/config.txt | 80 +++++++++++++++++++++--------------- doc/index.txt | 2 +- doc/option.txt | 53 +++++++++++++++++++++--- doc/storage.txt | 16 ++++---- tiramisu/option.py | 5 ++- tiramisu/storage/__init__.py | 8 +++- 6 files changed, 114 insertions(+), 50 deletions(-) diff --git a/doc/config.txt b/doc/config.txt index 2118f4c..3c6d9bc 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -6,15 +6,17 @@ Options handling basics Tiramisu is made of almost three main objects : -- :class:`tiramisu.config.Config` which is the whole configuration entry point - :class:`tiramisu.option.Option` stands for the option types - :class:`tiramisu.option.OptionDescription` is the shema, the option's structure +- :class:`tiramisu.config.Config` which is the whole configuration entry point + +.. image:: config.png Accessing the `Option`'s ------------------------- The :class:`~tiramisu.config.Config` object attribute access notation stands for -the value of the configuration's :class:`~tiramisu.option.Option`. That is, the +the value of the configuration's :class:`~tiramisu.option.Option`. :class:`~tiramisu.config.Config`'s object attribute is the name of the option, and the value is the value accessed by the `__getattr__` attribute access mechanism. @@ -49,7 +51,7 @@ are organized into a tree into nested as does every option group. The parts of the full name of the option are separated by dots: e.g. ``cfg.optgroup.optname``. -Let's make the protocol of accessing a config's attribute explicit +Let's make the protocol of accessing a `Config`'s attribute explicit (because explicit is better than implicit): 1. If the option has not been declared, an `AttributeError` is raised, @@ -67,11 +69,11 @@ But there are special exceptions. We will see later on that an option can be a :term:`mandatory option`. A mandatory option is an option that must have a value defined. -Setting the values of the options ----------------------------------------- +Setting the value of an option +------------------------------ -An important part of the setting of the configuration consists of setting the -values of the configuration options. There are different ways of setting values, +An important part of the setting's configuration consists of setting the +value's option. There are different ways of setting values, the first one is of course the `__setattr__` method :: @@ -103,10 +105,10 @@ Let's perform some common manipulation on some options >>> from tiramisu.config import Config >>> from tiramisu.option import UnicodeOption, OptionDescription ->>> +>>> # >>> var1 = UnicodeOption('var1', 'first variable') >>> var2 = UnicodeOption('var2', '', u'value') ->>> +>>> # >>> od1 = OptionDescription('od1', 'first OD', [var1, var2]) >>> rootod = OptionDescription('rootod', '', [od1]) @@ -127,10 +129,11 @@ None >>> print c.od1.var2 value -let's modify a value (careful to the value's type...) +let's modify a value (be careful to the value's type...) >>> c.od1.var1 = 'value' -Traceback (most recent call last): ValueError: invalid value value for option var1 +Traceback (most recent call last): +ValueError: invalid value value for option var1 >>> c.od1.var1 = u'value' >>> print c.od1.var1 value @@ -153,7 +156,8 @@ On the other side, in the `read_only` mode, it is not possible to modify the val >>> c.read_only() >>> c.od1.var2 = u'value2' Traceback (most recent call last): -tiramisu.error.PropertiesOptionError: cannot change the value to var2 for option ['frozen'] this option is frozen +tiramisu.error.PropertiesOptionError: cannot change the value for option var2 this option is frozen + let's retrieve the option `var1` description @@ -184,7 +188,7 @@ That's why a tree of options can easily be searched. First, let's build such a t >>> c = Config(rootod) >>> c.read_write() -Second, let's find an option by his name:: +Second, let's find an option by it's name:: >>> print c.find(byname='var1') [, @@ -232,7 +236,7 @@ If the organisation in a tree is not important, {'var5': None, 'var4': None, 'var6': None, 'var1': u'value', 'var3': None, 'var2': None} -.. note:: carefull with this `flatten` parameter, here we have just lost +.. note:: be carefull with this `flatten` parameter, here we have just lost two options named `var1` One can export only interesting parts of a tree of options into a dict, for @@ -249,7 +253,7 @@ and of course, :meth:`~config.SubConfig.make_dict()` can be called in a subtree: >>> print c.od1.make_dict(withoption='var1') {'var1': None, 'var3': None, 'var2': None} -the owners +The owners ~~~~~~~~~~~ .. glossary:: @@ -267,24 +271,36 @@ the owners Then let's retrieve the owner associated to an option:: - >>> print c.getowner('var1') - default - >>> c.od1.var1 = u'non' - >>> print c.getowner('var1') - user - >>> del(c.var1) - >>> print c.getowner('var1') - default - -the properties -~~~~~~~~~~~~~~~~ + >>> print c.getowner(var1) + default + >>> c.od1.var1 = u'no' + >>> print c.getowner(var1) + user + >>> del(c.var1) + >>> print c.getowner(var1) + default + +You can create your own owner, for example to distinguish modification made by +one user to an other one's. + + >>> from tiramisu.setting import owners + >>> owners.addowner('toto') + >>> c.cfgimpl_get_settings().setowner(owners.toto) + >>> print c.getowner(var1) + default + >>> c.od1.var1 = u'no' + >>> print c.getowner(var1) + toto + +The properties +~~~~~~~~~~~~~~ A property is an information on an option's state. Let's create options with properties:: >>> var1 = UnicodeOption('var1', '', u'value', properties=('hidden',)) >>> var2 = UnicodeOption('var2', '', properties=('mandatory',)) - >>> var3 = UnicodeOption('var3', '', u'value', properties=('frozen', 'inconnu')) + >>> var3 = UnicodeOption('var3', '', u'value', properties=('frozen', 'unknown')) >>> var4 = UnicodeOption('var4', '', u'value') >>> od1 = OptionDescription('od1', '', [var1, var2, var3]) >>> od2 = OptionDescription('od2', '', [var4], properties=('hidden',)) @@ -338,17 +354,17 @@ Let's try to modify a frozen option:: Tiramisu allows us to use user defined properties. Let's define and use one in read/write or read only mode:: - >>> c.cfgimpl_get_settings().append('inconnu') + >>> c.cfgimpl_get_settings().append('unknown') >>> print c.od1.var3 Traceback (most recent call last): tiramisu.error.PropertiesOptionError: trying to access to an option named: - var3 with properties ['inconnu'] - >>> c.cfgimpl_get_settings().remove('inconnu') + var3 with properties ['unknown'] + >>> c.cfgimpl_get_settings().remove('unknown') >>> print c.od1.var3 value -Properties can also be defined on an option group, (that is, on an -:term:`option description`), let's hide a group and try to access to it:: +Properties can also be defined on an option group (that is, on an +:term:`option description`) let's hide a group and try to access to it:: >>> c.read_write() >>> print c.od2.var4 diff --git a/doc/index.txt b/doc/index.txt index c073d99..1a55ca4 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -33,8 +33,8 @@ controlling options explanations getting-started config - storage option + storage status consistency error diff --git a/doc/option.txt b/doc/option.txt index 90dea0a..47053b8 100644 --- a/doc/option.txt +++ b/doc/option.txt @@ -9,7 +9,7 @@ Description of Options ---------------------- All the constructors take a ``name`` and a ``doc`` argument as first -arguments to give the option or option group a name and to document it. +arguments to give to the option or option description a name and a description document. Most constructors take a ``default`` argument that specifies the default value of the option. If this argument is not supplied the default value is assumed to be ``None``. @@ -17,7 +17,7 @@ is assumed to be ``None``. The `Option` base class ------------------------- -It's the abstract base class for almost all options (except the symblink). +It's the abstract base class for almost all options (except the symlink). .. _optioninit: @@ -28,22 +28,41 @@ It's the abstract base class for almost all options (except the symblink). All option types ------------------ +BoolOption +~~~~~~~~~~ + .. autoclass:: BoolOption :private-members: +IntOption +~~~~~~~~~ + .. autoclass:: IntOption :private-members: +FloatOption +~~~~~~~~~~~ + .. autoclass:: FloatOption :private-members: +StrOption +~~~~~~~~~ + .. autoclass:: StrOption :private-members: +UnicodeOption +~~~~~~~~~~~~~ + +.. autoclass:: UnicodeOption + :private-members: + +SymLinkOption +~~~~~~~~~~~~~ .. autoclass:: SymLinkOption - - .. automethod:: __init__ + :private-members: ``SymLinkOption`` redirects to another configuration option in the @@ -52,19 +71,41 @@ configuration, that is : - retrieves the value of the target, - can set the value of the target too +IPOption +~~~~~~~~ .. autoclass:: IPOption + :private-members: + +PortOption +~~~~~~~~~~ + +.. autoclass:: PortOption + :private-members: + +NetmaskOption +~~~~~~~~~~~~~ .. autoclass:: NetmaskOption + :private-members: + +NetworkOption +~~~~~~~~~~~~~ .. autoclass:: NetworkOption + :private-members: + +DomainnameOption +~~~~~~~~~~~~~~~~ .. autoclass:: DomainnameOption + :private-members: +ChoiceOption +~~~~~~~~~~~~ .. autoclass:: ChoiceOption - - .. automethod:: __init__ + :private-members: .. _optdescr: diff --git a/doc/storage.txt b/doc/storage.txt index 8ff5d93..6bfb18c 100644 --- a/doc/storage.txt +++ b/doc/storage.txt @@ -1,18 +1,12 @@ Storage ======= -Config's informations are, by default, volatiles. This means, all values and -settings changes will be lost. - -The storage is the system Tiramisu uses to communicate with various DB. -You can specified a persistent storage. - -.. image:: storage.png - .. automodule:: tiramisu.storage .. automethod:: tiramisu.storage.set_storage +.. image:: storage.png + Dictionary ~~~~~~~~~~ @@ -49,4 +43,10 @@ Example >>> c2 = Config(o, persistent=True, session_id='xxxx') >>> c2.str 'yes' +>>> del(c2) +>>> list_sessions() +['xxxx'] +>>> delete_session('xxxx') +>>> c3 = Config(o, persistent=True, session_id='xxxx') +>>> c3.str diff --git a/tiramisu/option.py b/tiramisu/option.py index 6d7f3b3..313299d 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -319,7 +319,7 @@ class Option(BaseOption): """ Abstract base class for configuration option's. - Reminder: an Option object is **not** a container for the value + Reminder: an Option object is **not** a container for the value. """ __slots__ = ('_multi', '_validator', '_default_multi', '_default', '_callback', '_multitype', '_master_slaves', '__weakref__') @@ -342,9 +342,10 @@ class Option(BaseOption): :param callback: the name of a function. If set, the function's output is responsible of the option's value :param callback_params: the callback's parameter - :param validator: the name of a function wich stands for a custom + :param validator: the name of a function which stands for a custom validation of the value :param validator_args: the validator's parameters + :param properties: tuple of default properties """ super(Option, self).__init__(name, doc, requires, properties) diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index 147d8a8..1394258 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -19,7 +19,13 @@ # the whole pypy projet is under MIT licence # ____________________________________________________________ -"""Storage is basic components used to set Config informations in DB. +"""Config's informations are, by default, volatiles. This means, all values and +settings changes will be lost. + +The storage is the system Tiramisu uses to communicate with various DB. +You can specified a persistent storage. + +Storage is basic components used to set Config informations in DB. The primary "entry point" class is the StorageType and it's public configurator ``set_storage()``. """ From e929745ebf91987a7f3e79f7a1e79b20f905ec6e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 16 Sep 2013 14:02:55 +0200 Subject: [PATCH 34/50] corrections in setup.py --- setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 1b73eea..1e67a75 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from distutils.core import setup -from os.path import dirname, abspath, join, normpath, isdir, basename +from os.path import dirname, abspath, join, normpath, isdir from os import listdir @@ -9,15 +9,16 @@ def fetch_version(): """Get version from version.in""" return file('VERSION', 'r').readline().strip() + def return_storages(): "returns all the storage plugins that are living in tiramisu/storage" here = dirname(abspath(__file__)) storages_path = normpath(join(here, 'tiramisu', 'storage')) - dir_content = [ content for content in listdir(storages_path) \ - if not content =='__pycache__'] - storages = filter(isdir, [join(storages_path, content) \ + dir_content = [content for content in listdir(storages_path) + if not content == '__pycache__'] + storages = filter(isdir, [join(storages_path, content) for content in dir_content]) - storage_list = [basename(storage) for storage in storages] + storage_list = ['.'.join(storage.split('/')[-3:]) for storage in storages] return storage_list packages = ['tiramisu', 'tiramisu.storage'] @@ -58,6 +59,6 @@ producing flexible and fast options access. This version requires Python 2.6 or later. -""" +""", packages=packages ) From 1249e1e2bc72854d455bc181a740ec5141149bef Mon Sep 17 00:00:00 2001 From: Daniel Dehennin Date: Mon, 16 Sep 2013 14:41:12 +0200 Subject: [PATCH 35/50] Incorrect filename for gettext catablog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Makefile: Do not use basename option “-s” as it's not compatible with, at least, Ubuntu Precise Pangolin. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ca436a5..8579680 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ endef define build_translation if [ -d ${1} ]; then \ for f in `find ${1} -name "*.po"`; do \ - msgfmt -o `dirname $$f`/`basename -s ".po" $$f`.mo $$f || true; \ + msgfmt -o `dirname $$f`/`basename $$f .po`.mo $$f || true; \ done; \ fi endef From 3ffbb4ab8fb06e597521a5be0a493c895f46e6ef Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 16 Sep 2013 14:43:58 +0200 Subject: [PATCH 36/50] Makefile for non-gnu version of basename --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ca436a5..9ead4ab 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ endef define build_translation if [ -d ${1} ]; then \ for f in `find ${1} -name "*.po"`; do \ - msgfmt -o `dirname $$f`/`basename -s ".po" $$f`.mo $$f || true; \ + msgfmt -o `dirname $$f`/`basename $$f ".po"`.mo $$f || true; \ done; \ fi endef From 9ddf100118d913c2f4225cca1adb65317b512d84 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 16 Sep 2013 15:02:14 +0200 Subject: [PATCH 37/50] when we get an option's value, we need it's values to calculate properties (ie for mandatory's option) if a disabled option has a callback to an other disabled value, it's raise ConfigError now only raise if option has no other propertiesError --- test/test_option_calculation.py | 11 +++++++++++ tiramisu/value.py | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/test/test_option_calculation.py b/test/test_option_calculation.py index 0266855..d57072f 100644 --- a/test/test_option_calculation.py +++ b/test/test_option_calculation.py @@ -498,3 +498,14 @@ def test_callback_hidden(): cfg.read_write() raises(PropertiesOptionError, 'cfg.od1.opt1') cfg.od2.opt2 + + +def test_callback_disable_make_dict(): + opt1 = BoolOption('opt1', '', properties=('disabled',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('disabled',)) + od1 = OptionDescription('od1', '', [opt1]) + od2 = OptionDescription('od2', '', [opt2]) + maconfig = OptionDescription('rootconfig', '', [od1, od2]) + cfg = Config(maconfig) + cfg.read_write() + raises(PropertiesOptionError, 'cfg.od1.opt1') diff --git a/tiramisu/value.py b/tiramisu/value.py index ffd34d6..3d172dc 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -179,6 +179,7 @@ class Values(object): is_frozen = 'frozen' in setting[opt] # if value is callback and is not set # or frozen with force_default_on_freeze + config_error = None if opt.impl_has_callback() and ( self._is_default_owner(path) or (is_frozen and 'force_default_on_freeze' in setting[opt])): @@ -193,11 +194,15 @@ class Values(object): no_value_slave = True if not no_value_slave: - value = self._getcallback_value(opt) - if (opt.impl_is_multi() and - opt.impl_get_multitype() == multitypes.slave): - if not isinstance(value, list): - value = [value for i in range(lenmaster)] + try: + value = self._getcallback_value(opt) + except ConfigError as config_error: + value = None + else: + if (opt.impl_is_multi() and + opt.impl_get_multitype() == multitypes.slave): + if not isinstance(value, list): + value = [value for i in range(lenmaster)] if opt.impl_is_multi(): value = Multi(value, self.context, opt, path, validate) # suppress value if already set @@ -218,6 +223,8 @@ class Values(object): setting.validate_properties(opt, False, False, value=value, path=path, force_permissive=force_permissive, force_properties=force_properties) + if config_error is not None: + raise ConfigError(config_error) return value def __setitem__(self, opt, value): From ffc9d086f9c41fe75fabd3581ac88b5a953faa26 Mon Sep 17 00:00:00 2001 From: gwen Date: Mon, 16 Sep 2013 15:21:08 +0200 Subject: [PATCH 38/50] double negation in error msg --- tiramisu/option.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tiramisu/option.py b/tiramisu/option.py index 313299d..0058866 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -718,7 +718,7 @@ class IPOption(Option): def _validate(self, value): ip = IP('{0}/32'.format(value)) if ip.iptype() == 'RESERVED': - raise ValueError(_("IP mustn't not be in reserved class")) + raise ValueError(_("IP shall not be in reserved class")) if self._only_private and not ip.iptype() == 'PRIVATE': raise ValueError(_("IP must be in private class")) @@ -800,7 +800,7 @@ class NetworkOption(Option): def _validate(self, value): ip = IP(value) if ip.iptype() == 'RESERVED': - raise ValueError(_("network mustn't not be in reserved class")) + raise ValueError(_("network shall not be in reserved class")) class NetmaskOption(Option): From 8def6c85d73337451274c558c53ba5fed014f921 Mon Sep 17 00:00:00 2001 From: gwen Date: Mon, 16 Sep 2013 15:24:46 +0200 Subject: [PATCH 39/50] double negation in error msg --- translations/fr/tiramisu.po | 4 ++-- translations/tiramisu.pot | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/translations/fr/tiramisu.po b/translations/fr/tiramisu.po index fd00df1..8bceb43 100644 --- a/translations/fr/tiramisu.po +++ b/translations/fr/tiramisu.po @@ -168,7 +168,7 @@ msgid "malformed symlinkoption must be an option for symlink {0}" msgstr "symlinkoption mal formé doit être une option pour symlink {0}" #: tiramisu/option.py:526 -msgid "IP mustn't not be in reserved class" +msgid "IP shall not be in reserved class" msgstr "IP ne doit pas être d'une classe reservée" #: tiramisu/option.py:528 @@ -184,7 +184,7 @@ msgid "max value is empty" msgstr "valeur maximum est vide" #: tiramisu/option.py:608 -msgid "network mustn't not be in reserved class" +msgid "network shall not be in reserved class" msgstr "réseau ne doit pas être dans la classe reservée" #: tiramisu/option.py:640 diff --git a/translations/tiramisu.pot b/translations/tiramisu.pot index 3b7b989..ef40426 100644 --- a/translations/tiramisu.pot +++ b/translations/tiramisu.pot @@ -156,7 +156,7 @@ msgid "malformed symlinkoption must be an option for symlink {0}" msgstr "" #: tiramisu/option.py:581 -msgid "IP mustn't not be in reserved class" +msgid "IP shall not be in reserved class" msgstr "" #: tiramisu/option.py:583 @@ -172,7 +172,7 @@ msgid "max value is empty" msgstr "" #: tiramisu/option.py:663 -msgid "network mustn't not be in reserved class" +msgid "network shall not be in reserved class" msgstr "" #: tiramisu/option.py:695 From 57f4dd8d3f383889f8c3358e0d90090d505a51dc Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 16 Sep 2013 20:51:13 +0200 Subject: [PATCH 40/50] allow mandatory value (see 9ddf100118d913c2f4225cca1adb65317b512d84 for more details) --- test/test_option_calculation.py | 28 +++++++++++++++++++++++++--- tiramisu/setting.py | 8 +++++++- tiramisu/value.py | 15 +++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/test/test_option_calculation.py b/test/test_option_calculation.py index d57072f..1441460 100644 --- a/test/test_option_calculation.py +++ b/test/test_option_calculation.py @@ -5,7 +5,7 @@ from tiramisu.setting import groups from tiramisu.config import Config from tiramisu.option import ChoiceOption, BoolOption, IntOption, FloatOption, \ StrOption, OptionDescription -from tiramisu.error import PropertiesOptionError, ConflictError, SlaveError +from tiramisu.error import PropertiesOptionError, ConflictError, SlaveError, ConfigError def return_val(): @@ -500,7 +500,7 @@ def test_callback_hidden(): cfg.od2.opt2 -def test_callback_disable_make_dict(): +def test_callback_two_disabled(): opt1 = BoolOption('opt1', '', properties=('disabled',)) opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('disabled',)) od1 = OptionDescription('od1', '', [opt1]) @@ -508,4 +508,26 @@ def test_callback_disable_make_dict(): maconfig = OptionDescription('rootconfig', '', [od1, od2]) cfg = Config(maconfig) cfg.read_write() - raises(PropertiesOptionError, 'cfg.od1.opt1') + raises(PropertiesOptionError, 'cfg.od2.opt2') + + +def test_callback_calculating_disabled(): + opt1 = BoolOption('opt1', '', properties=('disabled',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}) + od1 = OptionDescription('od1', '', [opt1]) + od2 = OptionDescription('od2', '', [opt2]) + maconfig = OptionDescription('rootconfig', '', [od1, od2]) + cfg = Config(maconfig) + cfg.read_write() + raises(ConfigError, 'cfg.od2.opt2') + + +def test_callback_calculating_mandatory(): + opt1 = BoolOption('opt1', '', properties=('disabled',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('mandatory',)) + od1 = OptionDescription('od1', '', [opt1]) + od2 = OptionDescription('od2', '', [opt2]) + maconfig = OptionDescription('rootconfig', '', [od1, od2]) + cfg = Config(maconfig) + cfg.read_only() + raises(ConfigError, 'cfg.od2.opt2') diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 249af37..531e846 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -385,13 +385,17 @@ class Settings(object): #____________________________________________________________ def validate_properties(self, opt_or_descr, is_descr, is_write, path, value=None, force_permissive=False, - force_properties=None): + force_properties=None, force_permissives=None): """ validation upon the properties related to `opt_or_descr` :param opt_or_descr: an option or an option description object :param force_permissive: behaves as if the permissive property was present + :param force_properties: set() with properties that is force to add + in global properties + :param force_permissives: set() with permissives that is force to add + in global permissives :param is_descr: we have to know if we are in an option description, just because the mandatory property doesn't exist here @@ -408,6 +412,8 @@ class Settings(object): self_properties = copy(self._getproperties()) if force_permissive is True or 'permissive' in self_properties: properties -= self._p_.getpermissive() + if force_permissives is not None: + properties -= force_permissives # global properties if force_properties is not None: diff --git a/tiramisu/value.py b/tiramisu/value.py index 3d172dc..911f4a8 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -177,9 +177,16 @@ class Values(object): # options with callbacks setting = self.context().cfgimpl_get_settings() is_frozen = 'frozen' in setting[opt] + # For calculating properties, we need value (ie for mandatory value). + # If value is calculating with a PropertiesOptionError's option + # _getcallback_value raise a ConfigError. + # We can not raise ConfigError if this option should raise + # PropertiesOptionError too. So we get config_error and raise + # ConfigError if properties did not raise. + config_error = None + force_permissives = None # if value is callback and is not set # or frozen with force_default_on_freeze - config_error = None if opt.impl_has_callback() and ( self._is_default_owner(path) or (is_frozen and 'force_default_on_freeze' in setting[opt])): @@ -198,6 +205,9 @@ class Values(object): value = self._getcallback_value(opt) except ConfigError as config_error: value = None + # should not raise PropertiesOptionError if option is + # mandatory + force_permissives = set(['mandatory']) else: if (opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.slave): @@ -222,7 +232,8 @@ class Values(object): if validate_properties: setting.validate_properties(opt, False, False, value=value, path=path, force_permissive=force_permissive, - force_properties=force_properties) + force_properties=force_properties, + force_permissives=force_permissives) if config_error is not None: raise ConfigError(config_error) return value From 866364059c33cebd3b0840cdfe82364a6f6541f6 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Tue, 17 Sep 2013 09:10:08 +0200 Subject: [PATCH 41/50] dont change anything if config_error --- test/test_option_calculation.py | 11 +++++++++++ tiramisu/value.py | 13 +++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/test_option_calculation.py b/test/test_option_calculation.py index 1441460..17f685c 100644 --- a/test/test_option_calculation.py +++ b/test/test_option_calculation.py @@ -531,3 +531,14 @@ def test_callback_calculating_mandatory(): cfg = Config(maconfig) cfg.read_only() raises(ConfigError, 'cfg.od2.opt2') + + +def test_callback_two_disabled_multi(): + opt1 = BoolOption('opt1', '', properties=('disabled',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('disabled',), multi=True) + od1 = OptionDescription('od1', '', [opt1]) + od2 = OptionDescription('od2', '', [opt2]) + maconfig = OptionDescription('rootconfig', '', [od1, od2]) + cfg = Config(maconfig) + cfg.read_write() + raises(PropertiesOptionError, 'cfg.od2.opt2') diff --git a/tiramisu/value.py b/tiramisu/value.py index 911f4a8..578c4ee 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -213,10 +213,11 @@ class Values(object): opt.impl_get_multitype() == multitypes.slave): if not isinstance(value, list): value = [value for i in range(lenmaster)] - if opt.impl_is_multi(): - value = Multi(value, self.context, opt, path, validate) - # suppress value if already set - self.reset(opt, path) + if config_error is None: + if opt.impl_is_multi(): + value = Multi(value, self.context, opt, path, validate) + # suppress value if already set + self.reset(opt, path) # frozen and force default elif is_frozen and 'force_default_on_freeze' in setting[opt]: value = self._getdefault(opt) @@ -224,9 +225,9 @@ class Values(object): value = Multi(value, self.context, opt, path, validate) else: value = self._getvalue(opt, path, validate) - if validate: + if config_error is None and validate: opt.impl_validate(value, self.context(), 'validator' in setting) - if self._is_default_owner(path) and \ + if config_error is None and self._is_default_owner(path) and \ 'force_store_value' in setting[opt]: self.setitem(opt, value, path, is_write=False) if validate_properties: From 90ae9aa70dae57821c6ae01dc27eeb799fb41e1e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Thu, 19 Sep 2013 21:38:46 +0200 Subject: [PATCH 42/50] refactore carry_out_calculation + test + documentation --- test/test_option_calculation.py | 163 +++++++++++++++++++++++++++++--- test/test_option_validator.py | 59 ++++++++++++ tiramisu/autolib.py | 139 +++++++++++++++++++-------- tiramisu/option.py | 91 ++++++++++++------ tiramisu/value.py | 33 +++++-- 5 files changed, 397 insertions(+), 88 deletions(-) create mode 100644 test/test_option_validator.py diff --git a/test/test_option_calculation.py b/test/test_option_calculation.py index 17f685c..8ca8687 100644 --- a/test/test_option_calculation.py +++ b/test/test_option_calculation.py @@ -4,7 +4,7 @@ from py.test import raises from tiramisu.setting import groups from tiramisu.config import Config from tiramisu.option import ChoiceOption, BoolOption, IntOption, FloatOption, \ - StrOption, OptionDescription + StrOption, OptionDescription, SymLinkOption from tiramisu.error import PropertiesOptionError, ConflictError, SlaveError, ConfigError @@ -12,11 +12,19 @@ def return_val(): return 'val' -def return_list(): +def return_concat(*args): + return '.'.join(list(args)) + + +def return_list(value=None): return ['val', 'val'] -def return_value(value): +def return_list2(*args): + return list(args) + + +def return_value(value=None): return value @@ -298,18 +306,73 @@ def test_callback(): def test_callback_value(): val1 = StrOption('val1', "", 'val') - val2 = StrOption('val2', "", callback=return_value, callback_params={'': (('val1', False),)}) - maconfig = OptionDescription('rootconfig', '', [val1, val2]) + val2 = StrOption('val2', "", callback=return_value, callback_params={'': ((val1, False),)}) + 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',)}) + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3, val4, val5]) cfg = Config(maconfig) cfg.read_write() assert cfg.val1 == 'val' assert cfg.val2 == 'val' + assert cfg.val4 == 'val' cfg.val1 = 'new-val' assert cfg.val1 == 'new-val' assert cfg.val2 == 'new-val' + assert cfg.val4 == 'new-val' del(cfg.val1) assert cfg.val1 == 'val' assert cfg.val2 == 'val' + assert cfg.val3 == 'yes' + assert cfg.val4 == 'val' + assert cfg.val5 == 'yes' + + +def test_callback_value_tuple(): + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "", 'val2') + val3 = StrOption('val3', "", callback=return_concat, callback_params={'': ((val1, False), (val2, False))}) + val4 = StrOption('val4', "", callback=return_concat, callback_params={'': ('yes', 'no')}) + raises(ValueError, "StrOption('val4', '', callback=return_concat, callback_params={'value': ('yes', 'no')})") + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3, val4]) + cfg = Config(maconfig) + cfg.read_write() + assert cfg.val1 == 'val1' + assert cfg.val2 == 'val2' + assert cfg.val3 == 'val1.val2' + assert cfg.val4 == 'yes.no' + cfg.val1 = 'new-val' + assert cfg.val3 == 'new-val.val2' + del(cfg.val1) + assert cfg.val3 == 'val1.val2' + + +def test_callback_value_force_permissive(): + val1 = StrOption('val1', "", 'val', properties=('disabled',)) + val2 = StrOption('val2', "", callback=return_value, callback_params={'': ((val1, False),)}) + val3 = StrOption('val3', "", callback=return_value, callback_params={'': ((val1, True),)}) + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3]) + cfg = Config(maconfig) + cfg.read_only() + raises(ConfigError, "cfg.val2") + assert cfg.val3 is None + + +def test_callback_symlink(): + val1 = StrOption('val1', "", 'val') + val2 = SymLinkOption('val2', val1) + val3 = StrOption('val3', "", callback=return_value, callback_params={'': ((val2, False),)}) + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3]) + cfg = Config(maconfig) + cfg.read_write() + assert cfg.val1 == 'val' + assert cfg.val3 == 'val' + cfg.val1 = 'new-val' + assert cfg.val1 == 'new-val' + assert cfg.val3 == 'new-val' + del(cfg.val1) + assert cfg.val1 == 'val' + assert cfg.val3 == 'val' def test_callback_list(): @@ -336,21 +399,28 @@ def test_callback_multi(): def test_callback_multi_value(): val1 = StrOption('val1', "", ['val'], multi=True) - val2 = StrOption('val2', "", multi=True, callback=return_value, callback_params={'': (('val1', False),)}) - maconfig = OptionDescription('rootconfig', '', [val1, val2]) + val2 = StrOption('val2', "", multi=True, callback=return_value, callback_params={'': ((val1, False),)}) + val3 = StrOption('val3', "", multi=True, callback=return_value, callback_params={'': ('yes',)}) + val4 = StrOption('val4', "", multi=True, callback=return_list2, callback_params={'': ((val1, False), 'yes')}) + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3, val4]) cfg = Config(maconfig) cfg.read_write() assert cfg.val1 == ['val'] assert cfg.val2 == ['val'] + assert cfg.val4 == ['val', 'yes'] cfg.val1 = ['new-val'] assert cfg.val1 == ['new-val'] assert cfg.val2 == ['new-val'] + assert cfg.val4 == ['new-val', 'yes'] cfg.val1.append('new-val2') assert cfg.val1 == ['new-val', 'new-val2'] assert cfg.val2 == ['new-val', 'new-val2'] + assert cfg.val4 == ['new-val', 'yes', 'new-val2', 'yes'] del(cfg.val1) assert cfg.val1 == ['val'] assert cfg.val2 == ['val'] + assert cfg.val3 == ['yes'] + assert cfg.val4 == ['val', 'yes'] def test_callback_multi_list(): @@ -455,41 +525,67 @@ def test_callback_master_and_slaves_slave_list(): def test_callback_master_and_slaves_value(): val1 = StrOption('val1', "", multi=True) - val2 = StrOption('val2', "", multi=True, callback=return_value, callback_params={'': (('val1.val1', False),)}) - interface1 = OptionDescription('val1', '', [val1, val2]) + val2 = StrOption('val2', "", multi=True, callback=return_value, callback_params={'': ((val1, False),)}) + val3 = StrOption('val3', "", multi=True, callback=return_value, callback_params={'': ('yes',)}) + val4 = StrOption('val4', '', multi=True, default=['val10', 'val11']) + val5 = StrOption('val5', "", multi=True, callback=return_value, callback_params={'': ((val4, False),)}) + interface1 = OptionDescription('val1', '', [val1, val2, val3, val5]) interface1.impl_set_group_type(groups.master) - maconfig = OptionDescription('rootconfig', '', [interface1]) + maconfig = OptionDescription('rootconfig', '', [interface1, val4]) cfg = Config(maconfig) cfg.read_write() assert cfg.val1.val1 == [] assert cfg.val1.val2 == [] + assert cfg.val1.val3 == [] + assert cfg.val1.val5 == [] # cfg.val1.val1 = ['val1'] assert cfg.val1.val1 == ['val1'] assert cfg.val1.val2 == ['val1'] + assert cfg.val1.val3 == ['yes'] + assert cfg.val1.val5 == ['val10'] # cfg.val1.val1.append('val2') assert cfg.val1.val1 == ['val1', 'val2'] assert cfg.val1.val2 == ['val1', 'val2'] + assert cfg.val1.val3 == ['yes', 'yes'] + assert cfg.val1.val5 == ['val10', 'val11'] # cfg.val1.val1 = ['val1', 'val2', 'val3'] assert cfg.val1.val1 == ['val1', 'val2', 'val3'] assert cfg.val1.val2 == ['val1', 'val2', 'val3'] + assert cfg.val1.val3 == ['yes', 'yes', 'yes'] + assert cfg.val1.val5 == ['val10', 'val11', None] # cfg.val1.val1.pop(2) assert cfg.val1.val1 == ['val1', 'val2'] assert cfg.val1.val2 == ['val1', 'val2'] + assert cfg.val1.val3 == ['yes', 'yes'] + assert cfg.val1.val5 == ['val10', 'val11'] # cfg.val1.val2 = ['val2', 'val2'] + cfg.val1.val3 = ['val2', 'val2'] + cfg.val1.val5 = ['val2', 'val2'] assert cfg.val1.val2 == ['val2', 'val2'] + assert cfg.val1.val3 == ['val2', 'val2'] + assert cfg.val1.val5 == ['val2', 'val2'] # cfg.val1.val1.append('val3') assert cfg.val1.val2 == ['val2', 'val2', 'val3'] + assert cfg.val1.val3 == ['val2', 'val2', 'yes'] + assert cfg.val1.val5 == ['val2', 'val2', None] + cfg.cfgimpl_get_settings().remove('cache') + cfg.val4 = ['val10', 'val11', 'val12'] + #if value is already set, not updated ! + cfg.val1.val1.pop(2) + cfg.val1.val1.append('val3') + cfg.val1.val1 = ['val1', 'val2', 'val3'] + assert cfg.val1.val5 == ['val2', 'val2', 'val12'] def test_callback_hidden(): opt1 = BoolOption('opt1', '') - opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': ((opt1, False),)}) od1 = OptionDescription('od1', '', [opt1], properties=('hidden',)) od2 = OptionDescription('od2', '', [opt2]) maconfig = OptionDescription('rootconfig', '', [od1, od2]) @@ -502,7 +598,7 @@ def test_callback_hidden(): def test_callback_two_disabled(): opt1 = BoolOption('opt1', '', properties=('disabled',)) - opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('disabled',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': ((opt1, False),)}, properties=('disabled',)) od1 = OptionDescription('od1', '', [opt1]) od2 = OptionDescription('od2', '', [opt2]) maconfig = OptionDescription('rootconfig', '', [od1, od2]) @@ -513,7 +609,7 @@ def test_callback_two_disabled(): def test_callback_calculating_disabled(): opt1 = BoolOption('opt1', '', properties=('disabled',)) - opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': ((opt1, False),)}) od1 = OptionDescription('od1', '', [opt1]) od2 = OptionDescription('od2', '', [opt2]) maconfig = OptionDescription('rootconfig', '', [od1, od2]) @@ -524,7 +620,7 @@ def test_callback_calculating_disabled(): def test_callback_calculating_mandatory(): opt1 = BoolOption('opt1', '', properties=('disabled',)) - opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('mandatory',)) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': ((opt1, False),)}, properties=('mandatory',)) od1 = OptionDescription('od1', '', [opt1]) od2 = OptionDescription('od2', '', [opt2]) maconfig = OptionDescription('rootconfig', '', [od1, od2]) @@ -535,10 +631,47 @@ def test_callback_calculating_mandatory(): def test_callback_two_disabled_multi(): opt1 = BoolOption('opt1', '', properties=('disabled',)) - opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': (('od1.opt1', False),)}, properties=('disabled',), multi=True) + opt2 = BoolOption('opt2', '', callback=return_value, callback_params={'': ((opt1, False),)}, properties=('disabled',), multi=True) od1 = OptionDescription('od1', '', [opt1]) od2 = OptionDescription('od2', '', [opt2]) maconfig = OptionDescription('rootconfig', '', [od1, od2]) cfg = Config(maconfig) cfg.read_write() raises(PropertiesOptionError, 'cfg.od2.opt2') + + +def test_callback_multi_list_params(): + val1 = StrOption('val1', "", multi=True, default=['val1', 'val2']) + val2 = StrOption('val2', "", multi=True, callback=return_list, callback_params={'': ((val1, False),)}) + oval2 = OptionDescription('val2', '', [val2]) + maconfig = OptionDescription('rootconfig', '', [val1, oval2]) + cfg = Config(maconfig) + cfg.read_write() + assert cfg.val2.val2 == ['val', 'val', 'val', 'val'] + + +def test_callback_multi_list_params_key(): + val1 = StrOption('val1', "", multi=True, default=['val1', 'val2']) + val2 = StrOption('val2', "", multi=True, callback=return_list, callback_params={'value': ((val1, False),)}) + oval2 = OptionDescription('val2', '', [val2]) + maconfig = OptionDescription('rootconfig', '', [val1, oval2]) + cfg = Config(maconfig) + cfg.read_write() + assert cfg.val2.val2 == ['val', 'val', 'val', 'val'] + + +def test_callback_multi_multi(): + val1 = StrOption('val1', "", multi=True, default=['val1', 'val2', 'val3']) + val2 = StrOption('val2', "", multi=True, default=['val11', 'val12']) + val3 = StrOption('val3', "", default='val4') + val4 = StrOption('val4', "", multi=True, callback=return_list2, callback_params={'': ((val1, False), (val2, False))}) + val5 = StrOption('val5', "", multi=True, callback=return_list2, callback_params={'': ((val1, False), (val3, False))}) + val6 = StrOption('val6', "", multi=True, default=['val21', 'val22', 'val23']) + val7 = StrOption('val7', "", multi=True, callback=return_list2, callback_params={'': ((val1, False), (val6, False))}) + raises(ValueError, "StrOption('val8', '', multi=True, callback=return_list2, callback_params={'value': ((val1, False), (val6, False))})") + maconfig = OptionDescription('rootconfig', '', [val1, val2, val3, val4, val5, val6, val7]) + cfg = Config(maconfig) + cfg.read_write() + raises(ConfigError, "cfg.val4") + assert cfg.val5 == ['val1', 'val4', 'val2', 'val4', 'val3', 'val4'] + assert cfg.val7 == ['val1', 'val21', 'val2', 'val22', 'val3', 'val23'] diff --git a/test/test_option_validator.py b/test/test_option_validator.py new file mode 100644 index 0000000..8e00916 --- /dev/null +++ b/test/test_option_validator.py @@ -0,0 +1,59 @@ +import autopath +from py.test import raises + +from tiramisu.config import Config +from tiramisu.option import StrOption, OptionDescription +from tiramisu.error import ConfigError + + +def return_true(value, param=None): + if value == 'val' and param in [None, 'yes']: + return True + + +def return_false(value, param=None): + if value == 'val' and param in [None, 'yes']: + return False + + +def return_val(value, param=None): + return 'val' + + +def test_validator(): + opt1 = StrOption('opt1', '', validator=return_true, default='val') + raises(ValueError, "StrOption('opt2', '', validator=return_false, default='val')") + raises(ConfigError, "StrOption('opt3', '', validator=return_val, default='val')") + opt2 = StrOption('opt2', '', validator=return_false) + opt3 = StrOption('opt3', '', validator=return_val) + root = OptionDescription('root', '', [opt1, opt2, opt3]) + cfg = Config(root) + assert cfg.opt1 == 'val' + raises(ValueError, "cfg.opt2 = 'val'") + raises(ConfigError, "cfg.opt3 = 'val'") + + +def test_validator_params(): + opt1 = StrOption('opt1', '', validator=return_true, validator_params={'': ('yes',)}, default='val') + raises(ValueError, "StrOption('opt2', '', validator=return_false, validator_params={'': ('yes',)}, default='val')") + raises(ConfigError, "StrOption('opt3', '', validator=return_val, validator_params={'': ('yes',)}, default='val')") + opt2 = StrOption('opt2', '', validator=return_false, validator_params={'': ('yes',)}) + opt3 = StrOption('opt3', '', validator=return_val, validator_params={'': ('yes',)}) + root = OptionDescription('root', '', [opt1, opt2, opt3]) + cfg = Config(root) + assert cfg.opt1 == 'val' + raises(ValueError, "cfg.opt2 = 'val'") + raises(ConfigError, "cfg.opt3 = 'val'") + + +def test_validator_params_key(): + opt1 = StrOption('opt1', '', validator=return_true, validator_params={'param': ('yes',)}, default='val') + raises(TypeError, "StrOption('opt2', '', validator=return_true, validator_params={'param_unknown': ('yes',)}, default='val')") + root = OptionDescription('root', '', [opt1]) + cfg = Config(root) + assert cfg.opt1 == 'val' + + +def test_validator_params_option(): + opt0 = StrOption('opt0', '', default='val') + raises(ValueError, "opt1 = StrOption('opt1', '', validator=return_true, validator_params={'': ((opt0, False),)}, default='val')") diff --git a/tiramisu/autolib.py b/tiramisu/autolib.py index 2cf41ca..de8a8c5 100644 --- a/tiramisu/autolib.py +++ b/tiramisu/autolib.py @@ -23,11 +23,9 @@ from tiramisu.error import PropertiesOptionError, ConfigError from tiramisu.i18n import _ # ____________________________________________________________ -def carry_out_calculation(name, - config, - callback, - callback_params, - index=None): + +def carry_out_calculation(name, config, callback, callback_params, + index=None, max_len=None): """a function that carries out a calculation for an option's value :param name: the option name (`opt._name`) @@ -40,36 +38,104 @@ def carry_out_calculation(name, :type callback_params: dict :param index: if an option is multi, only calculates the nth value :type index: int + :param max_len: max length for a multi + :type max_len: int + + * if no callback_params: + => calculate() + + * if callback_params={'': ('yes',)} + => calculate('yes') + + * if callback_params={'value': ('yes',)} + => calculate(value='yes') + + * if callback_params={'': ('yes', 'no')} + => calculate('yes', 'no') + + * if callback_params={'value': ('yes', 'no')} + => ValueError() + + * if callback_params={'': ((opt1, False),)} + + - a simple option: + opt1 == 11 + => calculate(11) + + - a multi option: + opt1 == [1, 2, 3] + => calculate(1) + => calculate(2) + => calculate(3) + + * if callback_params={'value': ((opt1, False),)} + + - a simple option: + opt1 == 11 + => calculate(value=11) + + - a multi option: + opt1 == [1, 2, 3] + => calculate(value=1) + => calculate(value=2) + => calculate(value=3) + + * if callback_params={'': ((opt1, False), (opt2, False))} + + - a multi option with a simple option + opt1 == [1, 2, 3] + opt2 == 11 + => calculate(1, 11) + => calculate(2, 11) + => calculate(3, 11) + + - a multi option with an other multi option but with same length + opt1 == [1, 2, 3] + opt2 == [11, 12, 13] + callback_params={'': ((opt1, False), (opt2, False))} + => calculate(1, 11) + => calculate(2, 12) + => calculate(3, 13) + + - a multi option with an other multi option but with different length + opt1 == [1, 2, 3] + opt2 == [11, 12] + callback_params={'': ((opt1, False), (opt2, False))} + => ConfigError() + + * if callback_params={'value': ((opt1, False), (opt2, False))} + => ConfigError() + + If index is not None, return a value, otherwise return: + + * a list if one parameters have multi option + * a value otherwise + + If calculate return list, this list is extend to return value. """ - #callback, callback_params = option.getcallback() - #if callback_params is None: - # callback_params = {} tcparams = {} one_is_multi = False len_multi = 0 - for key, values in callback_params.items(): - for value in values: - if type(value) == tuple: - path, check_disabled = value - if config is None: - if check_disabled: - continue - raise ConfigError(_('no config specified but needed')) + for key, callbacks in callback_params.items(): + for callbk in callbacks: + if isinstance(callbk, tuple): + option, force_permissive = callbk + # get value try: - opt_value = config._getattr(path, force_permissive=True) - opt = config.unwrap_from_path(path, force_permissive=True) + path = config.cfgimpl_get_description().impl_get_path_by_opt(option) + value = config._getattr(path, force_permissive=True) except PropertiesOptionError as err: - if check_disabled: + if force_permissive: continue raise ConfigError(_('unable to carry out a calculation, ' 'option {0} has properties: {1} for: ' - '{2}').format(path, err.proptype, + '{2}').format(option._name, err.proptype, name)) - is_multi = opt.impl_is_multi() + is_multi = option.impl_is_multi() if is_multi: - if opt_value is not None: - len_value = len(opt_value) + if value is not None: + len_value = len(value) if len_multi != 0 and len_multi != len_value: raise ConfigError(_('unable to carry out a ' 'calculation, option value with' @@ -77,16 +143,23 @@ def carry_out_calculation(name, 'length for: {0}').format(name)) len_multi = len_value one_is_multi = True - tcparams.setdefault(key, []).append((opt_value, is_multi)) + tcparams.setdefault(key, []).append((value, is_multi)) else: - tcparams.setdefault(key, []).append((value, False)) + tcparams.setdefault(key, []).append((callbk, False)) if one_is_multi: ret = [] if index: - range_ = [index] + if index < len_multi: + range_ = [index] + else: + range_ = [] + ret = None else: - range_ = range(len_multi) + if max_len and max_len < len_multi: + range_ = range(max_len) + else: + range_ = range(len_multi) for incr in range_: tcp = {} params = [] @@ -97,15 +170,9 @@ def carry_out_calculation(name, if key == '': params.append(value[incr]) else: - if len(value) > incr: - tcp[key] = value[incr] - else: - tcp[key] = '' + tcp[key] = value[incr] else: - if key == '': - params.append(value) - else: - tcp[key] = value + params.append(value) calc = calculate(name, callback, params, tcp) if index: ret = calc @@ -114,7 +181,6 @@ def carry_out_calculation(name, ret.extend(calc) else: ret.append(calc) - return ret else: tcp = {} @@ -130,7 +196,6 @@ def carry_out_calculation(name, def calculate(name, callback, params, tcparams): - # FIXME we don't need the option's name down there. """wrapper that launches the 'callback' :param callback: callback name diff --git a/tiramisu/option.py b/tiramisu/option.py index 0058866..1948f39 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -26,7 +26,7 @@ from copy import copy, deepcopy from types import FunctionType from IPy import IP -from tiramisu.error import ConflictError +from tiramisu.error import ConflictError, ConfigError from tiramisu.setting import groups, multitypes from tiramisu.i18n import _ from tiramisu.autolib import carry_out_calculation @@ -97,8 +97,8 @@ class BaseOption(object): "frozen" (which has noting to do with the high level "freeze" propertie or "read_only" property) """ - if not name.startswith('_state') and \ - name not in ('_cache_paths', '_consistencies'): + if not name.startswith('_state') and name not in ('_cache_paths', + '_consistencies'): is_readonly = False # never change _name if name == '_name': @@ -327,7 +327,7 @@ class Option(BaseOption): def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, - callback_params=None, validator=None, validator_args=None, + callback_params=None, validator=None, validator_params=None, properties=None): """ :param name: the option's name @@ -344,18 +344,15 @@ class Option(BaseOption): :param callback_params: the callback's parameter :param validator: the name of a function which stands for a custom validation of the value - :param validator_args: the validator's parameters + :param validator_params: the validator's parameters :param properties: tuple of default properties """ super(Option, self).__init__(name, doc, requires, properties) self._multi = multi if validator is not None: - if type(validator) != FunctionType: - raise TypeError(_("validator must be a function")) - if validator_args is None: - validator_args = {} - self._validator = (validator, validator_args) + validate_callback(validator, validator_params, 'validator') + self._validator = (validator, validator_params) else: self._validator = None if not self._multi and default_multi is not None: @@ -377,11 +374,7 @@ class Option(BaseOption): "no callback defined" " yet for option {0}").format(name)) if callback is not None: - if type(callback) != FunctionType: - raise ValueError('callback must be a function') - if callback_params is not None and \ - not isinstance(callback_params, dict): - raise ValueError('callback_params must be a dict') + validate_callback(callback, callback_params, 'callback') self._callback = (callback, callback_params) else: self._callback = None @@ -448,11 +441,23 @@ class Option(BaseOption): def val_validator(val): if self._validator is not None: - callback_params = deepcopy(self._validator[1]) - callback_params.setdefault('', []).insert(0, val) - return carry_out_calculation(self._name, config=context, - callback=self._validator[0], - callback_params=callback_params) + if self._validator[1] is not None: + validator_params = deepcopy(self._validator[1]) + if '' in validator_params: + lst = list(validator_params['']) + lst.insert(0, val) + validator_params[''] = tuple(lst) + else: + validator_params[''] = (val,) + else: + validator_params = {'': (val,)} + ret = carry_out_calculation(self._name, config=context, + callback=self._validator[0], + callback_params=validator_params) + if ret not in [False, True]: + raise ConfigError(_('validator should return a boolean, ' + 'not {0}').format(ret)) + return ret else: return True @@ -566,7 +571,7 @@ class ChoiceOption(Option): def __init__(self, name, doc, values, default=None, default_multi=None, requires=None, multi=False, callback=None, callback_params=None, open_values=False, validator=None, - validator_args=None, properties=()): + validator_params=None, properties=()): """ :param values: is a list of values the option can possibly take """ @@ -584,7 +589,7 @@ class ChoiceOption(Option): requires=requires, multi=multi, validator=validator, - validator_args=validator_args, + validator_params=validator_params, properties=properties) def impl_get_values(self): @@ -702,7 +707,7 @@ class IPOption(Option): def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, - callback_params=None, validator=None, validator_args=None, + callback_params=None, validator=None, validator_params=None, properties=None, only_private=False): self._only_private = only_private super(IPOption, self).__init__(name, doc, default=default, @@ -712,7 +717,7 @@ class IPOption(Option): requires=requires, multi=multi, validator=validator, - validator_args=validator_args, + validator_params=validator_params, properties=properties) def _validate(self, value): @@ -738,7 +743,7 @@ class PortOption(Option): def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, - callback_params=None, validator=None, validator_args=None, + callback_params=None, validator=None, validator_params=None, properties=None, allow_range=False, allow_zero=False, allow_wellknown=True, allow_registred=True, allow_private=False): @@ -772,7 +777,7 @@ class PortOption(Option): requires=requires, multi=multi, validator=validator, - validator_args=validator_args, + validator_params=validator_params, properties=properties) def _validate(self, value): @@ -857,7 +862,7 @@ class DomainnameOption(Option): def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, - callback_params=None, validator=None, validator_args=None, + callback_params=None, validator=None, validator_params=None, properties=None, allow_ip=False, type_='domainname'): #netbios: for MS domain #hostname: to identify the device @@ -876,7 +881,7 @@ class DomainnameOption(Option): requires=requires, multi=multi, validator=validator, - validator_args=validator_args, + validator_params=validator_params, properties=properties) def _validate(self, value): @@ -1252,3 +1257,33 @@ def validate_requires_arg(requires, name): 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_): + if type(callback) != FunctionType: + raise ValueError(_('{0} should be a function').format(type_)) + if callback_params is not None: + if not isinstance(callback_params, dict): + raise ValueError(_('{0}_params should be a dict').format(type_)) + for key, callbacks in callback_params.items(): + if key != '' and len(callbacks) != 1: + raise ValueError(_('{0}_params with key {1} should not have ' + 'length different to 1').format(type_, + key)) + if not isinstance(callbacks, tuple): + raise ValueError(_('{0}_params should be tuple for key "{1}"' + ).format(type_, key)) + for callbk in callbacks: + if isinstance(callbk, tuple): + option, force_permissive = callbk + if type_ == 'validator' and not force_permissive: + raise ValueError(_('validator not support tuple')) + if not isinstance(option, Option) and not \ + isinstance(option, SymLinkOption): + raise ValueError(_('{0}_params should have an option ' + 'not a {0} for first argument' + ).format(type_, type(option))) + if force_permissive not in [True, False]: + raise ValueError(_('{0}_params should have a boolean' + 'not a {0} for second argument' + ).format(type_, type(force_permissive))) diff --git a/tiramisu/value.py b/tiramisu/value.py index 578c4ee..b600eb8 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -124,7 +124,7 @@ class Values(object): return True return False - def _getcallback_value(self, opt, index=None): + def _getcallback_value(self, opt, index=None, max_len=None): """ retrieves a value for the options that have a callback @@ -139,7 +139,7 @@ class Values(object): return carry_out_calculation(opt._name, config=self.context(), callback=callback, callback_params=callback_params, - index=index) + index=index, max_len=max_len) def __getitem__(self, opt): "enables us to use the pythonic dictionary-like access to values" @@ -190,6 +190,7 @@ class Values(object): if opt.impl_has_callback() and ( self._is_default_owner(path) or (is_frozen and 'force_default_on_freeze' in setting[opt])): + lenmaster = None no_value_slave = False if (opt.impl_is_multi() and opt.impl_get_multitype() == multitypes.slave): @@ -202,7 +203,7 @@ class Values(object): if not no_value_slave: try: - value = self._getcallback_value(opt) + value = self._getcallback_value(opt, max_len=lenmaster) except ConfigError as config_error: value = None # should not raise PropertiesOptionError if option is @@ -389,20 +390,27 @@ class Multi(list): def _valid_slave(self, value): #if slave, had values until master's one + values = self.context().cfgimpl_get_values() masterp = self.context().cfgimpl_get_description().impl_get_path_by_opt( self.opt.impl_get_master_slaves()) mastervalue = getattr(self.context(), masterp) masterlen = len(mastervalue) valuelen = len(value) if valuelen > masterlen or (valuelen < masterlen and - not self.context().cfgimpl_get_values( - )._is_default_owner(self.path)): + not values._is_default_owner(self.path)): raise SlaveError(_("invalid len for the slave: {0}" " which has {1} as master").format( self.opt._name, masterp)) elif valuelen < masterlen: for num in range(0, masterlen - valuelen): - value.append(self.opt.impl_getdefault_multi()) + if self.opt.impl_has_callback(): + # if callback add a value, but this value will not change + # anymore automaticly (because this value has owner) + index = value.__len__() + value.append(values._getcallback_value(self.opt, + index=index)) + else: + value.append(self.opt.impl_getdefault_multi()) #else: same len so do nothing return value @@ -420,8 +428,17 @@ class Multi(list): self.opt._name, slave._name)) elif len(value_slave) < masterlen: for num in range(0, masterlen - len(value_slave)): - value_slave.append(slave.impl_getdefault_multi(), - force=True) + if slave.impl_has_callback(): + # if callback add a value, but this value will not + # change anymore automaticly (because this value + # has owner) + index = value_slave.__len__() + value_slave.append( + values._getcallback_value(slave, index=index), + force=True) + else: + value_slave.append(slave.impl_getdefault_multi(), + force=True) def __setitem__(self, key, value): self._validate(value) From 30c376e3ead25b14c3bc0b20189e10ae01e6ecf3 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Thu, 19 Sep 2013 21:39:17 +0200 Subject: [PATCH 43/50] add doc/config.png --- doc/config.png | Bin 0 -> 15539 bytes doc/config.svg | 257 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 doc/config.png create mode 100644 doc/config.svg diff --git a/doc/config.png b/doc/config.png new file mode 100644 index 0000000000000000000000000000000000000000..a468275da56ecf74534d340d77374e631f4deaae GIT binary patch literal 15539 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 literal 0 HcmV?d00001 diff --git a/doc/config.svg b/doc/config.svg new file mode 100644 index 0000000..3ff4bc9 --- /dev/null +++ b/doc/config.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + Config + + + + + OptionDescription + + + + Option + + + + Option + + + + OptionDescription + + + + Option + + + + + + + + From 28c416dd84bba8eb1e5164af9f00de43a5aa5a6e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Thu, 19 Sep 2013 21:51:55 +0200 Subject: [PATCH 44/50] add allow_reserved in IPOption --- test/test_config_ip.py | 9 +++++++++ tiramisu/option.py | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/test/test_config_ip.py b/test/test_config_ip.py index e889c92..2bdba53 100644 --- a/test/test_config_ip.py +++ b/test/test_config_ip.py @@ -29,6 +29,15 @@ def test_ip_default(): c.a == '88.88.88.88' +def test_ip_reserved(): + a = IPOption('a', '') + b = IPOption('b', '', allow_reserved=True) + od = OptionDescription('od', '', [a, b]) + c = Config(od) + raises(ValueError, "c.a = '226.94.1.1'") + c.b = '226.94.1.1' + + def test_network(): a = NetworkOption('a', '') od = OptionDescription('od', '', [a]) diff --git a/tiramisu/option.py b/tiramisu/option.py index 1948f39..0c7e732 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -702,14 +702,15 @@ class SymLinkOption(BaseOption): class IPOption(Option): "represents the choice of an ip" - __slots__ = ('_only_private',) + __slots__ = ('_only_private', '_allow_reserved') _opt_type = 'ip' def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, callback_params=None, validator=None, validator_params=None, - properties=None, only_private=False): + properties=None, only_private=False, allow_reserved=False): self._only_private = only_private + self._allow_reserved = allow_reserved super(IPOption, self).__init__(name, doc, default=default, default_multi=default_multi, callback=callback, @@ -722,8 +723,8 @@ class IPOption(Option): def _validate(self, value): ip = IP('{0}/32'.format(value)) - if ip.iptype() == 'RESERVED': - raise ValueError(_("IP shall not be in reserved class")) + if not self._allow_reserved and ip.iptype() == 'RESERVED': + raise ValueError(_("IP mustn't not be in reserved class")) if self._only_private and not ip.iptype() == 'PRIVATE': raise ValueError(_("IP must be in private class")) From ae4df32d0e1cbe48d2d4c1353a40f60ccf4c9e6e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Thu, 19 Sep 2013 23:02:15 +0200 Subject: [PATCH 45/50] error if change slave len for default's slave option --- test/test_parsing_group.py | 23 +++++++++++++++++++---- tiramisu/value.py | 13 ++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/test/test_parsing_group.py b/test/test_parsing_group.py index 7b9dffe..7ecd860 100644 --- a/test/test_parsing_group.py +++ b/test/test_parsing_group.py @@ -64,9 +64,9 @@ def test_make_dict_filter(): config = Config(descr) config.read_write() subresult = {'numero_etab': None, 'nombre_interfaces': 1, - 'serveur_ntp': [], 'mode_conteneur_actif': False, - 'time_zone': 'Paris', 'nom_machine': 'eoleng', - 'activer_proxy_client': False} + 'serveur_ntp': [], 'mode_conteneur_actif': False, + 'time_zone': 'Paris', 'nom_machine': 'eoleng', + 'activer_proxy_client': False} result = {} for key, value in subresult.items(): result['general.' + key] = value @@ -114,7 +114,6 @@ def test_iter_not_group(): raises(TypeError, "list(config.iter_groups(group_type='family'))") - def test_groups_with_master(): ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True) netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True) @@ -252,6 +251,22 @@ def test_values_with_master_and_slaves_master(): assert cfg.ip_admin_eth0.netmask_admin_eth0 == [] +def test_values_with_master_and_slaves_master_error(): + ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True) + netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True) + interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0]) + interface1.impl_set_group_type(groups.master) + maconfig = OptionDescription('toto', '', [interface1]) + cfg = Config(maconfig) + cfg.read_write() + cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145", "192.168.230.145"] + raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = ['255.255.255.0']") + raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = ['255.255.255.0', '255.255.255.0', '255.255.255.0']") + cfg.ip_admin_eth0.netmask_admin_eth0 = ['255.255.255.0', '255.255.255.0'] + raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = ['255.255.255.0']") + raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = ['255.255.255.0', '255.255.255.0', '255.255.255.0']") + + def test_values_with_master_owner(): ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True) netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True) diff --git a/tiramisu/value.py b/tiramisu/value.py index b600eb8..c6f0e76 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -251,7 +251,7 @@ class Values(object): opt.impl_validate(value, self.context(), 'validator' in self.context().cfgimpl_get_settings()) if opt.impl_is_multi() and not isinstance(value, Multi): - value = Multi(value, self.context, opt, path) + value = Multi(value, self.context, opt, path, setitem=True) self._setvalue(opt, path, value, force_permissive=force_permissive, is_write=is_write) @@ -369,11 +369,13 @@ class Multi(list): that support item notation for the values of multi options""" __slots__ = ('opt', 'path', 'context') - def __init__(self, value, context, opt, path, validate=True): + def __init__(self, value, context, opt, path, validate=True, + setitem=False): """ :param value: the Multi wraps a list value :param context: the home config that has the values :param opt: the option object that have this Multi value + :param setitem: only if set a value """ self.opt = opt self.path = path @@ -383,12 +385,12 @@ class Multi(list): if not isinstance(value, list): value = [value] if validate and self.opt.impl_get_multitype() == multitypes.slave: - value = self._valid_slave(value) + value = self._valid_slave(value, setitem) elif self.opt.impl_get_multitype() == multitypes.master: self._valid_master(value) super(Multi, self).__init__(value) - def _valid_slave(self, value): + def _valid_slave(self, value, setitem): #if slave, had values until master's one values = self.context().cfgimpl_get_values() masterp = self.context().cfgimpl_get_description().impl_get_path_by_opt( @@ -396,8 +398,9 @@ class Multi(list): mastervalue = getattr(self.context(), masterp) masterlen = len(mastervalue) valuelen = len(value) + is_default_owner = not values._is_default_owner(self.path) or setitem if valuelen > masterlen or (valuelen < masterlen and - not values._is_default_owner(self.path)): + is_default_owner): raise SlaveError(_("invalid len for the slave: {0}" " which has {1} as master").format( self.opt._name, masterp)) From 972dff0a1ca356b5622ee92ce43d3ff2ff46ef3a Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 20 Sep 2013 23:47:40 +0200 Subject: [PATCH 46/50] serialize new callback --- test/test_state.py | 33 ++++++++++++++++++++++++ tiramisu/option.py | 63 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/test/test_state.py b/test/test_state.py index 03ab670..ea1956c 100644 --- a/test/test_state.py +++ b/test/test_state.py @@ -3,6 +3,10 @@ from tiramisu.option import BoolOption, UnicodeOption, SymLinkOption, \ from pickle import dumps, loads +def return_value(value=None): + return value + + def _get_slots(opt): slots = set() for subclass in opt.__class__.__mro__: @@ -65,6 +69,18 @@ def _diff_opt(opt1, opt2): for index, consistency in enumerate(val1): assert consistency[0] == val2[index][0] assert consistency[1]._name == val2[index][1]._name + elif attr == '_callback': + assert val1[0] == val2[0] + if val1[1] is not None: + for key, values in val1[1].items(): + for idx, value in enumerate(values): + if isinstance(value, tuple): + assert val1[1][key][idx][0]._name == val2[1][key][idx][0]._name + assert val1[1][key][idx][1] == val2[1][key][idx][1] + else: + assert val1[1][key][idx] == val2[1][key][idx] + else: + assert val1[1] == val2[1] else: assert val1 == val2 @@ -104,6 +120,23 @@ def test_diff_opt_cache(): _diff_opt(o1.o.s, q.o.s) +def test_diff_opt_callback(): + b = BoolOption('b', '', callback=return_value) + b2 = BoolOption('b2', '', callback=return_value, callback_params={'': ('yes',)}) + b3 = BoolOption('b3', '', callback=return_value, callback_params={'': ('yes', (b, False)), 'value': ('no',)}) + o = OptionDescription('o', '', [b, b2, b3]) + o1 = OptionDescription('o1', '', [o]) + o1.impl_build_cache() + + a = dumps(o1) + q = loads(a) + _diff_opt(o1, q) + _diff_opt(o1.o, q.o) + _diff_opt(o1.o.b, q.o.b) + _diff_opt(o1.o.b2, q.o.b2) + _diff_opt(o1.o.b3, q.o.b3) + + def test_no_state_attr(): # all _state_xxx attributes should be deleted b = BoolOption('b', '') diff --git a/tiramisu/option.py b/tiramisu/option.py index 0c7e732..89d960a 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -188,9 +188,11 @@ class BaseOption(object): _list_cons = [] for _con in _cons: if load: - _list_cons.append(descr.impl_get_opt_by_path(_con)) + _list_cons.append( + descr.impl_get_opt_by_path(_con)) else: - _list_cons.append(descr.impl_get_path_by_opt(_con)) + _list_cons.append( + descr.impl_get_path_by_opt(_con)) new_value[key].append((key_cons, tuple(_list_cons))) if load: del(self._state_consistencies) @@ -322,7 +324,8 @@ class Option(BaseOption): Reminder: an Option object is **not** a container for the value. """ __slots__ = ('_multi', '_validator', '_default_multi', '_default', - '_callback', '_multitype', '_master_slaves', '__weakref__') + '_state_callback', '_callback', '_multitype', + '_master_slaves', '__weakref__') _empty = '' def __init__(self, name, doc, default=None, default_multi=None, @@ -558,6 +561,57 @@ class Option(BaseOption): "must be different as {2} option" "").format(value, self._name, optname)) + def _impl_convert_callbacks(self, descr, load=False): + if not load and self._callback is None: + self._state_callback = None + elif load and self._state_callback is None: + self._callback = None + del(self._state_callback) + else: + if load: + callback, callback_params = self._state_callback + else: + callback, callback_params = self._callback + if callback_params is not None: + cllbck_prms = {} + for key, values in callback_params.items(): + vls = [] + for value in values: + if isinstance(value, tuple): + if load: + value = (descr.impl_get_opt_by_path(value[0]), + value[1]) + else: + value = (descr.impl_get_path_by_opt(value[0]), + value[1]) + vls.append(value) + cllbck_prms[key] = tuple(vls) + else: + cllbck_prms = None + + if load: + del(self._state_callback) + self._callback = (callback, cllbck_prms) + else: + self._state_callback = (callback, cllbck_prms) + + # serialize + def _impl_getstate(self, descr): + """the under the hood stuff that need to be done + before the serialization. + """ + self._stated = True + self._impl_convert_callbacks(descr) + super(Option, self)._impl_getstate(descr) + + # unserialize + def _impl_setstate(self, descr): + """the under the hood stuff that need to be done + before the serialization. + """ + self._impl_convert_callbacks(descr, load=True) + super(Option, self)._impl_setstate(descr) + class ChoiceOption(Option): """represents a choice out of several objects. @@ -1287,4 +1341,5 @@ def validate_callback(callback, callback_params, type_): if force_permissive not in [True, False]: raise ValueError(_('{0}_params should have a boolean' 'not a {0} for second argument' - ).format(type_, type(force_permissive))) + ).format(type_, type( + force_permissive))) From c84d13a1c6c59e82d570d6c6fe645f4035528423 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 22 Sep 2013 20:57:52 +0200 Subject: [PATCH 47/50] we can serialize Config now --- test/test_option_setting.py | 18 ++--- test/test_state.py | 96 +++++++++++++++++++++++++ tiramisu/config.py | 42 ++++++++++- tiramisu/option.py | 27 ++----- tiramisu/setting.py | 22 +++++- tiramisu/storage/__init__.py | 25 +++++-- tiramisu/storage/dictionary/__init__.py | 4 +- tiramisu/storage/dictionary/setting.py | 17 +++-- tiramisu/storage/dictionary/storage.py | 12 ++-- tiramisu/storage/dictionary/value.py | 2 +- tiramisu/storage/sqlite3/__init__.py | 4 +- tiramisu/storage/sqlite3/setting.py | 77 +++++++++++--------- tiramisu/storage/sqlite3/sqlite3db.py | 7 +- tiramisu/storage/sqlite3/storage.py | 17 +++-- tiramisu/storage/sqlite3/value.py | 24 +++---- tiramisu/storage/{cache.py => util.py} | 54 +++++++++++++- tiramisu/value.py | 14 +++- 17 files changed, 354 insertions(+), 108 deletions(-) rename tiramisu/storage/{cache.py => util.py} (51%) diff --git a/test/test_option_setting.py b/test/test_option_setting.py index 2dc76ab..ed5f7d7 100644 --- a/test/test_option_setting.py +++ b/test/test_option_setting.py @@ -327,23 +327,23 @@ def test_reset_properties(): cfg = Config(descr) setting = cfg.cfgimpl_get_settings() option = cfg.cfgimpl_get_description().gc.dummy - assert setting._p_.get_properties(cfg) == {} + assert setting._p_.get_modified_properties() == {} setting.append('frozen') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'cache', 'validator'))} + assert setting._p_.get_modified_properties() == {None: set(('frozen', 'expire', 'cache', 'validator'))} setting.reset() - assert setting._p_.get_properties(cfg) == {} + assert setting._p_.get_modified_properties() == {} setting[option].append('test') - assert setting._p_.get_properties(cfg) == {'gc.dummy': set(('test',))} + assert setting._p_.get_modified_properties() == {'gc.dummy': set(('test',))} setting.reset() - assert setting._p_.get_properties(cfg) == {'gc.dummy': set(('test',))} + assert setting._p_.get_modified_properties() == {'gc.dummy': set(('test',))} setting.append('frozen') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} + assert setting._p_.get_modified_properties() == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} setting.reset(option) - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache'))} + assert setting._p_.get_modified_properties() == {None: set(('frozen', 'expire', 'validator', 'cache'))} setting[option].append('test') - assert setting._p_.get_properties(cfg) == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} + assert setting._p_.get_modified_properties() == {None: set(('frozen', 'expire', 'validator', 'cache')), 'gc.dummy': set(('test',))} setting.reset(all_properties=True) - assert setting._p_.get_properties(cfg) == {} + assert setting._p_.get_modified_properties() == {} raises(ValueError, 'setting.reset(all_properties=True, opt=option)') a = descr.wantref setting[a].append('test') diff --git a/test/test_state.py b/test/test_state.py index ea1956c..8587b4a 100644 --- a/test/test_state.py +++ b/test/test_state.py @@ -1,5 +1,9 @@ from tiramisu.option import BoolOption, UnicodeOption, SymLinkOption, \ OptionDescription +from tiramisu.config import Config +from tiramisu.setting import owners +from tiramisu.storage import delete_session +from tiramisu.error import ConfigError from pickle import dumps, loads @@ -152,3 +156,95 @@ def test_no_state_attr(): _no_state(q.o.b) _no_state(q.o.u) _no_state(q.o.s) + + +def test_state_config(): + val1 = BoolOption('val1', "") + maconfig = OptionDescription('rootconfig', '', [val1]) + try: + cfg = Config(maconfig, persistent=True, session_id='29090931') + except ValueError: + cfg = Config(maconfig, session_id='29090931') + cfg._impl_test = True + a = dumps(cfg) + q = loads(a) + _diff_opt(maconfig, q.cfgimpl_get_description()) + assert cfg.cfgimpl_get_values().get_modified_values() == q.cfgimpl_get_values().get_modified_values() + assert cfg.cfgimpl_get_settings().get_modified_properties() == q.cfgimpl_get_settings().get_modified_properties() + assert cfg.cfgimpl_get_settings().get_modified_permissives() == q.cfgimpl_get_settings().get_modified_permissives() + try: + delete_session('29090931') + except ConfigError: + pass + + +def test_state_properties(): + val1 = BoolOption('val1', "") + maconfig = OptionDescription('rootconfig', '', [val1]) + try: + cfg = Config(maconfig, persistent=True, session_id='29090932') + except ValueError: + cfg = Config(maconfig, session_id='29090932') + cfg._impl_test = True + cfg.read_write() + cfg.cfgimpl_get_settings()[val1].append('test') + a = dumps(cfg) + q = loads(a) + _diff_opt(maconfig, q.cfgimpl_get_description()) + assert cfg.cfgimpl_get_values().get_modified_values() == q.cfgimpl_get_values().get_modified_values() + assert cfg.cfgimpl_get_settings().get_modified_properties() == q.cfgimpl_get_settings().get_modified_properties() + assert cfg.cfgimpl_get_settings().get_modified_permissives() == q.cfgimpl_get_settings().get_modified_permissives() + try: + delete_session('29090931') + except ConfigError: + pass + + +def test_state_values(): + val1 = BoolOption('val1', "") + maconfig = OptionDescription('rootconfig', '', [val1]) + try: + cfg = Config(maconfig, persistent=True, session_id='29090933') + except ValueError: + cfg = Config(maconfig, session_id='29090933') + cfg._impl_test = True + cfg.val1 = True + a = dumps(cfg) + q = loads(a) + _diff_opt(maconfig, q.cfgimpl_get_description()) + assert cfg.cfgimpl_get_values().get_modified_values() == q.cfgimpl_get_values().get_modified_values() + assert cfg.cfgimpl_get_settings().get_modified_properties() == q.cfgimpl_get_settings().get_modified_properties() + assert cfg.cfgimpl_get_settings().get_modified_permissives() == q.cfgimpl_get_settings().get_modified_permissives() + q.val1 = False + #assert cfg.val1 is True + assert q.val1 is False + try: + delete_session('29090931') + except ConfigError: + pass + + +def test_state_values_owner(): + val1 = BoolOption('val1', "") + maconfig = OptionDescription('rootconfig', '', [val1]) + try: + cfg = Config(maconfig, persistent=True, session_id='29090934') + except ValueError: + cfg = Config(maconfig, session_id='29090934') + cfg._impl_test = True + owners.addowner('newowner') + cfg.cfgimpl_get_settings().setowner(owners.newowner) + cfg.val1 = True + a = dumps(cfg) + q = loads(a) + _diff_opt(maconfig, q.cfgimpl_get_description()) + assert cfg.cfgimpl_get_values().get_modified_values() == q.cfgimpl_get_values().get_modified_values() + assert cfg.cfgimpl_get_settings().get_modified_properties() == q.cfgimpl_get_settings().get_modified_properties() + assert cfg.cfgimpl_get_settings().get_modified_permissives() == q.cfgimpl_get_settings().get_modified_permissives() + q.val1 = False + nval1 = q.cfgimpl_get_description().val1 + assert q.getowner(nval1) == owners.newowner + try: + delete_session('29090931') + except ConfigError: + pass diff --git a/tiramisu/config.py b/tiramisu/config.py index fac3caf..d817fa1 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -24,7 +24,8 @@ import weakref from tiramisu.error import PropertiesOptionError, ConfigError from tiramisu.option import OptionDescription, Option, SymLinkOption from tiramisu.setting import groups, Settings, default_encoding -from tiramisu.storage import get_storages +from tiramisu.storage import get_storages, get_storage, set_storage, \ + _impl_getstate_setting from tiramisu.value import Values from tiramisu.i18n import _ @@ -521,7 +522,7 @@ class CommonConfig(SubConfig): # ____________________________________________________________ class Config(CommonConfig): "main configuration management entry" - __slots__ = ('__weakref__', ) + __slots__ = ('__weakref__', '_impl_test') def __init__(self, descr, session_id=None, persistent=False): """ Configuration option management master class @@ -542,6 +543,43 @@ class Config(CommonConfig): super(Config, self).__init__(descr, weakref.ref(self)) self._impl_build_all_paths() self._impl_meta = None + #undocumented option used only in test script + self._impl_test = False + + def __getstate__(self): + if self._impl_meta is not None: + raise ConfigError('cannot serialize Config with meta') + slots = set() + for subclass in self.__class__.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + slots -= frozenset(['_impl_context', '__weakref__']) + state = {} + for slot in slots: + try: + state[slot] = getattr(self, slot) + except AttributeError: + pass + storage = self._impl_values._p_._storage + if not storage.serializable: + raise ConfigError('this storage is not serialisable, could be a ' + 'none persistent storage') + state['_storage'] = {'session_id': storage.session_id, + 'persistent': storage.persistent} + state['_impl_setting'] = _impl_getstate_setting() + return state + + def __setstate__(self, state): + for key, value in state.items(): + if key not in ['_storage', '_impl_setting']: + setattr(self, key, value) + set_storage(**state['_impl_setting']) + self._impl_context = weakref.ref(self) + self._impl_settings.context = weakref.ref(self) + self._impl_values.context = weakref.ref(self) + storage = get_storage(test=self._impl_test, **state['_storage']) + self._impl_values._impl_setstate(storage) + self._impl_settings._impl_setstate(storage) def cfgimpl_reset_cache(self, only_expired=False, diff --git a/tiramisu/option.py b/tiramisu/option.py index 89d960a..10aba5c 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -242,8 +242,9 @@ class BaseOption(object): :param descr: the parent :class:`tiramisu.option.OptionDescription` """ self._stated = True - self._impl_convert_consistencies(descr) - self._impl_convert_requires(descr) + for func in dir(self): + if func.startswith('_impl_convert_'): + getattr(self, func)(descr) try: self._state_readonly = self._readonly except AttributeError: @@ -294,8 +295,9 @@ class BaseOption(object): :type descr: :class:`tiramisu.option.OptionDescription` """ - self._impl_convert_consistencies(descr, load=True) - self._impl_convert_requires(descr, load=True) + for func in dir(self): + if func.startswith('_impl_convert_'): + getattr(self, func)(descr, load=True) try: self._readonly = self._state_readonly del(self._state_readonly) @@ -595,23 +597,6 @@ class Option(BaseOption): else: self._state_callback = (callback, cllbck_prms) - # serialize - def _impl_getstate(self, descr): - """the under the hood stuff that need to be done - before the serialization. - """ - self._stated = True - self._impl_convert_callbacks(descr) - super(Option, self)._impl_getstate(descr) - - # unserialize - def _impl_setstate(self, descr): - """the under the hood stuff that need to be done - before the serialization. - """ - self._impl_convert_callbacks(descr, load=True) - super(Option, self)._impl_setstate(descr) - class ChoiceOption(Option): """represents a choice out of several objects. diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 531e846..684ec74 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -105,7 +105,7 @@ rw_remove = set(['permissive', 'everything_frozen', 'mandatory']) # ____________________________________________________________ -class _NameSpace: +class _NameSpace(object): """convenient class that emulates a module and builds constants (that is, unique names) when attribute is added, we cannot delete it @@ -591,3 +591,23 @@ class Settings(object): :returns: path """ return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt) + + def get_modified_properties(self): + return self._p_.get_modified_properties() + + def get_modified_permissives(self): + return self._p_.get_modified_permissives() + + def __getstate__(self): + return {'_p_': self._p_, '_owner': str(self._owner)} + + def _impl_setstate(self, storage): + self._p_._storage = storage + + def __setstate__(self, states): + self._p_ = states['_p_'] + try: + self._owner = getattr(owners, states['_owner']) + except AttributeError: + owners.addowner(states['_owner']) + self._owner = getattr(owners, states['_owner']) diff --git a/tiramisu/storage/__init__.py b/tiramisu/storage/__init__.py index 1394258..c232472 100644 --- a/tiramisu/storage/__init__.py +++ b/tiramisu/storage/__init__.py @@ -68,7 +68,7 @@ class StorageType(object): storage_type = StorageType() -def set_storage(name, **args): +def set_storage(name, **kwargs): """Change storage's configuration :params name: is the storage name. If storage is already set, cannot @@ -77,16 +77,31 @@ def set_storage(name, **args): Other attributes are differents according to the selected storage's name """ storage_type.set(name) - settings = storage_type.get().Setting() - for option, value in args.items(): + setting = storage_type.get().setting + for option, value in kwargs.items(): try: - getattr(settings, option) - setattr(settings, option, value) + getattr(setting, option) + setattr(setting, option, value) except AttributeError: raise ValueError(_('option {0} not already exists in storage {1}' '').format(option, name)) +def _impl_getstate_setting(): + setting = storage_type.get().setting + state = {'name': storage_type.storage_type} + for var in dir(setting): + if not var.startswith('_'): + state[var] = getattr(setting, var) + return state + + +def get_storage(session_id, persistent, test): + """all used when __setstate__ a Config + """ + return storage_type.get().Storage(session_id, persistent, test) + + def get_storages(context, session_id, persistent): def gen_id(config): return str(id(config)) + str(time()) diff --git a/tiramisu/storage/dictionary/__init__.py b/tiramisu/storage/dictionary/__init__.py index dadce23..bc81450 100644 --- a/tiramisu/storage/dictionary/__init__.py +++ b/tiramisu/storage/dictionary/__init__.py @@ -26,6 +26,6 @@ use it. But if something goes wrong, you will lost your modifications. """ from .value import Values from .setting import Settings -from .storage import Setting, Storage, list_sessions, delete_session +from .storage import setting, Storage, list_sessions, delete_session -__all__ = (Setting, Values, Settings, Storage, list_sessions, delete_session) +__all__ = (setting, Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/dictionary/setting.py b/tiramisu/storage/dictionary/setting.py index 706ab2a..1b7001b 100644 --- a/tiramisu/storage/dictionary/setting.py +++ b/tiramisu/storage/dictionary/setting.py @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from ..cache import Cache +from ..util import Cache class Settings(Cache): @@ -50,12 +50,21 @@ class Settings(Cache): except KeyError: pass - def get_properties(self, context): - return self._properties - # permissive def setpermissive(self, path, permissive): self._permissives[path] = frozenset(permissive) def getpermissive(self, path=None): return self._permissives.get(path, frozenset()) + + def get_modified_properties(self): + """return all modified settings in a dictionary + example: {'path1': set(['prop1', 'prop2'])} + """ + return self._properties + + def get_modified_permissives(self): + """return all modified permissives in a dictionary + example: {'path1': set(['perm1', 'perm2'])} + """ + return self._permissives diff --git a/tiramisu/storage/dictionary/storage.py b/tiramisu/storage/dictionary/storage.py index 6e15c1b..465fe26 100644 --- a/tiramisu/storage/dictionary/storage.py +++ b/tiramisu/storage/dictionary/storage.py @@ -18,9 +18,10 @@ # ____________________________________________________________ from tiramisu.i18n import _ from tiramisu.error import ConfigError +from ..util import SerializeObject -class Setting(object): +class Setting(SerializeObject): """Dictionary storage has no particular setting. """ pass @@ -39,15 +40,18 @@ def delete_session(session_id): class Storage(object): - __slots__ = ('session_id', 'values', 'settings') + __slots__ = ('session_id', 'persistent') storage = 'dictionary' + #if object could be serializable + serializable = True - def __init__(self, session_id, persistent): - if session_id in _list_sessions: + def __init__(self, session_id, persistent, test=False): + if not test and session_id in _list_sessions: raise ValueError(_('session already used')) if persistent: raise ValueError(_('a dictionary cannot be persistent')) self.session_id = session_id + self.persistent = persistent _list_sessions.append(self.session_id) def __del__(self): diff --git a/tiramisu/storage/dictionary/value.py b/tiramisu/storage/dictionary/value.py index c435d06..fedf1ec 100644 --- a/tiramisu/storage/dictionary/value.py +++ b/tiramisu/storage/dictionary/value.py @@ -18,7 +18,7 @@ # # ____________________________________________________________ -from ..cache import Cache +from ..util import Cache class Values(Cache): diff --git a/tiramisu/storage/sqlite3/__init__.py b/tiramisu/storage/sqlite3/__init__.py index dc6c14b..8d79070 100644 --- a/tiramisu/storage/sqlite3/__init__.py +++ b/tiramisu/storage/sqlite3/__init__.py @@ -24,6 +24,6 @@ You should not configure differents Configs with same session_id. """ from .value import Values from .setting import Settings -from .storage import Setting, Storage, list_sessions, delete_session +from .storage import setting, Storage, list_sessions, delete_session -__all__ = (Setting, Values, Settings, Storage, list_sessions, delete_session) +__all__ = (setting, Values, Settings, Storage, list_sessions, delete_session) diff --git a/tiramisu/storage/sqlite3/setting.py b/tiramisu/storage/sqlite3/setting.py index 720849b..ed79181 100644 --- a/tiramisu/storage/sqlite3/setting.py +++ b/tiramisu/storage/sqlite3/setting.py @@ -30,22 +30,22 @@ class Settings(Sqlite3DB): permissives_table += 'primary key, permissives text)' # should init cache too super(Settings, self).__init__(storage) - self.storage.execute(settings_table, commit=False) - self.storage.execute(permissives_table) + self._storage.execute(settings_table, commit=False) + self._storage.execute(permissives_table) # propertives def setproperties(self, path, properties): path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM property WHERE path = ?", (path,), - False) - self.storage.execute("INSERT INTO property(path, properties) VALUES " - "(?, ?)", (path, - self._sqlite_encode(properties))) + self._storage.execute("DELETE FROM property WHERE path = ?", (path,), + False) + self._storage.execute("INSERT INTO property(path, properties) VALUES " + "(?, ?)", (path, + self._sqlite_encode(properties))) def getproperties(self, path, default_properties): path = self._sqlite_encode_path(path) - value = self.storage.select("SELECT properties FROM property WHERE " - "path = ?", (path,)) + value = self._storage.select("SELECT properties FROM property WHERE " + "path = ?", (path,)) if value is None: return set(default_properties) else: @@ -53,42 +53,53 @@ class Settings(Sqlite3DB): def hasproperties(self, path): path = self._sqlite_encode_path(path) - return self.storage.select("SELECT properties FROM property WHERE " - "path = ?", (path,)) is not None + return self._storage.select("SELECT properties FROM property WHERE " + "path = ?", (path,)) is not None def reset_all_propertives(self): - self.storage.execute("DELETE FROM property") + self._storage.execute("DELETE FROM property") def reset_properties(self, path): path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM property WHERE path = ?", (path,)) - - def get_properties(self, context): - """return all properties in a dictionary - """ - ret = {} - for path, properties in self.storage.select("SELECT * FROM property", - only_one=False): - path = self._sqlite_decode_path(path) - properties = self._sqlite_decode(properties) - ret[path] = properties - return ret + self._storage.execute("DELETE FROM property WHERE path = ?", (path,)) # permissive def setpermissive(self, path, permissive): path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM permissive WHERE path = ?", (path,), - False) - self.storage.execute("INSERT INTO permissive(path, permissives) " - "VALUES (?, ?)", (path, - self._sqlite_encode(permissive) - )) + self._storage.execute("DELETE FROM permissive WHERE path = ?", (path,), + False) + self._storage.execute("INSERT INTO permissive(path, permissives) " + "VALUES (?, ?)", (path, + self._sqlite_encode(permissive) + )) def getpermissive(self, path='_none'): - permissives = self.storage.select("SELECT permissives FROM " - "permissive WHERE path = ?", - (path,)) + permissives = self._storage.select("SELECT permissives FROM " + "permissive WHERE path = ?", + (path,)) if permissives is None: return frozenset() else: return frozenset(self._sqlite_decode(permissives[0])) + + def get_modified_properties(self): + """return all modified settings in a dictionary + example: {'path1': set(['prop1', 'prop2'])} + """ + ret = {} + for path, properties in self._storage.select("SELECT * FROM property", + only_one=False): + path = self._sqlite_decode_path(path) + ret[path] = self._sqlite_decode(properties) + return ret + + def get_modified_permissives(self): + """return all modified permissives in a dictionary + example: {'path1': set(['perm1', 'perm2'])} + """ + ret = {} + for path, permissives in self._storage.select("SELECT * FROM permissive", + only_one=False): + path = self._sqlite_decode_path(path) + ret[path] = self._sqlite_decode(permissives) + return ret diff --git a/tiramisu/storage/sqlite3/sqlite3db.py b/tiramisu/storage/sqlite3/sqlite3db.py index 9a967cd..68f2886 100644 --- a/tiramisu/storage/sqlite3/sqlite3db.py +++ b/tiramisu/storage/sqlite3/sqlite3db.py @@ -17,8 +17,11 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ -from cPickle import loads, dumps -from ..cache import Cache +try: + from cPickle import loads, dumps +except ImportError: + from pickle import loads, dumps +from ..util import Cache class Sqlite3DB(Cache): diff --git a/tiramisu/storage/sqlite3/storage.py b/tiramisu/storage/sqlite3/storage.py index 2ab8e08..3b4f265 100644 --- a/tiramisu/storage/sqlite3/storage.py +++ b/tiramisu/storage/sqlite3/storage.py @@ -22,9 +22,10 @@ from os import unlink from os.path import basename, splitext, join import sqlite3 from glob import glob +from ..util import SerializeObject -class Setting(object): +class Setting(SerializeObject): """:param extension: database file extension (by default: db) :param dir_database: root database directory (by default: /tmp) """ @@ -52,13 +53,17 @@ def delete_session(session_id): class Storage(object): - __slots__ = ('_conn', '_cursor', 'persistent', '_session_id') + __slots__ = ('_conn', '_cursor', 'persistent', 'session_id', 'serializable') storage = 'sqlite3' - def __init__(self, session_id, persistent): + def __init__(self, session_id, persistent, test=False): self.persistent = persistent - self._session_id = session_id - self._conn = sqlite3.connect(_gen_filename(self._session_id)) + if self.persistent: + self.serializable = True + else: + self.serializable = False + self.session_id = session_id + self._conn = sqlite3.connect(_gen_filename(self.session_id)) self._conn.text_factory = str self._cursor = self._conn.cursor() @@ -80,4 +85,4 @@ class Storage(object): self._cursor.close() self._conn.close() if not self.persistent: - delete_session(self._session_id) + delete_session(self.session_id) diff --git a/tiramisu/storage/sqlite3/value.py b/tiramisu/storage/sqlite3/value.py index 3f76e2c..672ecab 100644 --- a/tiramisu/storage/sqlite3/value.py +++ b/tiramisu/storage/sqlite3/value.py @@ -32,11 +32,11 @@ class Values(Sqlite3DB): super(Values, self).__init__(storage) values_table = 'CREATE TABLE IF NOT EXISTS value(path text primary ' values_table += 'key, value text, owner text)' - self.storage.execute(values_table, commit=False) + self._storage.execute(values_table, commit=False) informations_table = 'CREATE TABLE IF NOT EXISTS information(key text primary ' informations_table += 'key, value text)' - self.storage.execute(informations_table) - for owner in self.storage.select("SELECT DISTINCT owner FROM value", tuple(), False): + self._storage.execute(informations_table) + for owner in self._storage.select("SELECT DISTINCT owner FROM value", tuple(), False): try: getattr(owners, owner[0]) except AttributeError: @@ -44,7 +44,7 @@ class Values(Sqlite3DB): # sqlite def _sqlite_select(self, path): - return self.storage.select("SELECT value FROM value WHERE path = ?", + return self._storage.select("SELECT value FROM value WHERE path = ?", (path,)) # value @@ -54,7 +54,7 @@ class Values(Sqlite3DB): """ self.resetvalue(path) path = self._sqlite_encode_path(path) - self.storage.execute("INSERT INTO value(path, value, owner) VALUES " + self._storage.execute("INSERT INTO value(path, value, owner) VALUES " "(?, ?, ?)", (path, self._sqlite_encode(value), str(owner))) @@ -76,14 +76,14 @@ class Values(Sqlite3DB): """remove value means delete value in storage """ path = self._sqlite_encode_path(path) - self.storage.execute("DELETE FROM value WHERE path = ?", (path,)) + self._storage.execute("DELETE FROM value WHERE path = ?", (path,)) def get_modified_values(self): """return all values in a dictionary example: {option1: (owner, 'value1'), option2: (owner, 'value2')} """ ret = {} - for path, value, owner in self.storage.select("SELECT * FROM value", + for path, value, owner in self._storage.select("SELECT * FROM value", only_one=False): path = self._sqlite_decode_path(path) owner = getattr(owners, owner) @@ -97,7 +97,7 @@ class Values(Sqlite3DB): """change owner for an option """ path = self._sqlite_encode_path(path) - self.storage.execute("UPDATE value SET owner = ? WHERE path = ?", + self._storage.execute("UPDATE value SET owner = ? WHERE path = ?", (str(owner), path)) def getowner(self, path, default): @@ -105,7 +105,7 @@ class Values(Sqlite3DB): return: owner object """ path = self._sqlite_encode_path(path) - owner = self.storage.select("SELECT owner FROM value WHERE path = ?", + owner = self._storage.select("SELECT owner FROM value WHERE path = ?", (path,)) if owner is None: return default @@ -125,9 +125,9 @@ class Values(Sqlite3DB): :param key: information's key (ex: "help", "doc" :param value: information's value (ex: "the help string") """ - self.storage.execute("DELETE FROM information WHERE key = ?", (key,), + self._storage.execute("DELETE FROM information WHERE key = ?", (key,), False) - self.storage.execute("INSERT INTO information(key, value) VALUES " + self._storage.execute("INSERT INTO information(key, value) VALUES " "(?, ?)", (key, self._sqlite_encode(value))) def get_information(self, key): @@ -135,7 +135,7 @@ class Values(Sqlite3DB): :param key: the item string (ex: "help") """ - value = self.storage.select("SELECT value FROM information WHERE key = ?", + value = self._storage.select("SELECT value FROM information WHERE key = ?", (key,)) if value is None: raise ValueError("not found") diff --git a/tiramisu/storage/cache.py b/tiramisu/storage/util.py similarity index 51% rename from tiramisu/storage/cache.py rename to tiramisu/storage/util.py index 347d270..68482e6 100644 --- a/tiramisu/storage/cache.py +++ b/tiramisu/storage/util.py @@ -17,15 +17,65 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ____________________________________________________________ +from tiramisu.setting import owners + + +class SerializeObject(object): + def __getstate__(self): + ret = {} + for key in dir(self): + if not key.startswith('__'): + ret[key] = getattr(self, key) + return ret class Cache(object): - __slots__ = ('_cache', 'storage') + __slots__ = ('_cache', '_storage') key_is_path = False def __init__(self, storage): self._cache = {} - self.storage = storage + self._storage = storage + + def __getstate__(self): + slots = set() + for subclass in self.__class__.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + slots -= frozenset(['__weakref__', '_storage']) + states = {} + for slot in slots: + try: + value = getattr(self, slot) + #value has owners object, need 'str()' it + if slot == '_values': + _value = {} + for key, values in value.items(): + vals = list(values) + vals[0] = str(vals[0]) + _value[key] = tuple(vals) + states[slot] = _value + else: + states[slot] = value + except AttributeError: + pass + return states + + def __setstate__(self, states): + for key, value in states.items(): + #value has owners object, need to reconstruct it + if key == '_values': + _value = {} + for key_, values_ in value.items(): + vals = list(values_) + try: + vals[0] = getattr(owners, vals[0]) + except AttributeError: + owners.addowner(vals[0]) + vals[0] = getattr(owners, vals[0]) + _value[key_] = tuple(vals) + value = _value + setattr(self, key, value) def setcache(self, path, val, time): self._cache[path] = (val, time) diff --git a/tiramisu/value.py b/tiramisu/value.py index c6f0e76..043f15f 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -204,7 +204,9 @@ class Values(object): if not no_value_slave: try: value = self._getcallback_value(opt, max_len=lenmaster) - except ConfigError as config_error: + except ConfigError as err: + # cannot assign config_err directly in python 3.3 + config_error = err value = None # should not raise PropertiesOptionError if option is # mandatory @@ -359,6 +361,14 @@ class Values(object): raise ValueError(_("information's item" " not found: {0}").format(key)) + def __getstate__(self): + return {'_p_': self._p_} + + def _impl_setstate(self, storage): + self._p_._storage = storage + + def __setstate__(self, states): + self._p_ = states['_p_'] # ____________________________________________________________ # multi types @@ -386,7 +396,7 @@ class Multi(list): value = [value] if validate and self.opt.impl_get_multitype() == multitypes.slave: value = self._valid_slave(value, setitem) - elif self.opt.impl_get_multitype() == multitypes.master: + elif validate and self.opt.impl_get_multitype() == multitypes.master: self._valid_master(value) super(Multi, self).__init__(value) From 051f1c877414c5793c4905aa529593ffc3b80ea0 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 22 Sep 2013 21:23:12 +0200 Subject: [PATCH 48/50] tiramisu/config.py: - find byvalue support Multi tiramisu/value.py: - Multi's pop comment --- tiramisu/config.py | 11 ++++++----- tiramisu/value.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tiramisu/config.py b/tiramisu/config.py index d817fa1..261f6ed 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -26,7 +26,7 @@ from tiramisu.option import OptionDescription, Option, SymLinkOption from tiramisu.setting import groups, Settings, default_encoding from tiramisu.storage import get_storages, get_storage, set_storage, \ _impl_getstate_setting -from tiramisu.value import Values +from tiramisu.value import Values, Multi from tiramisu.i18n import _ @@ -294,12 +294,13 @@ class SubConfig(object): return True try: value = getattr(self, path) - if value == byvalue: - return True + if isinstance(value, Multi): + return byvalue in value + else: + return value == byvalue except PropertiesOptionError: # a property is a restriction # upon the access of the value - pass - return False + return False def _filter_by_type(): if bytype is None: diff --git a/tiramisu/value.py b/tiramisu/value.py index 043f15f..d587de1 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -544,12 +544,15 @@ class Multi(list): "").format(str(value), self.opt._name, err)) - def pop(self, key, force=False): + def pop(self, index, force=False): """the list value can be updated (poped) only if the option is a master - :param key: index of the element to pop - :return: the requested element + :param index: remove item a index + :type index: int + :param force: force pop item (withoud check master/slave) + :type force: boolean + :returns: item at index """ if not force: if self.opt.impl_get_multitype() == multitypes.slave: @@ -562,8 +565,8 @@ class Multi(list): #get multi without valid properties values.getitem(slave, validate_properties=False - ).pop(key, force=True) + ).pop(index, force=True) #set value without valid properties - ret = super(Multi, self).pop(key) + ret = super(Multi, self).pop(index) self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self, validate_properties=not force) return ret From ff7714d8d319aa86f9b9ed82a8bf6af3499feafd Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 22 Sep 2013 21:31:37 +0200 Subject: [PATCH 49/50] add find test value in a multi's option --- test/test_config_api.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_config_api.py b/test/test_config_api.py index ba268bc..ab4b484 100644 --- a/test/test_config_api.py +++ b/test/test_config_api.py @@ -116,6 +116,23 @@ def test_find_in_config(): #assert conf.find_first(byvalue=False, byname='dummy', byattrs=dict(default=False)) == conf.unwrap_from_path('gc.dummy') +def test_find_multi(): + b = BoolOption('bool', '', multi=True) + o = OptionDescription('od', '', [b]) + conf = Config(o) + raises(AttributeError, "conf.find(byvalue=True)") + raises(AttributeError, "conf.find_first(byvalue=True)") + conf.bool.append(False) + raises(AttributeError, "conf.find(byvalue=True)") + raises(AttributeError, "conf.find_first(byvalue=True)") + conf.bool.append(False) + raises(AttributeError, "conf.find(byvalue=True)") + raises(AttributeError, "conf.find_first(byvalue=True)") + conf.bool.append(True) + assert conf.find(byvalue=True) == [b] + assert conf.find_first(byvalue=True) == b + + def test_does_not_find_in_config(): descr = make_description() conf = Config(descr) From d2f101b7bbd99375c1c85e2ff32a3c067bea1e67 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 22 Sep 2013 21:54:07 +0200 Subject: [PATCH 50/50] didnot getattr a second time in find if not needed --- tiramisu/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tiramisu/config.py b/tiramisu/config.py index 261f6ed..f1b2851 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -325,15 +325,15 @@ class SubConfig(object): continue if not _filter_by_value(): continue + if not _filter_by_type(): + continue #remove option with propertyerror, ... - if check_properties: + if byvalue is None and check_properties: try: value = getattr(self, path) except PropertiesOptionError: # a property restricts the access of the value continue - if not _filter_by_type(): - continue if type_ == 'value': retval = value elif type_ == 'path':