1# Copyright 2013-2021 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from .. import mesonlib, mlog 16from .baseobjects import TV_func, TYPE_var, TYPE_kwargs 17from .disabler import Disabler 18from .exceptions import InterpreterException, InvalidArguments 19from .operator import MesonOperator 20from ._unholder import _unholder 21 22from functools import wraps 23import abc 24import itertools 25import copy 26import typing as T 27if T.TYPE_CHECKING: 28 from .. import mparser 29 30def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', str]: 31 # First argument could be InterpreterBase, InterpreterObject or ModuleObject. 32 # In the case of a ModuleObject it is the 2nd argument (ModuleState) that 33 # contains the needed information. 34 s = wrapped_args[0] 35 if not hasattr(s, 'current_node'): 36 s = wrapped_args[1] 37 node = s.current_node 38 subproject = s.subproject 39 args = kwargs = None 40 if len(wrapped_args) >= 3: 41 args = wrapped_args[-2] 42 kwargs = wrapped_args[-1] 43 return node, args, kwargs, subproject 44 45def noPosargs(f: TV_func) -> TV_func: 46 @wraps(f) 47 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 48 args = get_callee_args(wrapped_args)[1] 49 if args: 50 raise InvalidArguments('Function does not take positional arguments.') 51 return f(*wrapped_args, **wrapped_kwargs) 52 return T.cast(TV_func, wrapped) 53 54def noKwargs(f: TV_func) -> TV_func: 55 @wraps(f) 56 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 57 kwargs = get_callee_args(wrapped_args)[2] 58 if kwargs: 59 raise InvalidArguments('Function does not take keyword arguments.') 60 return f(*wrapped_args, **wrapped_kwargs) 61 return T.cast(TV_func, wrapped) 62 63def stringArgs(f: TV_func) -> TV_func: 64 @wraps(f) 65 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 66 args = get_callee_args(wrapped_args)[1] 67 if not isinstance(args, list): 68 mlog.debug('Not a list:', str(args)) 69 raise InvalidArguments('Argument not a list.') 70 if not all(isinstance(s, str) for s in args): 71 mlog.debug('Element not a string:', str(args)) 72 raise InvalidArguments('Arguments must be strings.') 73 return f(*wrapped_args, **wrapped_kwargs) 74 return T.cast(TV_func, wrapped) 75 76def noArgsFlattening(f: TV_func) -> TV_func: 77 setattr(f, 'no-args-flattening', True) # noqa: B010 78 return f 79 80def noSecondLevelHolderResolving(f: TV_func) -> TV_func: 81 setattr(f, 'no-second-level-holder-flattening', True) # noqa: B010 82 return f 83 84def unholder_return(f: TV_func) -> T.Callable[..., TYPE_var]: 85 @wraps(f) 86 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 87 res = f(*wrapped_args, **wrapped_kwargs) 88 return _unholder(res) 89 return T.cast(T.Callable[..., TYPE_var], wrapped) 90 91def disablerIfNotFound(f: TV_func) -> TV_func: 92 @wraps(f) 93 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 94 kwargs = get_callee_args(wrapped_args)[2] 95 disabler = kwargs.pop('disabler', False) 96 ret = f(*wrapped_args, **wrapped_kwargs) 97 if disabler and not ret.found(): 98 return Disabler() 99 return ret 100 return T.cast(TV_func, wrapped) 101 102class permittedKwargs: 103 104 def __init__(self, permitted: T.Set[str]): 105 self.permitted = permitted # type: T.Set[str] 106 107 def __call__(self, f: TV_func) -> TV_func: 108 @wraps(f) 109 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 110 kwargs = get_callee_args(wrapped_args)[2] 111 unknowns = set(kwargs).difference(self.permitted) 112 if unknowns: 113 ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)]) 114 raise InvalidArguments(f'Got unknown keyword arguments {ustr}') 115 return f(*wrapped_args, **wrapped_kwargs) 116 return T.cast(TV_func, wrapped) 117 118if T.TYPE_CHECKING: 119 from .baseobjects import InterpreterObject 120 from typing_extensions import Protocol 121 122 _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True) 123 _TV_ARG1 = T.TypeVar('_TV_ARG1', bound=TYPE_var, contravariant=True) 124 125 class FN_Operator(Protocol[_TV_IntegerObject, _TV_ARG1]): 126 def __call__(s, self: _TV_IntegerObject, other: _TV_ARG1) -> TYPE_var: ... 127 _TV_FN_Operator = T.TypeVar('_TV_FN_Operator', bound=FN_Operator) 128 129def typed_operator(operator: MesonOperator, 130 types: T.Union[T.Type, T.Tuple[T.Type, ...]]) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: 131 """Decorator that does type checking for operator calls. 132 133 The principle here is similar to typed_pos_args, however much simpler 134 since only one other object ever is passed 135 """ 136 def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': 137 @wraps(f) 138 def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: 139 if not isinstance(other, types): 140 raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})') 141 return f(self, other) 142 return T.cast('_TV_FN_Operator', wrapper) 143 return inner 144 145def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: 146 """Decorator that does type checking for unary operator calls. 147 148 This decorator is for unary operators that do not take any other objects. 149 It should be impossible for a user to accidentally break this. Triggering 150 this check always indicates a bug in the Meson interpreter. 151 """ 152 def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': 153 @wraps(f) 154 def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: 155 if other is not None: 156 raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') 157 return f(self, other) 158 return T.cast('_TV_FN_Operator', wrapper) 159 return inner 160 161 162def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], 163 varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None, 164 optargs: T.Optional[T.List[T.Union[T.Type, T.Tuple[T.Type, ...]]]] = None, 165 min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]: 166 """Decorator that types type checking of positional arguments. 167 168 This supports two different models of optional arguments, the first is the 169 variadic argument model. Variadic arguments are a possibly bounded, 170 possibly unbounded number of arguments of the same type (unions are 171 supported). The second is the standard default value model, in this case 172 a number of optional arguments may be provided, but they are still 173 ordered, and they may have different types. 174 175 This function does not support mixing variadic and default arguments. 176 177 :name: The name of the decorated function (as displayed in error messages) 178 :varargs: They type(s) of any variadic arguments the function takes. If 179 None the function takes no variadic args 180 :min_varargs: the minimum number of variadic arguments taken 181 :max_varargs: the maximum number of variadic arguments taken. 0 means unlimited 182 :optargs: The types of any optional arguments parameters taken. If None 183 then no optional parameters are taken. 184 185 Some examples of usage blow: 186 >>> @typed_pos_args('mod.func', str, (str, int)) 187 ... def func(self, state: ModuleState, args: T.Tuple[str, T.Union[str, int]], kwargs: T.Dict[str, T.Any]) -> T.Any: 188 ... pass 189 190 >>> @typed_pos_args('method', str, varargs=str) 191 ... def method(self, node: BaseNode, args: T.Tuple[str, T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: 192 ... pass 193 194 >>> @typed_pos_args('method', varargs=str, min_varargs=1) 195 ... def method(self, node: BaseNode, args: T.Tuple[T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: 196 ... pass 197 198 >>> @typed_pos_args('method', str, optargs=[(str, int), str]) 199 ... def method(self, node: BaseNode, args: T.Tuple[str, T.Optional[T.Union[str, int]], T.Optional[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: 200 ... pass 201 202 When should you chose `typed_pos_args('name', varargs=str, 203 min_varargs=1)` vs `typed_pos_args('name', str, varargs=str)`? 204 205 The answer has to do with the semantics of the function, if all of the 206 inputs are the same type (such as with `files()`) then the former is 207 correct, all of the arguments are string names of files. If the first 208 argument is something else the it should be separated. 209 """ 210 def inner(f: TV_func) -> TV_func: 211 212 @wraps(f) 213 def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 214 args = get_callee_args(wrapped_args)[1] 215 216 # These are implementation programming errors, end users should never see them. 217 assert isinstance(args, list), args 218 assert max_varargs >= 0, 'max_varags cannot be negative' 219 assert min_varargs >= 0, 'min_varags cannot be negative' 220 assert optargs is None or varargs is None, \ 221 'varargs and optargs not supported together as this would be ambiguous' 222 223 num_args = len(args) 224 num_types = len(types) 225 a_types = types 226 227 if varargs: 228 min_args = num_types + min_varargs 229 max_args = num_types + max_varargs 230 if max_varargs == 0 and num_args < min_args: 231 raise InvalidArguments(f'{name} takes at least {min_args} arguments, but got {num_args}.') 232 elif max_varargs != 0 and (num_args < min_args or num_args > max_args): 233 raise InvalidArguments(f'{name} takes between {min_args} and {max_args} arguments, but got {num_args}.') 234 elif optargs: 235 if num_args < num_types: 236 raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.') 237 elif num_args > num_types + len(optargs): 238 raise InvalidArguments(f'{name} takes at most {num_types + len(optargs)} arguments, but got {num_args}.') 239 # Add the number of positional arguments required 240 if num_args > num_types: 241 diff = num_args - num_types 242 a_types = tuple(list(types) + list(optargs[:diff])) 243 elif num_args != num_types: 244 raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.') 245 246 for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1): 247 if not isinstance(arg, type_): 248 if isinstance(type_, tuple): 249 shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_)) 250 else: 251 shouldbe = f'"{type_.__name__}"' 252 raise InvalidArguments(f'{name} argument {i} was of type "{type(arg).__name__}" but should have been {shouldbe}') 253 254 # Ensure that we're actually passing a tuple. 255 # Depending on what kind of function we're calling the length of 256 # wrapped_args can vary. 257 nargs = list(wrapped_args) 258 i = nargs.index(args) 259 if varargs: 260 # if we have varargs we need to split them into a separate 261 # tuple, as python's typing doesn't understand tuples with 262 # fixed elements and variadic elements, only one or the other. 263 # so in that case we need T.Tuple[int, str, float, T.Tuple[str, ...]] 264 pos = args[:len(types)] 265 var = list(args[len(types):]) 266 pos.append(var) 267 nargs[i] = tuple(pos) 268 elif optargs: 269 if num_args < num_types + len(optargs): 270 diff = num_types + len(optargs) - num_args 271 nargs[i] = tuple(list(args) + [None] * diff) 272 else: 273 nargs[i] = args 274 else: 275 nargs[i] = tuple(args) 276 return f(*nargs, **wrapped_kwargs) 277 278 return T.cast(TV_func, wrapper) 279 return inner 280 281 282class ContainerTypeInfo: 283 284 """Container information for keyword arguments. 285 286 For keyword arguments that are containers (list or dict), this class encodes 287 that information. 288 289 :param container: the type of container 290 :param contains: the types the container holds 291 :param pairs: if the container is supposed to be of even length. 292 This is mainly used for interfaces that predate the addition of dictionaries, and use 293 `[key, value, key2, value2]` format. 294 :param allow_empty: Whether this container is allowed to be empty 295 There are some cases where containers not only must be passed, but must 296 not be empty, and other cases where an empty container is allowed. 297 """ 298 299 def __init__(self, container: T.Type, contains: T.Union[T.Type, T.Tuple[T.Type, ...]], *, 300 pairs: bool = False, allow_empty: bool = True): 301 self.container = container 302 self.contains = contains 303 self.pairs = pairs 304 self.allow_empty = allow_empty 305 306 def check(self, value: T.Any) -> bool: 307 """Check that a value is valid. 308 309 :param value: A value to check 310 :return: True if it is valid, False otherwise 311 """ 312 if not isinstance(value, self.container): 313 return False 314 iter_ = iter(value.values()) if isinstance(value, dict) else iter(value) 315 for each in iter_: 316 if not isinstance(each, self.contains): 317 return False 318 if self.pairs and len(value) % 2 != 0: 319 return False 320 if not value and not self.allow_empty: 321 return False 322 return True 323 324 def description(self) -> str: 325 """Human readable description of this container type. 326 327 :return: string to be printed 328 """ 329 container = 'dict' if self.container is dict else 'list' 330 if isinstance(self.contains, tuple): 331 contains = ','.join([t.__name__ for t in self.contains]) 332 else: 333 contains = self.contains.__name__ 334 s = f'{container}[{contains}]' 335 if self.pairs: 336 s += ' that has even size' 337 if not self.allow_empty: 338 s += ' that cannot be empty' 339 return s 340 341_T = T.TypeVar('_T') 342 343class _NULL_T: 344 """Special null type for evolution, this is an implementation detail.""" 345 346 347_NULL = _NULL_T() 348 349class KwargInfo(T.Generic[_T]): 350 351 """A description of a keyword argument to a meson function 352 353 This is used to describe a value to the :func:typed_kwargs function. 354 355 :param name: the name of the parameter 356 :param types: A type or tuple of types that are allowed, or a :class:ContainerType 357 :param required: Whether this is a required keyword argument. defaults to False 358 :param listify: If true, then the argument will be listified before being 359 checked. This is useful for cases where the Meson DSL allows a scalar or 360 a container, but internally we only want to work with containers 361 :param default: A default value to use if this isn't set. defaults to None, 362 this may be safely set to a mutable type, as long as that type does not 363 itself contain mutable types, typed_kwargs will copy the default 364 :param since: Meson version in which this argument has been added. defaults to None 365 :param deprecated: Meson version in which this argument has been deprecated. defaults to None 366 :param validator: A callable that does additional validation. This is mainly 367 intended for cases where a string is expected, but only a few specific 368 values are accepted. Must return None if the input is valid, or a 369 message if the input is invalid 370 :param convertor: A callable that converts the raw input value into a 371 different type. This is intended for cases such as the meson DSL using a 372 string, but the implementation using an Enum. This should not do 373 validation, just conversion. 374 :param deprecated_values: a dictionary mapping a value to the version of 375 meson it was deprecated in. 376 :param since_values: a dictionary mapping a value to the version of meson it was 377 added in. 378 :param not_set_warning: A warning message that is logged if the kwarg is not 379 set by the user. 380 """ 381 def __init__(self, name: str, 382 types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo], 383 *, required: bool = False, listify: bool = False, 384 default: T.Optional[_T] = None, 385 since: T.Optional[str] = None, 386 since_values: T.Optional[T.Dict[str, str]] = None, 387 deprecated: T.Optional[str] = None, 388 deprecated_values: T.Optional[T.Dict[str, str]] = None, 389 validator: T.Optional[T.Callable[[T.Any], T.Optional[str]]] = None, 390 convertor: T.Optional[T.Callable[[_T], object]] = None, 391 not_set_warning: T.Optional[str] = None): 392 self.name = name 393 self.types = types 394 self.required = required 395 self.listify = listify 396 self.default = default 397 self.since_values = since_values 398 self.since = since 399 self.deprecated = deprecated 400 self.deprecated_values = deprecated_values 401 self.validator = validator 402 self.convertor = convertor 403 self.not_set_warning = not_set_warning 404 405 def evolve(self, *, 406 name: T.Union[str, _NULL_T] = _NULL, 407 required: T.Union[bool, _NULL_T] = _NULL, 408 listify: T.Union[bool, _NULL_T] = _NULL, 409 default: T.Union[_T, None, _NULL_T] = _NULL, 410 since: T.Union[str, None, _NULL_T] = _NULL, 411 since_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL, 412 deprecated: T.Union[str, None, _NULL_T] = _NULL, 413 deprecated_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL, 414 validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL, 415 convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo': 416 """Create a shallow copy of this KwargInfo, with modifications. 417 418 This allows us to create a new copy of a KwargInfo with modifications. 419 This allows us to use a shared kwarg that implements complex logic, but 420 has slight differences in usage, such as being added to different 421 functions in different versions of Meson. 422 423 The use the _NULL special value here allows us to pass None, which has 424 meaning in many of these cases. _NULL itself is never stored, always 425 being replaced by either the copy in self, or the provided new version. 426 """ 427 return type(self)( 428 name if not isinstance(name, _NULL_T) else self.name, 429 self.types, 430 listify=listify if not isinstance(listify, _NULL_T) else self.listify, 431 required=required if not isinstance(required, _NULL_T) else self.required, 432 default=default if not isinstance(default, _NULL_T) else self.default, 433 since=since if not isinstance(since, _NULL_T) else self.since, 434 since_values=since_values if not isinstance(since_values, _NULL_T) else self.since_values, 435 deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated, 436 deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values, 437 validator=validator if not isinstance(validator, _NULL_T) else self.validator, 438 convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor, 439 ) 440 441 442def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]: 443 """Decorator for type checking keyword arguments. 444 445 Used to wrap a meson DSL implementation function, where it checks various 446 things about keyword arguments, including the type, and various other 447 information. For non-required values it sets the value to a default, which 448 means the value will always be provided. 449 450 If type tyhpe is a :class:ContainerTypeInfo, then the default value will be 451 passed as an argument to the container initializer, making a shallow copy 452 453 :param name: the name of the function, including the object it's attached to 454 (if applicable) 455 :param *types: KwargInfo entries for each keyword argument. 456 """ 457 def inner(f: TV_func) -> TV_func: 458 459 @wraps(f) 460 def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 461 _kwargs, subproject = get_callee_args(wrapped_args)[2:4] 462 # Cast here, as the convertor function may place something other than a TYPE_var in the kwargs 463 kwargs = T.cast(T.Dict[str, object], _kwargs) 464 465 all_names = {t.name for t in types} 466 unknowns = set(kwargs).difference(all_names) 467 if unknowns: 468 ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)]) 469 raise InvalidArguments(f'{name} got unknown keyword arguments {ustr}') 470 471 for info in types: 472 types_tuple = info.types if isinstance(info.types, tuple) else (info.types,) 473 def check_value_type(value: T.Any) -> bool: 474 for t in types_tuple: 475 if isinstance(t, ContainerTypeInfo): 476 if t.check(value): 477 return True 478 elif isinstance(value, t): 479 return True 480 return False 481 def types_description() -> str: 482 candidates = [] 483 for t in types_tuple: 484 if isinstance(t, ContainerTypeInfo): 485 candidates.append(t.description()) 486 else: 487 candidates.append(t.__name__) 488 shouldbe = 'one of: ' if len(candidates) > 1 else '' 489 shouldbe += ', '.join(candidates) 490 return shouldbe 491 value = kwargs.get(info.name) 492 if value is not None: 493 if info.since: 494 feature_name = info.name + ' arg in ' + name 495 FeatureNew.single_use(feature_name, info.since, subproject) 496 if info.deprecated: 497 feature_name = info.name + ' arg in ' + name 498 FeatureDeprecated.single_use(feature_name, info.deprecated, subproject) 499 if info.listify: 500 kwargs[info.name] = value = mesonlib.listify(value) 501 if not check_value_type(value): 502 shouldbe = types_description() 503 raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {type(value).__name__!r} but should have been {shouldbe}') 504 505 if info.validator is not None: 506 msg = info.validator(value) 507 if msg is not None: 508 raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}') 509 510 warn: bool 511 if info.deprecated_values is not None: 512 for n, version in info.deprecated_values.items(): 513 if isinstance(value, (dict, list)): 514 warn = n in value 515 else: 516 warn = n == value 517 518 if warn: 519 FeatureDeprecated.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject) 520 521 if info.since_values is not None: 522 for n, version in info.since_values.items(): 523 if isinstance(value, (dict, list)): 524 warn = n in value 525 else: 526 warn = n == value 527 528 if warn: 529 FeatureNew.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject) 530 531 elif info.required: 532 raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"') 533 else: 534 # set the value to the default, this ensuring all kwargs are present 535 # This both simplifies the typing checking and the usage 536 assert check_value_type(info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description()}' 537 # Create a shallow copy of the container. This allows mutable 538 # types to be used safely as default values 539 kwargs[info.name] = copy.copy(info.default) 540 if info.not_set_warning: 541 mlog.warning(info.not_set_warning) 542 543 if info.convertor: 544 kwargs[info.name] = info.convertor(kwargs[info.name]) 545 546 return f(*wrapped_args, **wrapped_kwargs) 547 return T.cast(TV_func, wrapper) 548 return inner 549 550 551class FeatureCheckBase(metaclass=abc.ABCMeta): 552 "Base class for feature version checks" 553 554 # In python 3.6 we can just forward declare this, but in 3.5 we can't 555 # This will be overwritten by the subclasses by necessity 556 feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]] 557 558 def __init__(self, feature_name: str, version: str, extra_message: T.Optional[str] = None): 559 self.feature_name = feature_name # type: str 560 self.feature_version = version # type: str 561 self.extra_message = extra_message or '' # type: str 562 563 @staticmethod 564 def get_target_version(subproject: str) -> str: 565 # Don't do any checks if project() has not been parsed yet 566 if subproject not in mesonlib.project_meson_versions: 567 return '' 568 return mesonlib.project_meson_versions[subproject] 569 570 @staticmethod 571 @abc.abstractmethod 572 def check_version(target_version: str, feature_Version: str) -> bool: 573 pass 574 575 def use(self, subproject: str) -> None: 576 tv = self.get_target_version(subproject) 577 # No target version 578 if tv == '': 579 return 580 # Target version is new enough 581 if self.check_version(tv, self.feature_version): 582 return 583 # Feature is too new for target version, register it 584 if subproject not in self.feature_registry: 585 self.feature_registry[subproject] = {self.feature_version: set()} 586 register = self.feature_registry[subproject] 587 if self.feature_version not in register: 588 register[self.feature_version] = set() 589 if self.feature_name in register[self.feature_version]: 590 # Don't warn about the same feature multiple times 591 # FIXME: This is needed to prevent duplicate warnings, but also 592 # means we won't warn about a feature used in multiple places. 593 return 594 register[self.feature_version].add(self.feature_name) 595 self.log_usage_warning(tv) 596 597 @classmethod 598 def report(cls, subproject: str) -> None: 599 if subproject not in cls.feature_registry: 600 return 601 warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject)) 602 fv = cls.feature_registry[subproject] 603 for version in sorted(fv.keys()): 604 warning_str += '\n * {}: {}'.format(version, fv[version]) 605 mlog.warning(warning_str) 606 607 def log_usage_warning(self, tv: str) -> None: 608 raise InterpreterException('log_usage_warning not implemented') 609 610 @staticmethod 611 def get_warning_str_prefix(tv: str) -> str: 612 raise InterpreterException('get_warning_str_prefix not implemented') 613 614 def __call__(self, f: TV_func) -> TV_func: 615 @wraps(f) 616 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 617 subproject = get_callee_args(wrapped_args)[3] 618 if subproject is None: 619 raise AssertionError(f'{wrapped_args!r}') 620 self.use(subproject) 621 return f(*wrapped_args, **wrapped_kwargs) 622 return T.cast(TV_func, wrapped) 623 624 @classmethod 625 def single_use(cls, feature_name: str, version: str, subproject: str, 626 extra_message: T.Optional[str] = None) -> None: 627 """Oneline version that instantiates and calls use().""" 628 cls(feature_name, version, extra_message).use(subproject) 629 630 631class FeatureNew(FeatureCheckBase): 632 """Checks for new features""" 633 634 # Class variable, shared across all instances 635 # 636 # Format: {subproject: {feature_version: set(feature_names)}} 637 feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]] 638 639 @staticmethod 640 def check_version(target_version: str, feature_version: str) -> bool: 641 return mesonlib.version_compare_condition_with_min(target_version, feature_version) 642 643 @staticmethod 644 def get_warning_str_prefix(tv: str) -> str: 645 return f'Project specifies a minimum meson_version \'{tv}\' but uses features which were added in newer versions:' 646 647 def log_usage_warning(self, tv: str) -> None: 648 args = [ 649 'Project targeting', f"'{tv}'", 650 'but tried to use feature introduced in', 651 f"'{self.feature_version}':", 652 f'{self.feature_name}.', 653 ] 654 if self.extra_message: 655 args.append(self.extra_message) 656 mlog.warning(*args) 657 658class FeatureDeprecated(FeatureCheckBase): 659 """Checks for deprecated features""" 660 661 # Class variable, shared across all instances 662 # 663 # Format: {subproject: {feature_version: set(feature_names)}} 664 feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]] 665 666 @staticmethod 667 def check_version(target_version: str, feature_version: str) -> bool: 668 # For deprecation checks we need to return the inverse of FeatureNew checks 669 return not mesonlib.version_compare_condition_with_min(target_version, feature_version) 670 671 @staticmethod 672 def get_warning_str_prefix(tv: str) -> str: 673 return 'Deprecated features used:' 674 675 def log_usage_warning(self, tv: str) -> None: 676 args = [ 677 'Project targeting', f"'{tv}'", 678 'but tried to use feature deprecated since', 679 f"'{self.feature_version}':", 680 f'{self.feature_name}.', 681 ] 682 if self.extra_message: 683 args.append(self.extra_message) 684 mlog.warning(*args) 685 686 687class FeatureCheckKwargsBase(metaclass=abc.ABCMeta): 688 689 @property 690 @abc.abstractmethod 691 def feature_check_class(self) -> T.Type[FeatureCheckBase]: 692 pass 693 694 def __init__(self, feature_name: str, feature_version: str, 695 kwargs: T.List[str], extra_message: T.Optional[str] = None): 696 self.feature_name = feature_name 697 self.feature_version = feature_version 698 self.kwargs = kwargs 699 self.extra_message = extra_message 700 701 def __call__(self, f: TV_func) -> TV_func: 702 @wraps(f) 703 def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: 704 kwargs, subproject = get_callee_args(wrapped_args)[2:4] 705 if subproject is None: 706 raise AssertionError(f'{wrapped_args!r}') 707 for arg in self.kwargs: 708 if arg not in kwargs: 709 continue 710 name = arg + ' arg in ' + self.feature_name 711 self.feature_check_class.single_use( 712 name, self.feature_version, subproject, self.extra_message) 713 return f(*wrapped_args, **wrapped_kwargs) 714 return T.cast(TV_func, wrapped) 715 716class FeatureNewKwargs(FeatureCheckKwargsBase): 717 feature_check_class = FeatureNew 718 719class FeatureDeprecatedKwargs(FeatureCheckKwargsBase): 720 feature_check_class = FeatureDeprecated 721