Unify message(), format() and fstring formatting

Share a common function to convert objects to display strings for
consistency.

While at it, also add support for formatting user options.
pull/11833/head
Xavier Claessens 2 years ago committed by Xavier Claessens
parent 465ad6d261
commit cec3edc08a
  1. 10
      docs/markdown/snippets/string_format.md
  2. 4
      docs/yaml/elementary/str.yml
  3. 29
      mesonbuild/interpreter/interpreter.py
  4. 13
      mesonbuild/interpreter/primitives/string.py
  5. 8
      mesonbuild/interpreterbase/__init__.py
  6. 25
      mesonbuild/interpreterbase/helpers.py
  7. 13
      mesonbuild/interpreterbase/interpreterbase.py
  8. 20
      test cases/common/264 format string/meson.build
  9. 1
      test cases/common/264 format string/meson_options.txt
  10. 28
      test cases/common/264 format string/test.json

@ -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.

@ -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@'

@ -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

@ -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))

@ -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

@ -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.')

@ -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.')

@ -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

@ -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: []>"
}
]
}
Loading…
Cancel
Save