Add support for multiline f-strings

+ Extend the parser to recognize the multiline f-strings, which the
documentation already implies will work.

The syntax is like:
```
x = 'hello'
y = 'world'

msg = f'''This is a multiline string.

Sending a message: '@x@ @y@'
'''
```

which produces:
```
This is a multiline string.

Sending a message: 'hello world'

```

+ Added some f-string tests cases to "62 string arithmetic" to exercise
the new behavior.
pull/10103/head
Peter Lesslie 3 years ago committed by Eli Schwartz
parent 78a6f3bd5c
commit d771fc7d0b
  1. 6
      docs/markdown/Syntax.md
  2. 22
      docs/markdown/snippets/support-multiline-fstring.md
  3. 7
      mesonbuild/interpreterbase/interpreterbase.py
  4. 21
      mesonbuild/mparser.py
  5. 43
      test cases/common/62 string arithmetic/meson.build
  6. 7
      test cases/common/62 string arithmetic/test.json

@ -178,7 +178,7 @@ These are raw strings that do not support the escape sequences listed
above. These strings can also be combined with the string formatting above. These strings can also be combined with the string formatting
functionality via `.format()` described below. functionality via `.format()` described below.
Note that multiline f-strings are not supported. Note that multiline f-string support was added in version 0.63.
### String index ### String index
@ -211,8 +211,8 @@ As can be seen, the formatting works by replacing placeholders of type
*(Added 0.58)* *(Added 0.58)*
Format strings can be used as a non-positional alternative to the Format strings can be used as a non-positional alternative to the
string formatting functionality described above. Note that multiline f-strings string formatting functionality described above. Note that multiline f-string
are not supported. support was added in version 0.63.
```meson ```meson
n = 10 n = 10

@ -0,0 +1,22 @@
## Added support for multiline fstrings
Added support for multiline f-strings which use the same syntax as f-strings
for string substition.
```meson
x = 'hello'
y = 'world'
msg = f'''Sending a message...
"@x@ @y@"
'''
```
which produces:
```
Sending a message....
"hello world"
```

@ -217,6 +217,9 @@ class InterpreterBase:
elif isinstance(cur, mparser.TernaryNode): elif isinstance(cur, mparser.TernaryNode):
return self.evaluate_ternary(cur) return self.evaluate_ternary(cur)
elif isinstance(cur, mparser.FormatStringNode): elif isinstance(cur, mparser.FormatStringNode):
if isinstance(cur, mparser.MultilineFormatStringNode):
return self.evaluate_multiline_fstring(cur)
else:
return self.evaluate_fstring(cur) return self.evaluate_fstring(cur)
elif isinstance(cur, mparser.ContinueNode): elif isinstance(cur, mparser.ContinueNode):
raise ContinueRequest() raise ContinueRequest()
@ -367,6 +370,10 @@ class InterpreterBase:
else: else:
return self.evaluate_statement(node.falseblock) return self.evaluate_statement(node.falseblock)
@FeatureNew('multiline format strings', '0.63.0')
def evaluate_multiline_fstring(self, node: mparser.MultilineFormatStringNode) -> InterpreterObject:
return self.evaluate_fstring(node)
@FeatureNew('format strings', '0.58.0') @FeatureNew('format strings', '0.58.0')
def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject: def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject:
assert isinstance(node, mparser.FormatStringNode) assert isinstance(node, mparser.FormatStringNode)

