From b30dddd4e5b4ae6e5e1e812085a00a47e3edfcf1 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 26 Jul 2021 14:16:31 -0700 Subject: [PATCH] interpreter/compiler: Add type checking for the Compiler object This adds a full set of `typed_pos_args` and `typed_kwarg` decorations, as well as fixing all of the typing errors reported by mypy. --- mesonbuild/interpreter/compiler.py | 741 ++++++++++++++--------------- unittests/allplatformstests.py | 9 +- 2 files changed, 349 insertions(+), 401 deletions(-) diff --git a/mesonbuild/interpreter/compiler.py b/mesonbuild/interpreter/compiler.py index 1050e26c2..927ac77ad 100644 --- a/mesonbuild/interpreter/compiler.py +++ b/mesonbuild/interpreter/compiler.py @@ -1,22 +1,90 @@ -import functools - -from ..interpreterbase.decorators import typed_kwargs, KwargInfo +# SPDX-Licnese-Identifier: Apache-2.0 +# Copyright 2012-2021 The Meson development team +# Copyright © 2021 Intel Corpration -from .interpreterobjects import (extract_required_kwarg, extract_search_dirs) +import enum +import functools +import typing as T +from .. import build +from .. import coredata +from .. import dependencies from .. import mesonlib from .. import mlog -from .. import dependencies -from ..interpreterbase import (ObjectHolder, noPosargs, noKwargs, permittedKwargs, - FeatureNew, FeatureNewKwargs, disablerIfNotFound, - check_stringlist, InterpreterException, InvalidArguments) - -import typing as T -import os +from ..compilers.compilers import CompileCheckMode +from ..interpreterbase import (ObjectHolder, noPosargs, noKwargs, + FeatureNew, disablerIfNotFound, + InterpreterException) +from ..interpreterbase.decorators import ContainerTypeInfo, typed_kwargs, KwargInfo, typed_pos_args +from .interpreterobjects import (extract_required_kwarg, extract_search_dirs) +from .type_checking import REQUIRED_KW if T.TYPE_CHECKING: from ..interpreter import Interpreter from ..compilers import Compiler, RunResult + from ..interpreterbase import TYPE_var, TYPE_kwargs + from .kwargs import ExtractRequired, ExtractSearchDirs + + from typing_extensions import TypedDict, Literal + + class GetSupportedArgumentKw(TypedDict): + + checked: Literal['warn', 'require', 'off'] + + class AlignmentKw(TypedDict): + + prefix: str + args: T.List[str] + dependencies: T.List[dependencies.Dependency] + + class CompileKW(TypedDict): + + name: str + no_builtin_args: bool + include_directories: T.List[build.IncludeDirs] + args: T.List[str] + dependencies: T.List[dependencies.Dependency] + + class CommonKW(TypedDict): + + prefix: str + no_builtin_args: bool + include_directories: T.List[build.IncludeDirs] + args: T.List[str] + dependencies: T.List[dependencies.Dependency] + + class CompupteIntKW(CommonKW): + + guess: T.Optional[int] + high: T.Optional[int] + low: T.Optional[int] + + class HeaderKW(CommonKW, ExtractRequired): + pass + + class FindLibraryKW(ExtractRequired, ExtractSearchDirs): + + disabler: bool + has_headers: T.List[str] + static: bool + + # This list must be all of the `HeaderKW` values with `header_` + # prepended to the key + header_args: T.List[str] + header_dependencies: T.List[dependencies.Dependency] + header_include_directories: T.List[build.IncludeDirs] + header_no_builtin_args: bool + header_prefix: str + header_required: T.Union[bool, coredata.UserFeatureOption] + + +class _TestMode(enum.Enum): + + """Whether we're doing a compiler or linker check.""" + + COMPILER = 0 + LINKER = 1 + class TryRunResultHolder(ObjectHolder['RunResult']): def __init__(self, res: 'RunResult', interpreter: 'Interpreter'): @@ -47,23 +115,37 @@ class TryRunResultHolder(ObjectHolder['RunResult']): def stderr_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: return self.held_object.stderr -header_permitted_kwargs = { - 'required', - 'prefix', - 'no_builtin_args', - 'include_directories', + +_ARGS_KW: KwargInfo[T.List[str]] = KwargInfo( 'args', + ContainerTypeInfo(list, str), + listify=True, + default=[], +) +_DEPENDENCIES_KW: KwargInfo[T.List['dependencies.Dependency']] = KwargInfo( 'dependencies', -} + ContainerTypeInfo(list, dependencies.Dependency), + listify=True, + default=[], +) +_INCLUDE_DIRS_KW: KwargInfo[T.List[build.IncludeDirs]] = KwargInfo( + 'include_directories', + ContainerTypeInfo(list, build.IncludeDirs), + default=[], + listify=True, +) +_PREFIX_KW = KwargInfo('prefix', str, default='') +_NO_BUILTIN_ARGS_KW = KwargInfo('no_builtin_args', bool, default=False) +_NAME_KW = KwargInfo('name', str, default='') + +# Many of the compiler methods take this kwarg signature exactly, this allows +# simplifying the `typed_kwargs` calls +_COMMON_KWS: T.List[KwargInfo] = [_ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _PREFIX_KW, _NO_BUILTIN_ARGS_KW] -find_library_permitted_kwargs = { - 'has_headers', - 'required', - 'dirs', - 'static', -} +# Common methods of compiles, links, runs, and similar +_COMPILES_KWS: T.List[KwargInfo] = [_NAME_KW, _ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _NO_BUILTIN_ARGS_KW] -find_library_permitted_kwargs |= {'header_' + k for k in header_permitted_kwargs} +_HEADER_KWS: T.List[KwargInfo] = [REQUIRED_KW.evolve(since='0.50.0', default=False), *_COMMON_KWS] class CompilerHolder(ObjectHolder['Compiler']): def __init__(self, compiler: 'Compiler', interpreter: 'Interpreter'): @@ -106,7 +188,8 @@ class CompilerHolder(ObjectHolder['Compiler']): def compiler(self) -> 'Compiler': return self.held_object - def _dep_msg(self, deps, endl): + @staticmethod + def _dep_msg(deps: T.List['dependencies.Dependency'], endl: str) -> str: msg_single = 'with dependency {}' msg_many = 'with dependencies {}' if not deps: @@ -129,37 +212,32 @@ class CompilerHolder(ObjectHolder['Compiler']): @noPosargs @noKwargs - def version_method(self, args, kwargs): + def version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: return self.compiler.version @noPosargs @noKwargs - def cmd_array_method(self, args, kwargs): + def cmd_array_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]: return self.compiler.exelist - def determine_args(self, kwargs, mode='link'): - nobuiltins = kwargs.get('no_builtin_args', False) - if not isinstance(nobuiltins, bool): - raise InterpreterException('Type of no_builtin_args not a boolean.') - args = [] - incdirs = mesonlib.extract_as_list(kwargs, 'include_directories') + def determine_args(self, nobuiltins: bool, + incdirs: T.List[build.IncludeDirs], + extra_args: T.List[str], + mode: CompileCheckMode = CompileCheckMode.LINK) -> T.List[str]: + args: T.List[str] = [] for i in incdirs: - from ..build import IncludeDirs - if not isinstance(i, IncludeDirs): - raise InterpreterException('Include directories argument must be an include_directories object.') for idir in i.to_string_list(self.environment.get_source_dir()): - args += self.compiler.get_include_args(idir, False) + args.extend(self.compiler.get_include_args(idir, False)) if not nobuiltins: opts = self.environment.coredata.options args += self.compiler.get_option_compile_args(opts) - if mode == 'link': - args += self.compiler.get_option_link_args(opts) - args += mesonlib.stringlistify(kwargs.get('args', [])) + if mode is CompileCheckMode.LINK: + args.extend(self.compiler.get_option_link_args(opts)) + args.extend(extra_args) return args - def determine_dependencies(self, kwargs, endl=':'): - deps = kwargs.get('dependencies', None) - if deps is not None: + def determine_dependencies(self, deps: T.List['dependencies.Dependency'], endl: str = ':') -> T.Tuple[T.List['dependencies.Dependency'], str]: + if deps: final_deps = [] while deps: next_deps = [] @@ -170,53 +248,40 @@ class CompilerHolder(ObjectHolder['Compiler']): next_deps.extend(d.ext_deps) deps = next_deps deps = final_deps + else: + # Ensure that we alway return a new instance + deps = deps.copy() return deps, self._dep_msg(deps, endl) - @permittedKwargs({ - 'prefix', - 'args', - 'dependencies', - }) - def alignment_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Alignment method takes exactly one positional argument.') - check_stringlist(args) + @typed_pos_args('compiler.alignment', str) + @typed_kwargs( + 'compiler.alignment', + _PREFIX_KW, + _ARGS_KW, + _DEPENDENCIES_KW, + ) + def alignment_method(self, args: T.Tuple[str], kwargs: 'AlignmentKw') -> int: typename = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of alignment must be a string.') - extra_args = mesonlib.stringlistify(kwargs.get('args', [])) - deps, msg = self.determine_dependencies(kwargs) - result = self.compiler.alignment(typename, prefix, self.environment, - extra_args=extra_args, + deps, msg = self.determine_dependencies(kwargs['dependencies']) + result = self.compiler.alignment(typename, kwargs['prefix'], self.environment, + extra_args=kwargs['args'], dependencies=deps) mlog.log('Checking for alignment of', mlog.bold(typename, True), msg, result) return result - @permittedKwargs({ - 'name', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def run_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Run method takes exactly one positional argument.') + @typed_pos_args('compiler.run', (str, mesonlib.File)) + @typed_kwargs('compiler.run', *_COMPILES_KWS) + def run_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> 'RunResult': code = args[0] if isinstance(code, mesonlib.File): code = mesonlib.File.from_absolute_file( code.rel_to_builddir(self.environment.source_dir)) - elif not isinstance(code, str): - raise InvalidArguments('Argument must be string or file.') - testname = kwargs.get('name', '') - if not isinstance(testname, str): - raise InterpreterException('Testname argument must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs, endl=None) + testname = kwargs['name'] + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies'], endl=None) result = self.compiler.run(code, self.environment, extra_args=extra_args, dependencies=deps) - if len(testname) > 0: + if testname: if not result.compiled: h = mlog.red('DID NOT COMPILE') elif result.returncode == 0: @@ -246,346 +311,226 @@ class CompilerHolder(ObjectHolder['Compiler']): ''' return self.compiler.symbols_have_underscore_prefix(self.environment) - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def has_member_method(self, args, kwargs): - if len(args) != 2: - raise InterpreterException('Has_member takes exactly two arguments.') - check_stringlist(args) + @typed_pos_args('compiler.has_member', str, str) + @typed_kwargs('compiler.has_member', *_COMMON_KWS) + def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool: typename, membername = args - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_member must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - had, cached = self.compiler.has_members(typename, [membername], prefix, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + had, cached = self.compiler.has_members(typename, [membername], kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') mlog.log('Checking whether type', mlog.bold(typename, True), - 'has member', mlog.bold(membername, True), msg, hadtxt, cached) + 'has member', mlog.bold(membername, True), msg, hadtxt, cached_msg) return had - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def has_members_method(self, args, kwargs): - if len(args) < 2: - raise InterpreterException('Has_members needs at least two arguments.') - check_stringlist(args) - typename, *membernames = args - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_members must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - had, cached = self.compiler.has_members(typename, membernames, prefix, + @typed_pos_args('compiler.has_members', str, varargs=str, min_varargs=1) + @typed_kwargs('compiler.has_members', *_COMMON_KWS) + def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'CommonKW') -> bool: + typename, membernames = args + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + had, cached = self.compiler.has_members(typename, membernames, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') members = mlog.bold(', '.join([f'"{m}"' for m in membernames])) mlog.log('Checking whether type', mlog.bold(typename, True), - 'has members', members, msg, hadtxt, cached) + 'has members', members, msg, hadtxt, cached_msg) return had - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def has_function_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Has_function takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.has_function', str) + @typed_kwargs('compiler.has_type', *_COMMON_KWS) + def has_function_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool: funcname = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_function must be a string.') - extra_args = self.determine_args(kwargs) - deps, msg = self.determine_dependencies(kwargs) - had, cached = self.compiler.has_function(funcname, prefix, self.environment, + extra_args = self.determine_args(kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + had, cached = self.compiler.has_function(funcname, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') - mlog.log('Checking for function', mlog.bold(funcname, True), msg, hadtxt, cached) + mlog.log('Checking for function', mlog.bold(funcname, True), msg, hadtxt, cached_msg) return had - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def has_type_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Has_type takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.has_type', str) + @typed_kwargs('compiler.has_type', *_COMMON_KWS) + def has_type_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool: typename = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_type must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - had, cached = self.compiler.has_type(typename, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + had, cached = self.compiler.has_type(typename, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') - mlog.log('Checking for type', mlog.bold(typename, True), msg, hadtxt, cached) + mlog.log('Checking for type', mlog.bold(typename, True), msg, hadtxt, cached_msg) return had @FeatureNew('compiler.compute_int', '0.40.0') - @permittedKwargs({ - 'prefix', - 'low', - 'high', - 'guess', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def compute_int_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Compute_int takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.compute_int', str) + @typed_kwargs( + 'compiler.compute_int', + KwargInfo('low', (int, None)), + KwargInfo('high', (int, None)), + KwargInfo('guess', (int, None)), + *_COMMON_KWS, + ) + def compute_int_method(self, args: T.Tuple[str], kwargs: 'CompupteIntKW') -> int: expression = args[0] - prefix = kwargs.get('prefix', '') - low = kwargs.get('low', None) - high = kwargs.get('high', None) - guess = kwargs.get('guess', None) - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of compute_int must be a string.') - if low is not None and not isinstance(low, int): - raise InterpreterException('Low argument of compute_int must be an int.') - if high is not None and not isinstance(high, int): - raise InterpreterException('High argument of compute_int must be an int.') - if guess is not None and not isinstance(guess, int): - raise InterpreterException('Guess argument of compute_int must be an int.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - res = self.compiler.compute_int(expression, low, high, guess, prefix, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + res = self.compiler.compute_int(expression, kwargs['low'], kwargs['high'], + kwargs['guess'], kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) mlog.log('Computing int of', mlog.bold(expression, True), msg, res) return res - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def sizeof_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('Sizeof takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.sizeof', str) + @typed_kwargs('compiler.sizeof', *_COMMON_KWS) + def sizeof_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> int: element = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of sizeof must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - esize = self.compiler.sizeof(element, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + esize = self.compiler.sizeof(element, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) mlog.log('Checking for size of', mlog.bold(element, True), msg, esize) return esize @FeatureNew('compiler.get_define', '0.40.0') - @permittedKwargs({ - 'prefix', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def get_define_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('get_define() takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.get_define', str) + @typed_kwargs('compiler.get_define', *_COMMON_KWS) + def get_define_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> str: element = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of get_define() must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - value, cached = self.compiler.get_define(element, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + value, cached = self.compiler.get_define(element, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' - mlog.log('Fetching value of define', mlog.bold(element, True), msg, value, cached) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log('Fetching value of define', mlog.bold(element, True), msg, value, cached_msg) return value - @permittedKwargs({ - 'name', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def compiles_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('compiles method takes exactly one argument.') + @typed_pos_args('compiler.compiles', (str, mesonlib.File)) + @typed_kwargs('compiler.compiles', *_COMPILES_KWS) + def compiles_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> bool: code = args[0] if isinstance(code, mesonlib.File): code = mesonlib.File.from_absolute_file( code.rel_to_builddir(self.environment.source_dir)) - elif not isinstance(code, str): - raise InvalidArguments('Argument must be string or file.') - testname = kwargs.get('name', '') - if not isinstance(testname, str): - raise InterpreterException('Testname argument must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs, endl=None) + testname = kwargs['name'] + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) result, cached = self.compiler.compiles(code, self.environment, extra_args=extra_args, dependencies=deps) - if len(testname) > 0: + if testname: if result: h = mlog.green('YES') else: h = mlog.red('NO') - cached = mlog.blue('(cached)') if cached else '' - mlog.log('Checking if', mlog.bold(testname, True), msg, 'compiles:', h, cached) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log('Checking if', mlog.bold(testname, True), msg, 'compiles:', h, cached_msg) return result - @permittedKwargs({ - 'name', - 'no_builtin_args', - 'include_directories', - 'args', - 'dependencies', - }) - def links_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('links method takes exactly one argument.') + @typed_pos_args('compiler.links', (str, mesonlib.File)) + @typed_kwargs('compiler.links', *_COMPILES_KWS) + def links_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> bool: code = args[0] if isinstance(code, mesonlib.File): code = mesonlib.File.from_absolute_file( code.rel_to_builddir(self.environment.source_dir)) - elif not isinstance(code, str): - raise InvalidArguments('Argument must be string or file.') - testname = kwargs.get('name', '') - if not isinstance(testname, str): - raise InterpreterException('Testname argument must be a string.') - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs, endl=None) + testname = kwargs['name'] + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) result, cached = self.compiler.links(code, self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' - if len(testname) > 0: + cached_msg = mlog.blue('(cached)') if cached else '' + if testname: if result: h = mlog.green('YES') else: h = mlog.red('NO') - mlog.log('Checking if', mlog.bold(testname, True), msg, 'links:', h, cached) + mlog.log('Checking if', mlog.bold(testname, True), msg, 'links:', h, cached_msg) return result @FeatureNew('compiler.check_header', '0.47.0') - @FeatureNewKwargs('compiler.check_header', '0.50.0', ['required']) - @permittedKwargs(header_permitted_kwargs) - def check_header_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('check_header method takes exactly one argument.') - check_stringlist(args) + @typed_pos_args('compiler.check_header', str) + @typed_kwargs('compiler.check_header', *_HEADER_KWS) + def check_header_method(self, args: T.Tuple[str], kwargs: 'HeaderKW') -> bool: hname = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_header must be a string.') disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) if disabled: mlog.log('Check usable header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - haz, cached = self.compiler.check_header(hname, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + haz, cached = self.compiler.check_header(hname, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if required and not haz: raise InterpreterException(f'{self.compiler.get_display_language()} header {hname!r} not usable') elif haz: h = mlog.green('YES') else: h = mlog.red('NO') - mlog.log('Check usable header', mlog.bold(hname, True), msg, h, cached) + mlog.log('Check usable header', mlog.bold(hname, True), msg, h, cached_msg) return haz - @FeatureNewKwargs('compiler.has_header', '0.50.0', ['required']) - @permittedKwargs(header_permitted_kwargs) - def has_header_method(self, args, kwargs): - if len(args) != 1: - raise InterpreterException('has_header method takes exactly one argument.') - check_stringlist(args) - hname = args[0] - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_header must be a string.') + def _has_header_impl(self, hname: str, kwargs: 'HeaderKW') -> bool: disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) if disabled: mlog.log('Has header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - haz, cached = self.compiler.has_header(hname, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + haz, cached = self.compiler.has_header(hname, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) - cached = mlog.blue('(cached)') if cached else '' + cached_msg = mlog.blue('(cached)') if cached else '' if required and not haz: raise InterpreterException(f'{self.compiler.get_display_language()} header {hname!r} not found') elif haz: h = mlog.green('YES') else: h = mlog.red('NO') - mlog.log('Has header', mlog.bold(hname, True), msg, h, cached) + mlog.log('Has header', mlog.bold(hname, True), msg, h, cached_msg) return haz - @FeatureNewKwargs('compiler.has_header_symbol', '0.50.0', ['required']) - @permittedKwargs(header_permitted_kwargs) - def has_header_symbol_method(self, args, kwargs): - if len(args) != 2: - raise InterpreterException('has_header_symbol method takes exactly two arguments.') - check_stringlist(args) + @typed_pos_args('compiler.has_header', str) + @typed_kwargs('compiler.has_header', *_HEADER_KWS) + def has_header_method(self, args: T.Tuple[str], kwargs: 'HeaderKW') -> bool: + return self._has_header_impl(args[0], kwargs) + + @typed_pos_args('compiler.has_header_symbol', str, str) + @typed_kwargs('compiler.has_header_symbol', *_HEADER_KWS) + def has_header_symbol_method(self, args: T.Tuple[str, str], kwargs: 'HeaderKW') -> bool: hname, symbol = args - prefix = kwargs.get('prefix', '') - if not isinstance(prefix, str): - raise InterpreterException('Prefix argument of has_header_symbol must be a string.') disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) if disabled: mlog.log(f'Header <{hname}> has symbol', mlog.bold(symbol, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self.determine_args, kwargs) - deps, msg = self.determine_dependencies(kwargs) - haz, cached = self.compiler.has_header_symbol(hname, symbol, prefix, self.environment, + extra_args = functools.partial(self.determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + deps, msg = self.determine_dependencies(kwargs['dependencies']) + haz, cached = self.compiler.has_header_symbol(hname, symbol, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) if required and not haz: @@ -594,97 +539,113 @@ class CompilerHolder(ObjectHolder['Compiler']): h = mlog.green('YES') else: h = mlog.red('NO') - cached = mlog.blue('(cached)') if cached else '' - mlog.log(f'Header <{hname}> has symbol', mlog.bold(symbol, True), msg, h, cached) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log(f'Header <{hname}> has symbol', mlog.bold(symbol, True), msg, h, cached_msg) return haz - def notfound_library(self, libname): + def notfound_library(self, libname: str) -> 'dependencies.ExternalLibrary': lib = dependencies.ExternalLibrary(libname, None, self.environment, self.compiler.language, silent=True) return lib - @FeatureNewKwargs('compiler.find_library', '0.51.0', ['static']) - @FeatureNewKwargs('compiler.find_library', '0.50.0', ['has_headers']) - @FeatureNewKwargs('compiler.find_library', '0.49.0', ['disabler']) @disablerIfNotFound - @permittedKwargs(find_library_permitted_kwargs) - def find_library_method(self, args, kwargs): + @typed_pos_args('compiler.find_library', str) + @typed_kwargs( + 'compiler.find_library', + KwargInfo('required', (bool, coredata.UserFeatureOption), default=True), + KwargInfo('has_headers', ContainerTypeInfo(list, str), listify=True, default=[], since='0.50.0'), + KwargInfo('static', (bool, None), since='0.51.0'), + KwargInfo('disabler', bool, default=False, since='0.49.0'), + KwargInfo('dirs', ContainerTypeInfo(list, str), listify=True, default=[]), + *[k.evolve(name=f'header_{k.name}') for k in _HEADER_KWS] + ) + def find_library_method(self, args: T.Tuple[str], kwargs: 'FindLibraryKW') -> 'dependencies.ExternalLibrary': # TODO add dependencies support? - if len(args) != 1: - raise InterpreterException('find_library method takes one argument.') libname = args[0] - if not isinstance(libname, str): - raise InterpreterException('Library name not a string.') disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: mlog.log('Library', mlog.bold(libname), 'skipped: feature', mlog.bold(feature), 'disabled') return self.notfound_library(libname) - has_header_kwargs = {k[7:]: v for k, v in kwargs.items() if k.startswith('header_')} - has_header_kwargs['required'] = required - headers = mesonlib.stringlistify(kwargs.get('has_headers', [])) - for h in headers: - if not self.has_header_method([h], has_header_kwargs): + # This could be done with a comprehension, but that confuses the type + # checker, and having it check this seems valuable + has_header_kwargs: 'HeaderKW' = { + 'required': required, + 'args': kwargs['header_args'], + 'dependencies': kwargs['header_dependencies'], + 'include_directories': kwargs['header_include_directories'], + 'prefix': kwargs['header_prefix'], + 'no_builtin_args': kwargs['header_no_builtin_args'], + } + for h in kwargs['has_headers']: + if not self._has_header_impl(h, has_header_kwargs): return self.notfound_library(libname) search_dirs = extract_search_dirs(kwargs) - libtype = mesonlib.LibType.PREFER_SHARED - if 'static' in kwargs: - if not isinstance(kwargs['static'], bool): - raise InterpreterException('static must be a boolean') - libtype = mesonlib.LibType.STATIC if kwargs['static'] else mesonlib.LibType.SHARED + if kwargs['static'] is True: + libtype = mesonlib.LibType.STATIC + elif kwargs['static'] is False: + libtype = mesonlib.LibType.SHARED + else: + libtype = mesonlib.LibType.PREFER_SHARED linkargs = self.compiler.find_library(libname, self.environment, search_dirs, libtype) if required and not linkargs: if libtype == mesonlib.LibType.PREFER_SHARED: - libtype = 'shared or static' + libtype_s = 'shared or static' else: - libtype = libtype.name.lower() + libtype_s = libtype.name.lower() raise InterpreterException('{} {} library {!r} not found' .format(self.compiler.get_display_language(), - libtype, libname)) + libtype_s, libname)) lib = dependencies.ExternalLibrary(libname, linkargs, self.environment, self.compiler.language) return lib + def _has_argument_impl(self, arguments: T.Union[str, T.List[str]], + mode: _TestMode = _TestMode.COMPILER) -> bool: + """Shared implementaiton for methods checking compiler and linker arguments.""" + # This simplifies the callers + if isinstance(arguments, str): + arguments = [arguments] + test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments + result, cached = test(arguments, self.environment) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log( + 'Compiler for', + self.compiler.get_display_language(), + 'supports{}'.format(' link' if mode is _TestMode.LINKER else ''), + 'arguments {}:'.format(' '.join(arguments)), + mlog.green('YES') if result else mlog.red('NO'), + cached_msg) + return result + @noKwargs - def has_argument_method(self, args: T.Sequence[str], kwargs) -> bool: - args = mesonlib.stringlistify(args) - if len(args) != 1: - raise InterpreterException('has_argument takes exactly one argument.') - return self.has_multi_arguments_method(args, kwargs) + @typed_pos_args('compiler.has_argument', str) + def has_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return self._has_argument_impl([args[0]]) @noKwargs - def has_multi_arguments_method(self, args: T.Sequence[str], kwargs: dict): - args = mesonlib.stringlistify(args) - result, cached = self.compiler.has_multi_arguments(args, self.environment) - if result: - h = mlog.green('YES') - else: - h = mlog.red('NO') - cached = mlog.blue('(cached)') if cached else '' - mlog.log( - 'Compiler for {} supports arguments {}:'.format( - self.compiler.get_display_language(), ' '.join(args)), - h, cached) - return result + @typed_pos_args('compiler.has_multi_arguments', varargs=str) + def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool: + return self._has_argument_impl(args[0]) @FeatureNew('compiler.get_supported_arguments', '0.43.0') + @typed_pos_args('compiler.get_supported_arguments', varargs=str) @typed_kwargs( 'compiler.get_supported_arguments', KwargInfo('checked', str, default='off', since='0.59.0', validator=lambda s: 'must be one of "warn", "require" or "off"' if s not in ['warn', 'require', 'off'] else None) ) - def get_supported_arguments_method(self, args: T.Sequence[str], kwargs: T.Dict[str, T.Any]): - args = mesonlib.stringlistify(args) - supported_args = [] - checked = kwargs.pop('checked') + def get_supported_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'GetSupportedArgumentKw') -> T.List[str]: + supported_args: T.List[str] = [] + checked = kwargs['checked'] - for arg in args: - if not self.has_argument_method(arg, kwargs): + for arg in args[0]: + if not self._has_argument_impl([arg]): msg = f'Compiler for {self.compiler.get_display_language()} does not support "{arg}"' if checked == 'warn': mlog.warning(msg) @@ -695,9 +656,10 @@ class CompilerHolder(ObjectHolder['Compiler']): return supported_args @noKwargs - def first_supported_argument_method(self, args: T.Sequence[str], kwargs: dict) -> T.List[str]: - for arg in mesonlib.stringlistify(args): - if self.has_argument_method(arg, kwargs): + @typed_pos_args('compiler.first_supported_argument', varargs=str) + def first_supported_argument_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]: + for arg in args[0]: + if self._has_argument_impl([arg]): mlog.log('First supported argument:', mlog.bold(arg)) return [arg] mlog.log('First supported argument:', mlog.red('None')) @@ -705,68 +667,59 @@ class CompilerHolder(ObjectHolder['Compiler']): @FeatureNew('compiler.has_link_argument', '0.46.0') @noKwargs - def has_link_argument_method(self, args, kwargs): - args = mesonlib.stringlistify(args) - if len(args) != 1: - raise InterpreterException('has_link_argument takes exactly one argument.') - return self.has_multi_link_arguments_method(args, kwargs) + @typed_pos_args('compiler.has_link_argument', str) + def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return self._has_argument_impl([args[0]], mode=_TestMode.LINKER) @FeatureNew('compiler.has_multi_link_argument', '0.46.0') @noKwargs - def has_multi_link_arguments_method(self, args, kwargs): - args = mesonlib.stringlistify(args) - result, cached = self.compiler.has_multi_link_arguments(args, self.environment) - cached = mlog.blue('(cached)') if cached else '' - if result: - h = mlog.green('YES') - else: - h = mlog.red('NO') - mlog.log( - 'Compiler for {} supports link arguments {}:'.format( - self.compiler.get_display_language(), ' '.join(args)), - h, cached) - return result + @typed_pos_args('compiler.has_multi_link_argument', varargs=str) + def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool: + return self._has_argument_impl(args[0], mode=_TestMode.LINKER) - @FeatureNew('compiler.get_supported_link_arguments_method', '0.46.0') + @FeatureNew('compiler.get_supported_link_arguments', '0.46.0') @noKwargs - def get_supported_link_arguments_method(self, args, kwargs): - args = mesonlib.stringlistify(args) - supported_args = [] - for arg in args: - if self.has_link_argument_method(arg, kwargs): + @typed_pos_args('compiler.get_supported_link_arguments', varargs=str) + def get_supported_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]: + supported_args: T.List[str] = [] + for arg in args[0]: + if self._has_argument_impl([arg], mode=_TestMode.LINKER): supported_args.append(arg) return supported_args @FeatureNew('compiler.first_supported_link_argument_method', '0.46.0') @noKwargs - def first_supported_link_argument_method(self, args, kwargs): - for i in mesonlib.stringlistify(args): - if self.has_link_argument_method(i, kwargs): - mlog.log('First supported link argument:', mlog.bold(i)) - return [i] + @typed_pos_args('compiler.first_supported_link_argument', varargs=str) + def first_supported_link_argument_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]: + for arg in args[0]: + if self._has_argument_impl([arg], mode=_TestMode.LINKER): + mlog.log('First supported link argument:', mlog.bold(arg)) + return [arg] mlog.log('First supported link argument:', mlog.red('None')) return [] - @FeatureNew('compiler.has_function_attribute', '0.48.0') - @noKwargs - def has_func_attribute_method(self, args, kwargs): - args = mesonlib.stringlistify(args) - if len(args) != 1: - raise InterpreterException('has_func_attribute takes exactly one argument.') - result, cached = self.compiler.has_func_attribute(args[0], self.environment) - cached = mlog.blue('(cached)') if cached else '' + def _has_function_attribute_impl(self, attr: str) -> bool: + """Common helper for function attribute testing.""" + result, cached = self.compiler.has_func_attribute(attr, self.environment) + cached_msg = mlog.blue('(cached)') if cached else '' h = mlog.green('YES') if result else mlog.red('NO') - mlog.log('Compiler for {} supports function attribute {}:'.format(self.compiler.get_display_language(), args[0]), h, cached) + mlog.log('Compiler for {} supports function attribute {}:'.format(self.compiler.get_display_language(), attr), h, cached_msg) return result + @FeatureNew('compiler.has_function_attribute', '0.48.0') + @noKwargs + @typed_pos_args('compiler.has_function_attribute', str) + def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return self._has_function_attribute_impl(args[0]) + @FeatureNew('compiler.get_supported_function_attributes', '0.48.0') @noKwargs - def get_supported_function_attributes_method(self, args, kwargs): - args = mesonlib.stringlistify(args) - return [a for a in args if self.has_func_attribute_method(a, kwargs)] + @typed_pos_args('compiler.get_supported_function_attributes', varargs=str) + def get_supported_function_attributes_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]: + return [a for a in args[0] if self._has_function_attribute_impl(a)] @FeatureNew('compiler.get_argument_syntax_method', '0.49.0') @noPosargs @noKwargs - def get_argument_syntax_method(self, args, kwargs): + def get_argument_syntax_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: return self.compiler.get_argument_syntax() diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index b29634190..a97ba63a7 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -1826,13 +1826,8 @@ class AllPlatformTests(BasePlatformTests): def test_permitted_method_kwargs(self): tdir = os.path.join(self.unit_test_dir, '25 non-permitted kwargs') - out = self.init(tdir) - for expected in [ - r'WARNING: Passed invalid keyword argument "prefixxx".', - r'WARNING: Passed invalid keyword argument "argsxx".', - r'WARNING: Passed invalid keyword argument "invalidxx".', - ]: - self.assertRegex(out, re.escape(expected)) + out = self.init(tdir, allow_fail=True) + self.assertIn('Function does not take keyword arguments.', out) def test_templates(self): ninja = mesonbuild.environment.detect_ninja()