1import yaml
2
3from conans.errors import ConanException
4from conans.model.values import Values
5
6
7def bad_value_msg(name, value, value_range):
8    tip = ""
9    if "settings" in name:
10        tip = '\nRead "http://docs.conan.io/en/latest/faq/troubleshooting.html' \
11              '#error-invalid-setting"'
12
13    return ("Invalid setting '%s' is not a valid '%s' value.\nPossible values are %s%s"
14            % (value, name, value_range, tip))
15
16
17def undefined_field(name, field, fields=None, value=None):
18    value_str = " for '%s'" % value if value else ""
19    result = ["'%s.%s' doesn't exist%s" % (name, field, value_str),
20              "'%s' possible configurations are %s" % (name, fields or "none")]
21    return ConanException("\n".join(result))
22
23
24def undefined_value(name):
25    return ConanException("'%s' value not defined" % name)
26
27
28class SettingsItem(object):
29    """ represents a setting value and its child info, which could be:
30    - A range of valid values: [Debug, Release] (for settings.compiler.runtime of VS)
31    - "ANY", as string to accept any value
32    - List ["None", "ANY"] to accept None or any value
33    - A dict {subsetting: definition}, e.g. {version: [], runtime: []} for VS
34    """
35    def __init__(self, definition, name):
36        self._name = name  # settings.compiler
37        self._value = None  # gcc
38        if isinstance(definition, dict):
39            self._definition = {}
40            # recursive
41            for k, v in definition.items():
42                k = str(k)
43                self._definition[k] = Settings(v, name, k)
44        elif definition == "ANY":
45            self._definition = "ANY"
46        else:
47            # list or tuple of possible values
48            self._definition = [str(v) for v in definition]
49
50    def __contains__(self, value):
51        return value in (self._value or "")
52
53    def copy(self):
54        """ deepcopy, recursive
55        """
56        result = SettingsItem({}, name=self._name)
57        result._value = self._value
58        if self.is_final:
59            result._definition = self._definition[:]
60        else:
61            result._definition = {k: v.copy() for k, v in self._definition.items()}
62        return result
63
64    def copy_values(self):
65        if self._value is None and "None" not in self._definition:
66            return None
67
68        result = SettingsItem({}, name=self._name)
69        result._value = self._value
70        if self.is_final:
71            result._definition = self._definition[:]
72        else:
73            result._definition = {k: v.copy_values() for k, v in self._definition.items()}
74        return result
75
76    @property
77    def is_final(self):
78        return not isinstance(self._definition, dict)
79
80    def __bool__(self):
81        if not self._value:
82            return False
83        return self._value.lower() not in ["false", "none", "0", "off"]
84
85    def __nonzero__(self):
86        return self.__bool__()
87
88    def __str__(self):
89        return str(self._value)
90
91    def _not_any(self):
92        return self._definition != "ANY" and "ANY" not in self._definition
93
94    def __eq__(self, other):
95        if other is None:
96            return self._value is None
97        other = str(other)
98        if self._not_any() and other not in self.values_range:
99            raise ConanException(bad_value_msg(self._name, other, self.values_range))
100        return other == self.__str__()
101
102    def __ne__(self, other):
103        return not self.__eq__(other)
104
105    def __delattr__(self, item):
106        """ This is necessary to remove libcxx subsetting from compiler in config()
107           del self.settings.compiler.stdlib
108        """
109        try:
110            self._get_child(self._value).remove(item)
111        except Exception:
112            pass
113
114    def remove(self, values):
115        if not isinstance(values, (list, tuple, set)):
116            values = [values]
117        for v in values:
118            v = str(v)
119            if isinstance(self._definition, dict):
120                self._definition.pop(v, None)
121            elif self._definition == "ANY":
122                if v == "ANY":
123                    self._definition = []
124            elif v in self._definition:
125                self._definition.remove(v)
126
127        if self._value is not None and self._value not in self._definition and self._not_any():
128            raise ConanException(bad_value_msg(self._name, self._value, self.values_range))
129
130    def _get_child(self, item):
131        if not isinstance(self._definition, dict):
132            raise undefined_field(self._name, item, None, self._value)
133        if self._value is None:
134            raise undefined_value(self._name)
135        return self._definition[self._value]
136
137    def __getattr__(self, item):
138        item = str(item)
139        sub_config_dict = self._get_child(item)
140        return getattr(sub_config_dict, item)
141
142    def __setattr__(self, item, value):
143        if item[0] == "_" or item.startswith("value"):
144            return super(SettingsItem, self).__setattr__(item, value)
145
146        item = str(item)
147        sub_config_dict = self._get_child(item)
148        return setattr(sub_config_dict, item, value)
149
150    def __getitem__(self, value):
151        value = str(value)
152        try:
153            return self._definition[value]
154        except Exception:
155            raise ConanException(bad_value_msg(self._name, value, self.values_range))
156
157    @property
158    def value(self):
159        return self._value
160
161    @value.setter
162    def value(self, v):
163        v = str(v)
164        if self._not_any() and v not in self.values_range:
165            raise ConanException(bad_value_msg(self._name, v, self.values_range))
166        self._value = v
167
168    @property
169    def values_range(self):
170        try:
171            return sorted(list(self._definition.keys()))
172        except Exception:
173            return self._definition
174
175    @property
176    def values_list(self):
177        if self._value is None:
178            return []
179        result = []
180        partial_name = ".".join(self._name.split(".")[1:])
181        result.append((partial_name, self._value))
182        if isinstance(self._definition, dict):
183            sub_config_dict = self._definition[self._value]
184            result.extend(sub_config_dict.values_list)
185        return result
186
187    def validate(self):
188        if self._value is None and "None" not in self._definition:
189            raise undefined_value(self._name)
190        if isinstance(self._definition, dict):
191            key = "None" if self._value is None else self._value
192            self._definition[key].validate()
193
194
195class Settings(object):
196    def __init__(self, definition=None, name="settings", parent_value=None):
197        if parent_value == "None" and definition:
198            raise ConanException("settings.yml: None setting can't have subsettings")
199        definition = definition or {}
200        self._name = name  # settings, settings.compiler
201        self._parent_value = parent_value  # gcc, x86
202        self._data = {str(k): SettingsItem(v, "%s.%s" % (name, k))
203                      for k, v in definition.items()}
204
205    def get_safe(self, name, default=None):
206        try:
207            tmp = self
208            for prop in name.split("."):
209                tmp = getattr(tmp, prop, None)
210        except ConanException:
211            return default
212        if tmp is not None and tmp.value and tmp.value != "None":  # In case of subsettings is None
213            return str(tmp)
214        return default
215
216    def copy(self):
217        """ deepcopy, recursive
218        """
219        result = Settings({}, name=self._name, parent_value=self._parent_value)
220        for k, v in self._data.items():
221            result._data[k] = v.copy()
222        return result
223
224    def copy_values(self):
225        """ deepcopy, recursive
226        """
227        result = Settings({}, name=self._name, parent_value=self._parent_value)
228        for k, v in self._data.items():
229            value = v.copy_values()
230            if value is not None:
231                result._data[k] = value
232        return result
233
234    @staticmethod
235    def loads(text):
236        try:
237            return Settings(yaml.safe_load(text) or {})
238        except (yaml.YAMLError, AttributeError) as ye:
239            raise ConanException("Invalid settings.yml format: {}".format(ye))
240
241    def validate(self):
242        for field in self.fields:
243            child = self._data[field]
244            child.validate()
245
246    @property
247    def fields(self):
248        return sorted(list(self._data.keys()))
249
250    def remove(self, item):
251        if not isinstance(item, (list, tuple, set)):
252            item = [item]
253        for it in item:
254            it = str(it)
255            self._data.pop(it, None)
256
257    def clear(self):
258        self._data = {}
259
260    def _check_field(self, field):
261        if field not in self._data:
262            raise undefined_field(self._name, field, self.fields, self._parent_value)
263
264    def __getattr__(self, field):
265        assert field[0] != "_", "ERROR %s" % field
266        self._check_field(field)
267        return self._data[field]
268
269    def __delattr__(self, field):
270        assert field[0] != "_", "ERROR %s" % field
271        self._check_field(field)
272        del self._data[field]
273
274    def __setattr__(self, field, value):
275        if field[0] == "_" or field.startswith("values"):
276            return super(Settings, self).__setattr__(field, value)
277
278        self._check_field(field)
279        self._data[field].value = value
280
281    @property
282    def values(self):
283        return Values.from_list(self.values_list)
284
285    @property
286    def values_list(self):
287        result = []
288        for field in self.fields:
289            config_item = self._data[field]
290            result.extend(config_item.values_list)
291        return result
292
293    def items(self):
294        return self.values_list
295
296    def iteritems(self):
297        return self.values_list
298
299    def update_values(self, vals):
300        """ receives a list of tuples (compiler.version, value)
301        This is more an updated than a setter
302        """
303        assert isinstance(vals, list), vals
304        for (name, value) in vals:
305            list_settings = name.split(".")
306            attr = self
307            for setting in list_settings[:-1]:
308                attr = getattr(attr, setting)
309            setattr(attr, list_settings[-1], str(value))
310
311    @values.setter
312    def values(self, vals):
313        assert isinstance(vals, Values)
314        self.update_values(vals.as_list())
315
316    def constraint(self, constraint_def):
317        """ allows to restrict a given Settings object with the input of another Settings object
318        1. The other Settings object MUST be exclusively a subset of the former.
319           No additions allowed
320        2. If the other defines {"compiler": None} means to keep the full specification
321        """
322        if isinstance(constraint_def, (list, tuple, set)):
323            constraint_def = {str(k): None for k in constraint_def or []}
324        else:
325            constraint_def = {str(k): v for k, v in constraint_def.items()}
326
327        fields_to_remove = []
328        for field, config_item in self._data.items():
329            if field not in constraint_def:
330                fields_to_remove.append(field)
331                continue
332
333            other_field_def = constraint_def[field]
334            if other_field_def is None:  # Means leave it as is
335                continue
336            if isinstance(other_field_def, str):
337                other_field_def = [other_field_def]
338
339            values_to_remove = []
340            for value in config_item.values_range:  # value = "Visual Studio"
341                if value not in other_field_def:
342                    values_to_remove.append(value)
343                else:  # recursion
344                    if (not config_item.is_final and isinstance(other_field_def, dict) and
345                            other_field_def[value] is not None):
346                        config_item[value].constraint(other_field_def[value])
347
348            # Sanity check of input constraint values
349            for value in other_field_def:
350                if value not in config_item.values_range:
351                    raise ConanException(bad_value_msg(field, value, config_item.values_range))
352
353            config_item.remove(values_to_remove)
354
355        # Sanity check for input constraint wrong fields
356        for field in constraint_def:
357            if field not in self._data:
358                raise undefined_field(self._name, field, self.fields)
359
360        # remove settings not defined in the constraint
361        self.remove(fields_to_remove)
362