From e4b644b9c7699bc1ab3433057af228bb7678759b Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 2 Jun 2026 10:44:43 -0700 Subject: [PATCH] interpreter: Add a default() function This gives a way to conditionally override a keyword argument, such as when setting a value specifically for one platform. For example, ```meson library( ..., name_prefix : host_machine.system() == 'windows' ? '' : default(), ) ``` --- data/syntax-highlighting/vim/syntax/meson.vim | 1 + docs/markdown/snippets/default.md | 13 +++++ docs/yaml/functions/default.yaml | 4 ++ docs/yaml/objects/default.yaml | 5 ++ mesonbuild/ast/interpreter.py | 1 + mesonbuild/interpreter/interpreter.py | 13 +++-- mesonbuild/interpreterbase/__init__.py | 2 + mesonbuild/interpreterbase/baseobjects.py | 4 ++ mesonbuild/interpreterbase/decorators.py | 23 +++++++++ mesonbuild/interpreterbase/interpreterbase.py | 8 +++- test cases/common/300 default/meson.build | 48 +++++++++++++++++++ test cases/common/300 default/prog.cpp | 8 ++++ 12 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 docs/markdown/snippets/default.md create mode 100644 docs/yaml/functions/default.yaml create mode 100644 docs/yaml/objects/default.yaml create mode 100644 test cases/common/300 default/meson.build create mode 100644 test cases/common/300 default/prog.cpp diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 905e9fe64..63429ef45 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -86,6 +86,7 @@ syn keyword mesonBuiltin \ custom_target \ debug \ declare_dependency + \ default \ dependency \ disabler \ environment diff --git a/docs/markdown/snippets/default.md b/docs/markdown/snippets/default.md new file mode 100644 index 000000000..f2e4b7351 --- /dev/null +++ b/docs/markdown/snippets/default.md @@ -0,0 +1,13 @@ +## Add a new `default()` function and object + +This function and object allow setting a keyword argument to it's default value. +This is especially useful for cases when a non-default value is wanted in some +cases but not all. + +For example: +```meson +library( + ... + name_prefix : host_machine.system() == 'windows' ? '' : default(), +) +``` diff --git a/docs/yaml/functions/default.yaml b/docs/yaml/functions/default.yaml new file mode 100644 index 000000000..68ce8d10d --- /dev/null +++ b/docs/yaml/functions/default.yaml @@ -0,0 +1,4 @@ +name: default +returns: default +description: Returns a [[@default]] object. +since: 1.12.0 diff --git a/docs/yaml/objects/default.yaml b/docs/yaml/objects/default.yaml new file mode 100644 index 000000000..9cb7bad09 --- /dev/null +++ b/docs/yaml/objects/default.yaml @@ -0,0 +1,5 @@ +name: default +long_name: Default +description: | + A default object can be passed to keyword arguments to get the default value. + diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 035ee2af2..f1ac3500e 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -242,6 +242,7 @@ class AstInterpreter(InterpreterBase): 'is_disabler': self.func_do_nothing, 'is_variable': self.func_do_nothing, 'disabler': self.func_do_nothing, + 'default': self.func_do_nothing, 'jar': self.func_do_nothing, 'warning': self.func_do_nothing, 'shared_module': self.func_do_nothing, diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index f4cb0b653..2a5a2e97b 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -32,7 +32,7 @@ from .decorators import apply_machine_map from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest from ..interpreterbase import Disabler, disablerIfNotFound from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureBroken, FeatureNewKwargs -from ..interpreterbase import ObjectHolder, ContextManagerObject +from ..interpreterbase import ObjectHolder, ContextManagerObject, DefaultObject from ..interpreterbase import stringifyUserArguments, Feature, FeatureValue from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule, __path__ as modules_path from ..optinterpreter import optname_regex @@ -390,6 +390,7 @@ class Interpreter(InterpreterBase, HoldableObject): 'declare_dependency': self.func_declare_dependency, 'dependency': self.func_dependency, 'disabler': self.func_disabler, + 'default': self.func_default, 'environment': self.func_environment, 'error': self.func_error, 'executable': self.func_executable, @@ -1972,6 +1973,12 @@ class Interpreter(InterpreterBase, HoldableObject): f.use(self.subproject, node) return d + @FeatureNew('default', '1.12.0') + @noKwargs + @noPosargs + def func_default(self, node: mparser.BaseNode, args: list[TYPE_var], kwargs: TYPE_kwargs) -> DefaultObject: + return DefaultObject() + @FeatureNew('disabler', '0.44.0') @noKwargs @noPosargs @@ -4056,7 +4063,7 @@ class Interpreter(InterpreterBase, HoldableObject): def is_subproject(self) -> bool: return self.subproject != '' - @typed_pos_args('set_variable', str, object) + @typed_pos_args('set_variable', str, (object, DefaultObject)) @noKwargs @noArgsFlattening @noSecondLevelHolderResolving @@ -4066,7 +4073,7 @@ class Interpreter(InterpreterBase, HoldableObject): raise InvalidCode('Invalid variable name: ' + varname) self.set_variable(varname, value, holderify=True) - @typed_pos_args('get_variable', (str, Disabler), optargs=[object]) + @typed_pos_args('get_variable', (str, Disabler, DefaultObject), optargs=[object]) @noKwargs @noArgsFlattening @unholder_return diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index dddb2027f..9004b09af 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -8,6 +8,7 @@ __all__ = [ 'IterableObject', 'MutableInterpreterObject', 'ContextManagerObject', + 'DefaultObject', 'MesonOperator', @@ -70,6 +71,7 @@ from .baseobjects import ( IterableObject, MutableInterpreterObject, ContextManagerObject, + DefaultObject, TV_func, TYPE_elementary, diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index 39f3cebdc..23407a42f 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -233,3 +233,7 @@ class IterableObject(metaclass=SimpleABC): class ContextManagerObject(MesonInterpreterObject, AbstractContextManager): def __init__(self, subproject: 'SubProject') -> None: super().__init__(subproject=subproject) + + +class DefaultObject(MesonInterpreterObject): + """A default value to a keyword argument.""" diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index bfd7cc24a..ff2848cc2 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -5,6 +5,7 @@ from __future__ import annotations from .. import coredata, mesonlib, mlog from .disabler import Disabler +from .baseobjects import DefaultObject from .exceptions import InterpreterException, InvalidArguments from ._unholder import _unholder @@ -216,6 +217,7 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], num_args = len(args) num_types = len(types) a_types = types + last_pos = num_args if varargs: min_args = num_types + min_varargs @@ -238,6 +240,18 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1): if not isinstance(arg, type_): + # if DefaultObject is an explicit allowed allowed type allow + # it through. + if isinstance(arg, DefaultObject): + if i >= last_pos: + if varargs: + msg = 'not allowed for variadic arguments' + else: + msg = 'not allowed for optional positional arguments' + else: + msg = 'not allowed for required positional arguments' + raise InvalidArguments(f'default() objects are {msg}') + if isinstance(type_, tuple): shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_)) else: @@ -565,6 +579,15 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T for info in types: types_tuple = info.types if isinstance(info.types, tuple) else (info.types,) value = kwargs.get(info.name) + if isinstance(value, DefaultObject): + # Ensure that default() is not used for required options + # Otherwise, set the value to None, which will send us down + # the "unset" path + if info.required: + raise InvalidArguments(f'"{name}" got a default() value for the required keyword argument "{info.name}". ' + 'default() may not be used for required keyword arguments.') + value = None + if value is not None: if info.since: feature_name = info.name + ' arg in ' + name diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 3899858f5..247e25e83 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -14,6 +14,7 @@ from .baseobjects import ( ObjectHolder, IterableObject, ContextManagerObject, + DefaultObject, HoldableTypes, ) @@ -612,7 +613,12 @@ class InterpreterBase: def expand_default_kwargs(self, kwargs: T.Dict[str, T.Optional[InterpreterObject]]) -> T.Dict[str, T.Optional[InterpreterObject]]: if 'kwargs' not in kwargs: return kwargs - to_expand = _unholder(kwargs.pop('kwargs')) + + kwargs_var = kwargs.pop('kwargs') + if isinstance(kwargs_var, DefaultObject): + return kwargs + + to_expand = _unholder(kwargs_var) if not isinstance(to_expand, dict): raise InterpreterException('Value of "kwargs" must be dictionary.') if 'kwargs' in to_expand: diff --git a/test cases/common/300 default/meson.build b/test cases/common/300 default/meson.build new file mode 100644 index 000000000..7133e5e6a --- /dev/null +++ b/test cases/common/300 default/meson.build @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2026 Intel Corporation + +project( + 'default', + 'cpp', + meson_version : '>= 1.12.0', + default_options : {'cpp_std' : 'c++14'}, +) + +exe = executable( + 'prog', + 'prog.cpp', + override_options : default(), +) + +exe2 = executable( + 'prog2', + 'prog.cpp', + kwargs : default(), +) + +test_kwargs = { + 'protocol' : default(), +} + +test('exe_test', exe, kwargs : test_kwargs) + +testcase expect_error('default() objects are not allowed for required positional arguments') + executable(default(), 'prog.cpp') +endtestcase + +testcase expect_error('default() objects are not allowed for variadic arguments') + add_languages(default()) +endtestcase + +testcase expect_error('default() objects are not allowed for optional positional arguments') + assert(true, default()) +endtestcase + +testcase expect_error('"install_symlink" got a default() value for the required keyword argument "install_dir". default() may not be used for required keyword arguments.') + install_symlink('foo', pointing_to : '/foo', install_dir : default()) +endtestcase + +foo = default() +get_variable('foo') +unset_variable('foo') +set_variable('var', default()) diff --git a/test cases/common/300 default/prog.cpp b/test cases/common/300 default/prog.cpp new file mode 100644 index 000000000..54a466f64 --- /dev/null +++ b/test cases/common/300 default/prog.cpp @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright © 2026 Intel Corporation + */ + +int main() { + return __cplusplus == 201402L ? 0 : 1; +}