aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/markdown/snippets/string_format.md10
-rw-r--r--docs/yaml/elementary/str.yml4
-rw-r--r--mesonbuild/interpreter/interpreter.py29
-rw-r--r--mesonbuild/interpreter/primitives/string.py13
-rw-r--r--mesonbuild/interpreterbase/__init__.py8
-rw-r--r--mesonbuild/interpreterbase/helpers.py25
-rw-r--r--mesonbuild/interpreterbase/interpreterbase.py13
-rw-r--r--test cases/common/264 format string/meson.build20
-rw-r--r--test cases/common/264 format string/meson_options.txt1
-rw-r--r--test cases/common/264 format string/test.json28
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: []>"
+ }
+ ]
+ }