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