From ab29126232098992702e9bf1194a3e784410dba1 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 1 Nov 2021 13:51:28 -0700 Subject: [PATCH 01/10] interpreter: use typed_kwargs for add_test_setup This also sets a few missing "new kwarg" warnings, as they're quite easy to set with typwed_kwargs --- mesonbuild/interpreter/interpreter.py | 58 ++++++++++++--------------- mesonbuild/interpreter/kwargs.py | 9 +++++ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d9466787b4ad..7518c6f67a1f 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -2345,49 +2345,43 @@ def build_incdir_object(self, incdir_strings: T.List[str], is_system: bool = Fal i = build.IncludeDirs(self.subdir, incdir_strings, is_system) return i - @permittedKwargs({'exe_wrapper', 'gdb', 'timeout_multiplier', 'env', 'is_default', - 'exclude_suites'}) @typed_pos_args('add_test_setup', str) - def func_add_test_setup(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None: + @typed_kwargs( + 'add_test_setup', + KwargInfo('exe_wrapper', ContainerTypeInfo(list, (str, ExternalProgram)), listify=True, default=[]), + KwargInfo('gdb', bool, default=False), + KwargInfo('timeout_multiplier', int, default=1), + KwargInfo('exclude_suites', ContainerTypeInfo(list, str), listify=True, default=[], since='0.57.0'), + KwargInfo('is_default', bool, default=False, since='0.49.0'), + ENV_KW, + ) + def func_add_test_setup(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwargs.AddTestSetup') -> None: setup_name = args[0] if re.fullmatch('([_a-zA-Z][_0-9a-zA-Z]*:)?[_a-zA-Z][_0-9a-zA-Z]*', setup_name) is None: raise InterpreterException('Setup name may only contain alphanumeric characters.') if ":" not in setup_name: - setup_name = (self.subproject if self.subproject else self.build.project_name) + ":" + setup_name - try: - inp = extract_as_list(kwargs, 'exe_wrapper') - exe_wrapper = [] - for i in inp: - if isinstance(i, str): - exe_wrapper.append(i) - elif isinstance(i, ExternalProgram): - if not i.found(): - raise InterpreterException('Tried to use non-found executable.') - exe_wrapper += i.get_command() - else: - raise InterpreterException('Exe wrapper can only contain strings or external binaries.') - except KeyError: - exe_wrapper = None - gdb = kwargs.get('gdb', False) - if not isinstance(gdb, bool): - raise InterpreterException('Gdb option must be a boolean') - timeout_multiplier = kwargs.get('timeout_multiplier', 1) - if not isinstance(timeout_multiplier, int): - raise InterpreterException('Timeout multiplier must be a number.') + setup_name = f'{(self.subproject if self.subproject else self.build.project_name)}: {setup_name}' + + exe_wrapper: T.List[str] = [] + for i in kwargs['exe_wrapper']: + if isinstance(i, str): + exe_wrapper.append(i) + else: + if not i.found(): + raise InterpreterException('Tried to use non-found executable.') + exe_wrapper += i.get_command() + + timeout_multiplier = kwargs['timeout_multiplier'] if timeout_multiplier <= 0: FeatureNew('add_test_setup() timeout_multiplier <= 0', '0.57.0').use(self.subproject) - is_default = kwargs.get('is_default', False) - if not isinstance(is_default, bool): - raise InterpreterException('is_default option must be a boolean') - if is_default: + + if kwargs['is_default']: if self.build.test_setup_default_name is not None: raise InterpreterException(f'{self.build.test_setup_default_name!r} is already set as default. ' 'is_default can be set to true only once') self.build.test_setup_default_name = setup_name - exclude_suites = mesonlib.stringlistify(kwargs.get('exclude_suites', [])) - env = self.unpack_env_kwarg(kwargs) - self.build.test_setups[setup_name] = build.TestSetup(exe_wrapper, gdb, timeout_multiplier, env, - exclude_suites) + self.build.test_setups[setup_name] = build.TestSetup(exe_wrapper, kwargs['gdb'], timeout_multiplier, kwargs['env'], + kwargs['exclude_suites']) @typed_pos_args('add_global_arguments', varargs=str) @typed_kwargs('add_global_arguments', NATIVE_KW, LANGUAGE_KW) diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 2229984c9093..3eca65c20074 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -186,3 +186,12 @@ class CustomTarget(TypedDict): install_tag: T.List[T.Union[str, bool]] output: T.List[str] override_options: T.Dict[OptionKey, str] + +class AddTestSetup(TypedDict): + + exe_wrapper: T.List[T.Union[str, ExternalProgram]] + gdb: bool + timeout_multiplier: int + is_default: bool + exclude_suites: T.List[str] + env: build.EnvironmentVariables From 75d163f56d8264a4e68343367f0b9580d746e21b Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 1 Nov 2021 13:53:09 -0700 Subject: [PATCH 02/10] build: TestSetup.exe_wrapper doesn't need to be optional It works fine as-is with an empty list, and since that's easier to get using our typed_kwargs, and thus is what we're passing, go ahead and simplify the class to only take a list of strings. --- mesonbuild/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 18c09114d38c..721234b39229 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2796,7 +2796,7 @@ def __init__(self, sources: T.List[File], install_dir: str, install_dir_name: st self.data_type = data_type class TestSetup: - def __init__(self, exe_wrapper: T.Optional[T.List[str]], gdb: bool, + def __init__(self, exe_wrapper: T.List[str], gdb: bool, timeout_multiplier: int, env: EnvironmentVariables, exclude_suites: T.List[str]): self.exe_wrapper = exe_wrapper From f34013fb08b8d24d570c96084c5d58c5eaf4f5da Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 3 Nov 2021 09:50:44 -0700 Subject: [PATCH 03/10] interpreter: use typed_kwargs for project --- mesonbuild/interpreter/interpreter.py | 72 +++++++++++++++---------- mesonbuild/interpreter/kwargs.py | 9 ++++ mesonbuild/interpreter/type_checking.py | 9 ++++ 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 7518c6f67a1f..76096e94c777 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -56,6 +56,7 @@ CT_INPUT_KW, CT_INSTALL_DIR_KW, CT_OUTPUT_KW, + DEFAULT_OPTIONS, DEPENDS_KW, DEPEND_FILES_KW, DEPFILE_KW, @@ -97,6 +98,15 @@ build.GeneratedList] +def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) -> T.Optional[str]: + if isinstance(value, list): + if len(value) != 1: + return 'when passed as array must have a length of 1' + elif not isinstance(value[0], (str, mesonlib.File)): + return 'when passed as array must contain a string or File' + return None + + def stringifyUserArguments(args, quote=False): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args]) @@ -1019,16 +1029,29 @@ def set_backend(self): options = {k: v for k, v in self.environment.options.items() if k.is_backend()} self.coredata.set_options(options) - @permittedKwargs({'version', 'meson_version', 'default_options', 'license', 'subproject_dir'}) @typed_pos_args('project', str, varargs=str) - def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str]], kwargs: 'TYPE_kwargs') -> None: + @typed_kwargs( + 'project', + DEFAULT_OPTIONS, + KwargInfo('meson_version', (str, NoneType)), + KwargInfo( + 'version', + (str, mesonlib.File, NoneType, list), + default='undefined', + validator=_project_version_validator, + convertor=lambda x: x[0] if isinstance(x, list) else x, + ), + KwargInfo('license', ContainerTypeInfo(list, str), default=['unknown'], listify=True), + KwargInfo('subproject_dir', str, default='subprojects'), + ) + def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str]], kwargs: 'kwargs.Project') -> None: proj_name, proj_langs = args if ':' in proj_name: raise InvalidArguments(f"Project name {proj_name!r} must not contain ':'") # This needs to be evaluated as early as possible, as meson uses this # for things like deprecation testing. - if 'meson_version' in kwargs: + if kwargs['meson_version']: cv = coredata.version pv = kwargs['meson_version'] if not mesonlib.version_compare(cv, pv): @@ -1045,8 +1068,8 @@ def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str # values previously set from command line. That means that changing # default_options in a project will trigger a reconfigure but won't # have any effect. - self.project_default_options = mesonlib.stringlistify(kwargs.get('default_options', [])) - self.project_default_options = coredata.create_options_dict(self.project_default_options, self.subproject) + self.project_default_options = coredata.create_options_dict( + kwargs['default_options'], self.subproject) # If this is the first invocation we always need to initialize # builtins, if this is a subproject that is new in a re-invocation we @@ -1062,11 +1085,8 @@ def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str if not self.is_subproject(): self.build.project_name = proj_name self.active_projectname = proj_name - version = kwargs.get('version', 'undefined') - if isinstance(version, list): - if len(version) != 1: - raise InvalidCode('Version argument is an array with more than one entry.') - version = version[0] + + version = kwargs['version'] if isinstance(version, mesonlib.File): FeatureNew.single_use('version from file', '0.57.0', self.subproject) self.add_build_def_file(version) @@ -1081,33 +1101,29 @@ def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str if len(ver_data) != 1: raise InterpreterException('Version file must contain exactly one line of text.') self.project_version = ver_data[0] - elif isinstance(version, str): - self.project_version = version else: - raise InvalidCode('The version keyword argument must be a string or a file.') + self.project_version = version + if self.build.project_version is None: self.build.project_version = self.project_version - proj_license = mesonlib.stringlistify(kwargs.get('license', 'unknown')) + proj_license = kwargs['license'] self.build.dep_manifest[proj_name] = build.DepManifest(self.project_version, proj_license) if self.subproject in self.build.projects: raise InvalidCode('Second call to project().') # spdirname is the subproject_dir for this project, relative to self.subdir. # self.subproject_dir is the subproject_dir for the main project, relative to top source dir. - spdirname = kwargs.get('subproject_dir') - if spdirname: - if not isinstance(spdirname, str): - raise InterpreterException('Subproject_dir must be a string') - if os.path.isabs(spdirname): - raise InterpreterException('Subproject_dir must not be an absolute path.') - if spdirname.startswith('.'): - raise InterpreterException('Subproject_dir must not begin with a period.') - if '..' in spdirname: - raise InterpreterException('Subproject_dir must not contain a ".." segment.') - if not self.is_subproject(): - self.subproject_dir = spdirname - else: - spdirname = 'subprojects' + spdirname = kwargs['subproject_dir'] + if not isinstance(spdirname, str): + raise InterpreterException('Subproject_dir must be a string') + if os.path.isabs(spdirname): + raise InterpreterException('Subproject_dir must not be an absolute path.') + if spdirname.startswith('.'): + raise InterpreterException('Subproject_dir must not begin with a period.') + if '..' in spdirname: + raise InterpreterException('Subproject_dir must not contain a ".." segment.') + if not self.is_subproject(): + self.subproject_dir = spdirname self.build.subproject_dir = self.subproject_dir # Load wrap files from this (sub)project. diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 3eca65c20074..00dfdfd3e61e 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -195,3 +195,12 @@ class AddTestSetup(TypedDict): is_default: bool exclude_suites: T.List[str] env: build.EnvironmentVariables + + +class Project(TypedDict): + + version: T.Optional[FileOrString] + meson_version: T.Optional[str] + default_options: T.List[str] + license: T.List[str] + subproject_dir: str diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index 9e443ae8df90..50164eefdae2 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -285,3 +285,12 @@ def _output_validator(outputs: T.List[str]) -> T.Optional[str]: listify=True, default=[], ) + +# for cases like default_options and override_options +DEFAULT_OPTIONS: KwargInfo[T.List[str]] = KwargInfo( + 'default_options', + ContainerTypeInfo(list, (str, IncludeDirs)), + listify=True, + default=[], + validator=_env_validator, +) From 3295621706d47ca6f4731695e9c9acc0ddcc572b Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 3 Nov 2021 10:06:22 -0700 Subject: [PATCH 04/10] interpreter: add typed_kwargs to subdir --- mesonbuild/interpreter/interpreter.py | 20 ++++++++++++++------ mesonbuild/interpreter/kwargs.py | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 76096e94c777..35c26d81ad26 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1946,10 +1946,19 @@ def func_install_emptydir(self, node: mparser.BaseNode, args: T.Tuple[str], kwar return d - @FeatureNewKwargs('subdir', '0.44.0', ['if_found']) - @permittedKwargs({'if_found'}) @typed_pos_args('subdir', str) - def func_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None: + @typed_kwargs( + 'subdir', + KwargInfo( + 'if_found', + ContainerTypeInfo(list, object), + validator=lambda a: 'Objects must have a found() method' if not all(hasattr(x, 'found') for x in a) else None, + since='0.44.0', + default=[], + listify=True, + ), + ) + def func_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwargs.Subdir') -> None: mesonlib.check_direntry_issues(args) if '..' in args[0]: raise InvalidArguments('Subdir contains ..') @@ -1957,11 +1966,10 @@ def func_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_ raise InvalidArguments('Must not go into subprojects dir with subdir(), use subproject() instead.') if self.subdir == '' and args[0].startswith('meson-'): raise InvalidArguments('The "meson-" prefix is reserved and cannot be used for top-level subdir().') - for i in mesonlib.extract_as_list(kwargs, 'if_found'): - if not hasattr(i, 'found'): - raise InterpreterException('Object used in if_found does not have a found method.') + for i in kwargs['if_found']: if not i.found(): return + prev_subdir = self.subdir subdir = os.path.join(prev_subdir, args[0]) if os.path.isabs(subdir): diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 00dfdfd3e61e..50bc5d19e794 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -6,7 +6,7 @@ import typing as T -from typing_extensions import TypedDict, Literal +from typing_extensions import TypedDict, Literal, Protocol from .. import build from .. import coredata @@ -204,3 +204,18 @@ class Project(TypedDict): default_options: T.List[str] license: T.List[str] subproject_dir: str + + +class _FoundProto(Protocol): + + """Protocol for subdir arguments. + + This allows us to define any objec that has a found(self) -> bool method + """ + + def found(self) -> bool: ... + + +class Subdir(TypedDict): + + if_found: T.List[_FoundProto] From d05a0fbf335b23814ede488c668f43c97fd4f06c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 10:53:17 -0700 Subject: [PATCH 05/10] interpreter: use typed_* args for the summary function This also includes a few type annotation cleans for the Summary object. Getting the positional arguments exactly right is impossible, as this is really a function with two different signatures: ``` summary(value: dictionary): void summary(key: string, value: any): void ``` We can get close enough in the typed_pos_args by enforcing that there are two parameters, one required and one optional, and that the first must be either a dictionary or a string. --- mesonbuild/interpreter/interpreter.py | 45 +++++++++++++-------------- mesonbuild/interpreter/kwargs.py | 7 +++++ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 35c26d81ad26..6a30cdda697e 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -121,19 +121,14 @@ def stringifyUserArguments(args, quote=False): raise InvalidArguments('Function accepts only strings, integers, bools, lists, dictionaries and lists thereof.') class Summary: - def __init__(self, project_name, project_version): + def __init__(self, project_name: str, project_version: str): self.project_name = project_name self.project_version = project_version self.sections = collections.defaultdict(dict) self.max_key_len = 0 - def add_section(self, section, values, kwargs, subproject): - bool_yn = kwargs.get('bool_yn', False) - if not isinstance(bool_yn, bool): - raise InterpreterException('bool_yn keyword argument must be boolean') - list_sep = kwargs.get('list_sep') - if list_sep is not None and not isinstance(list_sep, str): - raise InterpreterException('list_sep keyword argument must be string') + def add_section(self, section: str, values: T.Dict[str, T.Any], bool_yn: bool, + list_sep: T.Optional[str], subproject: str) -> None: for k, v in values.items(): if k in self.sections[section]: raise InterpreterException(f'Summary section {section!r} already have key {k!r}') @@ -267,7 +262,7 @@ def __init__( self.environment = self.build.environment self.coredata = self.environment.get_coredata() self.backend = backend - self.summary = {} + self.summary: T.Dict[str, 'Summary'] = {} self.modules = {} # Subproject directory is usually the name of the subproject, but can # be different for dependencies provided by wrap files. @@ -1193,31 +1188,33 @@ def message_impl(self, args): mlog.log(mlog.bold('Message:'), *args) @noArgsFlattening - @FeatureNewKwargs('summary', '0.54.0', ['list_sep']) - @permittedKwargs({'section', 'bool_yn', 'list_sep'}) @FeatureNew('summary', '0.53.0') - def func_summary(self, node, args, kwargs): - if len(args) == 1: + @typed_pos_args('summary', (str, dict), optargs=[object]) + @typed_kwargs( + 'summary', + KwargInfo('section', str, default=''), + KwargInfo('bool_yn', bool, default=False), + KwargInfo('list_sep', (str, NoneType), since='0.54.0') + ) + def func_summary(self, node: mparser.BaseNode, args: T.Tuple[T.Union[str, T.Dict[str, T.Any]], T.Optional[T.Any]], + kwargs: 'kwargs.Summary') -> None: + if args[1] is None: if not isinstance(args[0], dict): raise InterpreterException('Summary first argument must be dictionary.') values = args[0] - elif len(args) == 2: + else: if not isinstance(args[0], str): raise InterpreterException('Summary first argument must be string.') values = {args[0]: args[1]} - else: - raise InterpreterException('Summary accepts at most 2 arguments.') - section = kwargs.get('section', '') - if not isinstance(section, str): - raise InterpreterException('Summary\'s section keyword argument must be string.') - self.summary_impl(section, values, kwargs) + self.summary_impl(kwargs['section'], values, kwargs) - def summary_impl(self, section, values, kwargs): + def summary_impl(self, section: str, values, kwargs: 'kwargs.Summary') -> None: if self.subproject not in self.summary: self.summary[self.subproject] = Summary(self.active_projectname, self.project_version) - self.summary[self.subproject].add_section(section, values, kwargs, self.subproject) + self.summary[self.subproject].add_section( + section, values, kwargs['bool_yn'], kwargs['list_sep'], self.subproject) - def _print_summary(self): + def _print_summary(self) -> None: # Add automatic 'Supbrojects' section in main project. all_subprojects = collections.OrderedDict() for name, subp in sorted(self.subprojects.items()): @@ -1244,7 +1241,7 @@ def _print_summary(self): sorted_options = sorted(self.user_defined_options.cmd_line_options.items()) values.update({str(k): v for k, v in sorted_options}) if values: - self.summary_impl('User defined options', values, {}) + self.summary_impl('User defined options', values, {'bool_yn': False, 'list_sep': None}) # Print all summaries, main project last. mlog.log('') # newline main_summary = self.summary.pop('', None) diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 50bc5d19e794..4777d8ac4e7c 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -219,3 +219,10 @@ def found(self) -> bool: ... class Subdir(TypedDict): if_found: T.List[_FoundProto] + + +class Summary(TypedDict): + + section: str + bool_yn: bool + list_sep: T.Optional[str] From 38b4673a94201075e9d24d14777d2f0ea121fd38 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 11:31:55 -0700 Subject: [PATCH 06/10] interpreter: add type annotations to find_program_impl --- mesonbuild/interpreter/interpreter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 6a30cdda697e..88cec1042d82 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1408,9 +1408,13 @@ def notfound_program(self, args): # TODO update modules to always pass `for_machine`. It is bad-form to assume # the host machine. - def find_program_impl(self, args, for_machine: MachineChoice = MachineChoice.HOST, - required=True, silent=True, wanted='', search_dirs=None, - version_func=None): + def find_program_impl(self, args: T.List[mesonlib.FileOrString], + for_machine: MachineChoice = MachineChoice.HOST, + required: bool = True, silent: bool = True, + wanted: T.Union[str, T.List[str]] = '', + search_dirs: T.Optional[T.List[str]] = None, + version_func: T.Optional[T.Callable[[T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']], str]] = None + ) -> T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']: args = mesonlib.listify(args) extra_info = [] @@ -1434,7 +1438,7 @@ def find_program_impl(self, args, for_machine: MachineChoice = MachineChoice.HOS interp = self.subprojects[progobj.subproject].held_object assert isinstance(interp, Interpreter) version = interp.project_version - elif isinstance(progobj, ExternalProgram): + else: version = progobj.get_version(self) is_found, not_found, found = mesonlib.version_compare_many(version, wanted) if not is_found: From f9478c573b9ec37772f8f529608f6897eb8bf968 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 11:32:54 -0700 Subject: [PATCH 07/10] interpreter: use typed_pos_args for find_program --- mesonbuild/interpreter/interpreter.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 88cec1042d82..d591a49dd7a4 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1493,19 +1493,17 @@ def find_program_fallback(self, fallback, args, required, extra_info): @FeatureNewKwargs('find_program', '0.49.0', ['disabler']) @disablerIfNotFound @permittedKwargs({'required', 'native', 'version', 'dirs'}) - def func_find_program(self, node, args, kwargs) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']: - if not args: - raise InterpreterException('No program name specified.') - + @typed_pos_args('find_program', varargs=(str, mesonlib.File), min_varargs=1) + def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]], kwargs) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']: disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: - mlog.log('Program', mlog.bold(' '.join(args)), 'skipped: feature', mlog.bold(feature), 'disabled') - return self.notfound_program(args) + mlog.log('Program', mlog.bold(' '.join(args[0])), 'skipped: feature', mlog.bold(feature), 'disabled') + return self.notfound_program(args[0]) search_dirs = extract_search_dirs(kwargs) wanted = mesonlib.stringlistify(kwargs.get('version', [])) for_machine = self.machine_from_native_kwarg(kwargs) - return self.find_program_impl(args, for_machine, required=required, + return self.find_program_impl(args[0], for_machine, required=required, silent=False, wanted=wanted, search_dirs=search_dirs) @@ -2385,7 +2383,7 @@ def func_add_test_setup(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs if re.fullmatch('([_a-zA-Z][_0-9a-zA-Z]*:)?[_a-zA-Z][_0-9a-zA-Z]*', setup_name) is None: raise InterpreterException('Setup name may only contain alphanumeric characters.') if ":" not in setup_name: - setup_name = f'{(self.subproject if self.subproject else self.build.project_name)}: {setup_name}' + setup_name = f'{(self.subproject if self.subproject else self.build.project_name)}:{setup_name}' exe_wrapper: T.List[str] = [] for i in kwargs['exe_wrapper']: From 5593352b87a2cd931fa4e7e5b1e4fa3c4e7fb996 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 11:37:51 -0700 Subject: [PATCH 08/10] interpreter: move disabler KwargInfo to the type_checking module --- mesonbuild/interpreter/interpreter.py | 3 ++- mesonbuild/interpreter/type_checking.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d591a49dd7a4..b11a0009d997 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -60,6 +60,7 @@ DEPENDS_KW, DEPEND_FILES_KW, DEPFILE_KW, + DISABLER_KW, ENV_KW, INSTALL_KW, INSTALL_MODE_KW, @@ -570,7 +571,7 @@ def _import_module(self, modname: str, required: bool) -> T.Union[ExtensionModul @typed_kwargs( 'import', REQUIRED_KW.evolve(since='0.59.0'), - KwargInfo('disabler', bool, default=False, since='0.59.0'), + DISABLER_KW.evolve(since='0.59.0'), ) @disablerIfNotFound def func_import(self, node: mparser.BaseNode, args: T.Tuple[str], diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index 50164eefdae2..5391e9f6c513 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -129,6 +129,8 @@ def _lower_strlist(input: T.List[str]) -> T.List[str]: # TODO: extract_required_kwarg could be converted to a convertor ) +DISABLER_KW: KwargInfo[bool] = KwargInfo('disabler', bool, default=False) + def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None]) -> T.Optional[str]: def _splitter(v: str) -> T.Optional[str]: split = v.split('=', 1) From 822b087f2496ab06e71d7c0a7f85fb36fd01f16d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 12:32:25 -0700 Subject: [PATCH 09/10] interpreter: use find_program_impl internally instead of func_find_program Calling interpreter implementation methods is just a bad idea, apart from the extra type checking that goes into it, we need to pass more arguments than we need to calling the impl method. --- mesonbuild/interpreter/interpreter.py | 6 +++--- mesonbuild/interpreter/mesonmain.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index b11a0009d997..3fb0fc377369 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1751,7 +1751,7 @@ def _func_custom_target_impl(self, node, args, kwargs): kwargs['env'] = self.unpack_env_kwarg(kwargs) if 'command' in kwargs and isinstance(kwargs['command'], list) and kwargs['command']: if isinstance(kwargs['command'][0], str): - kwargs['command'][0] = self.func_find_program(node, kwargs['command'][0], {}) + kwargs['command'][0] = self.find_program_impl([kwargs['command'][0]]) tg = build.CustomTarget(name, self.subdir, self.subproject, kwargs, backend=self.backend) self.add_target(tg.name, tg) return tg @@ -1771,7 +1771,7 @@ def func_run_target(self, node: mparser.FunctionNode, args: T.Tuple[str], if isinstance(i, ExternalProgram) and not i.found(): raise InterpreterException(f'Tried to use non-existing executable {i.name!r}') if isinstance(all_args[0], str): - all_args[0] = self.func_find_program(node, all_args[0], {}) + all_args[0] = self.find_program_impl([all_args[0]]) name = args[0] tg = build.RunTarget(name, all_args, kwargs['depends'], self.subdir, self.subproject, kwargs['env']) self.add_target(name, tg) @@ -1849,7 +1849,7 @@ def make_test(self, node: mparser.BaseNode, name = name.replace(':', '_') exe = args[1] if isinstance(exe, mesonlib.File): - exe = self.func_find_program(node, args[1], {}) + exe = self.find_program_impl([exe]) env = self.unpack_env_kwarg(kwargs) diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py index adc342f296df..e973c5d12324 100644 --- a/mesonbuild/interpreter/mesonmain.py +++ b/mesonbuild/interpreter/mesonmain.py @@ -86,7 +86,7 @@ def _find_source_script( largs.append(prog) largs.extend(args) return self.interpreter.backend.get_executable_serialisation(largs) - found = self.interpreter.func_find_program({}, prog, {}) + found = self.interpreter.find_program_impl([prog]) largs.append(found) largs.extend(args) es = self.interpreter.backend.get_executable_serialisation(largs) From c167a7691f8c9dc98b3035922b007dea65a6806c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 4 Nov 2021 11:44:00 -0700 Subject: [PATCH 10/10] interpreter: use typed_kwargs for find_program --- mesonbuild/interpreter/interpreter.py | 24 ++++++++++++++---------- mesonbuild/interpreter/kwargs.py | 6 ++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 3fb0fc377369..27cf9b48ffae 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1489,23 +1489,27 @@ def find_program_fallback(self, fallback, args, required, extra_info): self.do_subproject(fallback, 'meson', sp_kwargs) return self.program_from_overrides(args, extra_info) - @FeatureNewKwargs('find_program', '0.53.0', ['dirs']) - @FeatureNewKwargs('find_program', '0.52.0', ['version']) - @FeatureNewKwargs('find_program', '0.49.0', ['disabler']) - @disablerIfNotFound - @permittedKwargs({'required', 'native', 'version', 'dirs'}) @typed_pos_args('find_program', varargs=(str, mesonlib.File), min_varargs=1) - def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]], kwargs) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']: + @typed_kwargs( + 'find_program', + DISABLER_KW.evolve(since='0.49.0'), + NATIVE_KW, + REQUIRED_KW, + KwargInfo('dirs', ContainerTypeInfo(list, str), default=[], listify=True, since='0.53.0'), + KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True, since='0.52.0'), + ) + @disablerIfNotFound + def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]], + kwargs: 'kwargs.FindProgram', + ) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']: disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: mlog.log('Program', mlog.bold(' '.join(args[0])), 'skipped: feature', mlog.bold(feature), 'disabled') return self.notfound_program(args[0]) search_dirs = extract_search_dirs(kwargs) - wanted = mesonlib.stringlistify(kwargs.get('version', [])) - for_machine = self.machine_from_native_kwarg(kwargs) - return self.find_program_impl(args[0], for_machine, required=required, - silent=False, wanted=wanted, + return self.find_program_impl(args[0], kwargs['native'], required=required, + silent=False, wanted=kwargs['version'], search_dirs=search_dirs) def func_find_library(self, node, args, kwargs): diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 4777d8ac4e7c..4f8cf0693653 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -226,3 +226,9 @@ class Summary(TypedDict): section: str bool_yn: bool list_sep: T.Optional[str] + + +class FindProgram(ExtractRequired, ExtractSearchDirs): + + native: MachineChoice + version: T.List[str]