Fix memoize decorator: raise instead of storing None
[leap_pycommon.git] / src / leap / common / config / baseconfig.py
1 # -*- coding: utf-8 -*-
2 # baseconfig.py
3 # Copyright (C) 2013 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 """
19 Implements the abstract base class for configuration
20 """
21
22 import copy
23 import logging
24 import functools
25 import os
26
27 from abc import ABCMeta, abstractmethod
28
29 from leap.common.check import leap_assert, leap_check
30 from leap.common.files import mkdir_p
31 from leap.common.config.pluggableconfig import PluggableConfig
32 from leap.common.config import get_path_prefix
33
34 logger = logging.getLogger(__name__)
35
36
37 class NonExistingSchema(Exception):
38     """
39     Raised if the schema needed to verify the config is None.
40     """
41
42
43 class BaseConfig:
44     """
45     Abstract base class for any JSON based configuration.
46     """
47
48     __metaclass__ = ABCMeta
49
50     """
51     Standalone is a class wide parameter.
52
53     :param standalone: if True it will return the prefix for a
54                        standalone application. Otherwise, it will
55                        return the system
56                        default for configuration storage.
57     :type standalone: bool
58     """
59     standalone = False
60
61     def __init__(self):
62         self._data = {}
63         self._config_checker = None
64         self._api_version = None
65
66     @abstractmethod
67     def _get_schema(self):
68         """
69         Returns the schema corresponding to the version given.
70
71         :rtype: dict or None if the version is not supported.
72         """
73         pass
74
75     def _get_spec(self):
76         """
77         Returns the spec object for the specific configuration.
78
79         :rtype: dict or None if the version is not supported.
80         """
81         leap_assert(self._api_version is not None,
82                     "You should set the API version.")
83
84         return self._get_schema()
85
86     def _safe_get_value(self, key):
87         """
88         Tries to return a value only if the config has already been loaded.
89
90         :rtype: depends on the config structure, dict, str, array, int
91         :return: returns the value for the specified key in the config
92         """
93         leap_assert(self._config_checker, "Load the config first")
94         return self._config_checker.config.get(key, None)
95
96     def set_api_version(self, version):
97         """
98         Sets the supported api version.
99
100         :param api_version: the version of the api supported by the provider.
101         :type api_version: str
102         """
103         self._api_version = version
104         leap_assert(self._get_schema() is not None,
105                     "Version %s is not supported." % (version, ))
106
107     def get_path_prefix(self):
108         """
109         Returns the platform dependant path prefixer
110         """
111         return get_path_prefix(standalone=self.standalone)
112
113     def loaded(self):
114         """
115         Returns True if the configuration has been already
116         loaded. False otherwise
117         """
118         return self._config_checker is not None
119
120     def save(self, path_list):
121         """
122         Saves the current configuration to disk.
123
124         :param path_list: list of components that form the relative
125                           path to configuration. The absolute path
126                           will be calculated depending on the platform.
127         :type path_list: list
128
129         :return: True if saved to disk correctly, False otherwise
130         """
131         config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1]))
132         mkdir_p(config_path)
133
134         try:
135             self._config_checker.serialize(os.path.join(config_path,
136                                                         path_list[-1]))
137         except Exception as e:
138             logger.warning("%s" % (e,))
139             raise
140         return True
141
142     def load(self, path="", data=None, mtime=None, relative=True):
143         """
144         Loads the configuration from disk.
145         It may raise NonExistingSchema exception.
146
147         :param path: if relative=True, this is a relative path
148                      to configuration. The absolute path
149                      will be calculated depending on the platform
150         :type path: str
151
152         :param relative: if True, path is relative. If False, it's absolute.
153         :type relative: bool
154
155         :return: True if loaded from disk correctly, False otherwise
156         :rtype: bool
157         """
158
159         if relative is True:
160             config_path = os.path.join(
161                 self.get_path_prefix(), path)
162         else:
163             config_path = path
164
165         schema = self._get_spec()
166         leap_check(schema is not None,
167                    "There is no schema to use.", NonExistingSchema)
168
169         self._config_checker = PluggableConfig(format="json")
170         self._config_checker.options = copy.deepcopy(schema)
171
172         try:
173             if data is None:
174                 self._config_checker.load(fromfile=config_path, mtime=mtime)
175             else:
176                 self._config_checker.load(data, mtime=mtime)
177         except Exception as e:
178             logger.error("Something went wrong while loading " +
179                          "the config from %s\n%s" % (config_path, e))
180             self._config_checker = None
181             return False
182         return True
183
184
185 class LocalizedKey(object):
186     """
187     Decorator used for keys that are localized in a configuration.
188     """
189
190     def __init__(self, func, **kwargs):
191         self._func = func
192
193     def __call__(self, instance, lang=None):
194         """
195         Tries to return the string for the specified language, otherwise
196         returns the default language string.
197
198         :param lang: language code
199         :type lang: str
200
201         :return: localized value from the possible values returned by
202                  self._func
203                  It returns None in case that the provider does not provides
204                  a matching pair of default_language and string for
205                  that language.
206                  e.g.:
207                      'default_language': 'es',
208                      'description': {'en': 'test description'}
209                 Note that the json schema can't check that.
210         """
211         descriptions = self._func(instance)
212         config_lang = instance.get_default_language()
213         if lang is None:
214             lang = config_lang
215
216         for key in descriptions.keys():
217             if lang.startswith(key):
218                 config_lang = key
219                 break
220
221         description_lang = descriptions.get(config_lang)
222         if description_lang is None:
223             logger.error("There is a misconfiguration in the "
224                          "provider's language strings.")
225
226         return description_lang
227
228     def __get__(self, instance, instancetype):
229         """
230         Implement the descriptor protocol to make decorating instance
231         method possible.
232         """
233         # Return a partial function with the first argument is the instance
234         # of the class decorated.
235         return functools.partial(self.__call__, instance)
236
237 if __name__ == "__main__":
238     try:
239         config = BaseConfig()  # should throw TypeError for _get_spec
240     except Exception as e:
241         assert isinstance(e, TypeError), "Something went wrong"
242         print "Abstract BaseConfig class is working as expected"