@ -114,6 +114,7 @@ class Lexer:
self.token_specification = [ self.token_specification = [
# Need to be sorted longest to shortest. # Need to be sorted longest to shortest.
('ignore', re.compile(r'[ \t]')), ('ignore', re.compile(r'[ \t]')),
('multiline_fstring', re.compile(r"f'''(.|\n)*?'''", re.M)),
('fstring', re.compile(r"f'([^'\\]|(\\.))*'")), ('fstring', re.compile(r"f'([^'\\]|(\\.))*'")),
('id', re.compile('[_a-zA-Z][_0-9a-zA-Z]*')), ('id', re.compile('[_a-zA-Z][_0-9a-zA-Z]*')),
('number', re.compile(r'0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|0|[1-9]\d*')), ('number', re.compile(r'0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|0|[1-9]\d*')),
@ -203,9 +204,17 @@ class Lexer:
value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value) value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value)
except MesonUnicodeDecodeError as err: except MesonUnicodeDecodeError as err:
raise MesonException(f"Failed to parse escape sequence: '{err.match}' in string:\n {match_text}") raise MesonException(f"Failed to parse escape sequence: '{err.match}' in string:\n {match_text}")
elif tid == 'multiline_string': elif tid in {'multiline_string', 'multiline_fstring'}:
tid = 'string' # For multiline strings, parse out the value and pass
# through the normal string logic.
# For multiline format strings, we have to emit a
# different AST node so we can add a feature check,
# but otherwise, it follows the normal fstring logic.
if tid == 'multiline_string':
value = match_text[3:-3] value = match_text[3:-3]
tid = 'string'
else:
value = match_text[4:-3]
lines = match_text.split('\n') lines = match_text.split('\n')
if len(lines) > 1: if len(lines) > 1:
lineno += len(lines) - 1 lineno += len(lines) - 1
@ -298,7 +307,11 @@ class FormatStringNode(ElementaryNode[str]):
assert isinstance(self.value, str) assert isinstance(self.value, str)
def __str__(self) -> str: def __str__(self) -> str:
return "Format string node: '{self.value}' ({self.lineno}, {self.colno})." return f"Format string node: '{self.value}' ({self.lineno}, {self.colno})."
class MultilineFormatStringNode(FormatStringNode):
def __str__(self) -> str:
return f"Multiline Format string node: '{self.value}' ({self.lineno}, {self.colno})."
class ContinueNode(ElementaryNode): class ContinueNode(ElementaryNode):
pass pass
@ -685,6 +698,8 @@ class Parser:
return StringNode(t) return StringNode(t)
if self.accept('fstring'): if self.accept('fstring'):
return FormatStringNode(t) return FormatStringNode(t)
if self.accept('multiline_fstring'):
return MultilineFormatStringNode(t)
return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) return EmptyNode(self.current.lineno, self.current.colno, self.current.filename)
def key_values(self) -> ArgumentNode: def key_values(self) -> ArgumentNode:

@ -1,4 +1,4 @@
project('string arithmetic', 'c') project('string arithmetic', 'c', meson_version: '>=0.62.0')
assert('foo' + 'bar' == 'foobar') assert('foo' + 'bar' == 'foobar')
assert('foo' + 'bar' + 'baz' == 'foobarbaz') assert('foo' + 'bar' + 'baz' == 'foobarbaz')
@ -6,3 +6,44 @@ assert('foo' + 'bar' + 'baz' == 'foobarbaz')
a = 'a' a = 'a'
b = 'b' b = 'b'
assert(a + b + 'c' == 'abc') assert(a + b + 'c' == 'abc')
# ------------------------------------------------------------------------------
# format strings:
# ------------------------------------------------------------------------------
sub1 = 'the'
sub2 = ' quick\n'
sub3 = ' brown'
sub4 = '\nfox'
x = f'@sub1@@sub2@@sub3@@sub4@'
assert(x == sub1 + sub2 + sub3 + sub4)
assert(x == 'the quick\n brown\nfox')
# ------------------------------------------------------------------------------
# multi-line format strings
# ------------------------------------------------------------------------------
y_actual = f'''This is a multi-line comment with string substition:
"@sub1@@sub2@@sub3@@sub4@"
And I can even substitute the entry multiple times!
@sub1@
@sub2@
@sub3@
'''
y_expect = '''This is a multi-line comment with string substition:
"the quick
brown
fox"
And I can even substitute the entry multiple times!
the
quick
brown
'''
message('actual=' + y_actual)
message('expect=' + y_expect)
assert(y_actual == y_expect)

@ -0,0 +1,7 @@
{
"stdout": [
{
"line": "test cases/common/62 string arithmetic/meson.build:25: WARNING: Project targeting '>=0.62.0' but tried to use feature introduced in '0.63.0': multiline format strings."
}
]
}
Loading…
Cancel
Save