diff options
-rw-r--r-- | docs/markdown/snippets/string_format.md | 10 | ||||
-rw-r--r-- | docs/yaml/elementary/str.yml | 4 | ||||
-rw-r--r-- | mesonbuild/interpreter/interpreter.py | 29 | ||||
-rw-r--r-- | mesonbuild/interpreter/primitives/string.py | 13 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/__init__.py | 8 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/helpers.py | 25 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/interpreterbase.py | 13 | ||||
-rw-r--r-- | test cases/common/264 format string/meson.build | 20 | ||||
-rw-r--r-- | test cases/common/264 format string/meson_options.txt | 1 | ||||
-rw-r--r-- | test cases/common/264 format string/test.json | 28 |
10 files changed, 119 insertions, 32 deletions
diff --git a/docs/markdown/snippets/string_format.md b/docs/markdown/snippets/string_format.md new file mode 100644 index 0000000..fa33617 --- /dev/null +++ b/docs/markdown/snippets/string_format.md @@ -0,0 +1,10 @@ +## Unified message(), str.format() and f-string formatting + +They now all support the same set of values: strings, integers, bools, options, +dictionaries and lists thereof. + +- Feature options (i.e. enabled, disabled, auto) were not previously supported + by any of those functions. +- Lists and dictionaries were not previously supported by f-string. +- str.format() allowed any type and often resulted in printing the internal + representation which is now deprecated. diff --git a/docs/yaml/elementary/str.yml b/docs/yaml/elementary/str.yml index 7748121..83ab3dd 100644 --- a/docs/yaml/elementary/str.yml +++ b/docs/yaml/elementary/str.yml @@ -14,6 +14,10 @@ methods: See [the Meson syntax entry](Syntax.md#string-formatting) for more information. + + *Since 1.3.0* values other than strings, integers, bools, options, + dictionaries and lists thereof are deprecated. They were previously printing + the internal representation of the raw Python object. example: | ```meson template = 'string: @0@, number: @1@, bool: @2@' diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index dc24312..ff9b9ee 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -35,6 +35,7 @@ from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCod from ..interpreterbase import Disabler, disablerIfNotFound from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureBroken, FeatureNewKwargs from ..interpreterbase import ObjectHolder, ContextManagerObject +from ..interpreterbase import stringifyUserArguments from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule from . import interpreterobjects as OBJ @@ -139,20 +140,6 @@ def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) return 'when passed as array must contain a File' return None - -def stringifyUserArguments(args: T.List[T.Any], quote: bool = False) -> str: - if isinstance(args, list): - return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args]) - elif isinstance(args, dict): - return '{%s}' % ', '.join(['{} : {}'.format(stringifyUserArguments(k, True), stringifyUserArguments(v, True)) for k, v in args.items()]) - elif isinstance(args, bool): - return 'true' if args else 'false' - elif isinstance(args, int): - return str(args) - elif isinstance(args, str): - return f"'{args}'" if quote else args - raise InvalidArguments('Function accepts only strings, integers, bools, lists, dictionaries and lists thereof.') - class Summary: def __init__(self, project_name: str, project_version: str): self.project_name = project_name @@ -1343,12 +1330,18 @@ class Interpreter(InterpreterBase, HoldableObject): success &= self.add_languages(langs, required, MachineChoice.HOST) return success + def _stringify_user_arguments(self, args: T.List[TYPE_var], func_name: str) -> T.List[str]: + try: + return [stringifyUserArguments(i, self.subproject) for i in args] + except InvalidArguments as e: + raise InvalidArguments(f'{func_name}(): {str(e)}') + @noArgsFlattening @noKwargs def func_message(self, node: mparser.BaseNode, args, kwargs): if len(args) > 1: FeatureNew.single_use('message with more than one argument', '0.54.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'message') self.message_impl(args_str) def message_impl(self, args): @@ -1427,7 +1420,7 @@ class Interpreter(InterpreterBase, HoldableObject): def func_warning(self, node, args, kwargs): if len(args) > 1: FeatureNew.single_use('warning with more than one argument', '0.54.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'warning') mlog.warning(*args_str, location=node) @noArgsFlattening @@ -1435,14 +1428,14 @@ class Interpreter(InterpreterBase, HoldableObject): def func_error(self, node, args, kwargs): if len(args) > 1: FeatureNew.single_use('error with more than one argument', '0.58.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'error') raise InterpreterException('Problem encountered: ' + ' '.join(args_str)) @noArgsFlattening @FeatureNew('debug', '0.63.0') @noKwargs def func_debug(self, node, args, kwargs): - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'debug') mlog.debug('Debug:', *args_str) @noKwargs diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py index b825128..bc98934 100644 --- a/mesonbuild/interpreter/primitives/string.py +++ b/mesonbuild/interpreter/primitives/string.py @@ -17,8 +17,9 @@ from ...interpreterbase import ( noKwargs, noPosargs, typed_pos_args, - InvalidArguments, + FeatureBroken, + stringifyUserArguments, ) @@ -90,12 +91,14 @@ class StringHolder(ObjectHolder[str]): @noArgsFlattening @noKwargs @typed_pos_args('str.format', varargs=object) - def format_method(self, args: T.Tuple[T.List[object]], kwargs: TYPE_kwargs) -> str: + def format_method(self, args: T.Tuple[T.List[TYPE_var]], kwargs: TYPE_kwargs) -> str: arg_strings: T.List[str] = [] for arg in args[0]: - if isinstance(arg, bool): # Python boolean is upper case. - arg = str(arg).lower() - arg_strings.append(str(arg)) + try: + arg_strings.append(stringifyUserArguments(arg, self.subproject)) + except InvalidArguments as e: + FeatureBroken.single_use(f'str.format: {str(e)}', '1.3.0', self.subproject, location=self.current_node) + arg_strings.append(str(arg)) def arg_replace(match: T.Match[str]) -> str: idx = int(match.group(1)) diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index f0c2002..3cb9530 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -35,6 +35,7 @@ __all__ = [ 'default_resolve_key', 'flatten', 'resolve_second_level_holders', + 'stringifyUserArguments', 'noPosargs', 'noKwargs', @@ -134,6 +135,11 @@ from .exceptions import ( ) from .disabler import Disabler, is_disabled -from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .helpers import ( + default_resolve_key, + flatten, + resolve_second_level_holders, + stringifyUserArguments, +) from .interpreterbase import InterpreterBase from .operator import MesonOperator diff --git a/mesonbuild/interpreterbase/helpers.py b/mesonbuild/interpreterbase/helpers.py index 2196b4e..f2ee1b1 100644 --- a/mesonbuild/interpreterbase/helpers.py +++ b/mesonbuild/interpreterbase/helpers.py @@ -14,13 +14,15 @@ from __future__ import annotations from .. import mesonlib, mparser -from .exceptions import InterpreterException +from .exceptions import InterpreterException, InvalidArguments +from ..coredata import UserOption + import collections.abc import typing as T if T.TYPE_CHECKING: - from .baseobjects import TYPE_var, TYPE_kwargs + from .baseobjects import TYPE_var, TYPE_kwargs, SubProject def flatten(args: T.Union['TYPE_var', T.List['TYPE_var']]) -> T.List['TYPE_var']: if isinstance(args, mparser.StringNode): @@ -54,3 +56,22 @@ def default_resolve_key(key: mparser.BaseNode) -> str: if not isinstance(key, mparser.IdNode): raise InterpreterException('Invalid kwargs format.') return key.value + +def stringifyUserArguments(args: TYPE_var, subproject: SubProject, quote: bool = False) -> str: + if isinstance(args, str): + return f"'{args}'" if quote else args + elif isinstance(args, bool): + return 'true' if args else 'false' + elif isinstance(args, int): + return str(args) + elif isinstance(args, list): + return '[%s]' % ', '.join([stringifyUserArguments(x, subproject, True) for x in args]) + elif isinstance(args, dict): + l = ['{} : {}'.format(stringifyUserArguments(k, subproject, True), + stringifyUserArguments(v, subproject, True)) for k, v in args.items()] + return '{%s}' % ', '.join(l) + elif isinstance(args, UserOption): + from .decorators import FeatureNew + FeatureNew.single_use('User option in string format', '1.3.0', subproject) + return stringifyUserArguments(args.printable_value(), subproject) + raise InvalidArguments('Value other than strings, integers, bools, options, dictionaries and lists thereof.') diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 9aff5b9..d23a23d 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -40,7 +40,7 @@ from .exceptions import ( from .decorators import FeatureNew from .disabler import Disabler, is_disabled -from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .helpers import default_resolve_key, flatten, resolve_second_level_holders, stringifyUserArguments from .operator import MesonOperator from ._unholder import _unholder @@ -433,11 +433,12 @@ class InterpreterBase: var = str(match.group(1)) try: val = _unholder(self.variables[var]) - if not isinstance(val, (str, int, float, bool)): - raise InvalidCode(f'Identifier "{var}" does not name a formattable variable ' + - '(has to be an integer, a string, a floating point number or a boolean).') - - return str(val) + if isinstance(val, (list, dict)): + FeatureNew.single_use('List or dictionary in f-string', '1.3.0', self.subproject, location=self.current_node) + try: + return stringifyUserArguments(val, self.subproject) + except InvalidArguments as e: + raise InvalidArguments(f'f-string: {str(e)}') except KeyError: raise InvalidCode(f'Identifier "{var}" does not name a variable.') diff --git a/test cases/common/264 format string/meson.build b/test cases/common/264 format string/meson.build new file mode 100644 index 0000000..200cfac --- /dev/null +++ b/test cases/common/264 format string/meson.build @@ -0,0 +1,20 @@ +project('test format string') + +# Test all supported types in message(), format(), f-string. +foreach t : [get_option('opt'), 42, true, false, 'str', ['list'], {'dict': 'value'}] + message(t, '@0@'.format(t), f'@t@', [t], {'key': t}) +endforeach + +# Deprecated but should work with str.format(). +env = environment() +message('@0@'.format(env)) + +# Should fail with f-string and message() +error_msg = 'Value other than strings, integers, bools, options, dictionaries and lists thereof.' +testcase expect_error('message(): ' + error_msg) + message(env) +endtestcase + +testcase expect_error('f-string: ' + error_msg) + message(f'@env@') +endtestcase diff --git a/test cases/common/264 format string/meson_options.txt b/test cases/common/264 format string/meson_options.txt new file mode 100644 index 0000000..2e39176 --- /dev/null +++ b/test cases/common/264 format string/meson_options.txt @@ -0,0 +1 @@ +option('opt', type: 'feature') diff --git a/test cases/common/264 format string/test.json b/test cases/common/264 format string/test.json new file mode 100644 index 0000000..2369864 --- /dev/null +++ b/test cases/common/264 format string/test.json @@ -0,0 +1,28 @@ +{ + "stdout": [ + { + "line": "Message: auto auto auto [auto] {'key' : auto}" + }, + { + "line": "Message: 42 42 42 [42] {'key' : 42}" + }, + { + "line": "Message: true true true [true] {'key' : true}" + }, + { + "line": "Message: false false false [false] {'key' : false}" + }, + { + "line": "Message: str str str ['str'] {'key' : 'str'}" + }, + { + "line": "Message: ['list'] ['list'] ['list'] [['list']] {'key' : ['list']}" + }, + { + "line": "Message: {'dict' : 'value'} {'dict' : 'value'} {'dict' : 'value'} [{'dict' : 'value'}] {'key' : {'dict' : 'value'}}" + }, + { + "line": "Message: <EnvironmentVariables: []>" + } + ] + } |