Mirrored from GitHub github.com/roostorg/osprey
0

Configure Feed

Select the types of activity you want to include in your feed.

[engine] Escape literal braces when parsing f-strings (#347)

author
hailey
committer
GitHub
date (Jun 16, 2026, 5:29 PM -0700) commit 0982c317 parent af340b5d
+23 -3
+11 -3
osprey_worker/src/osprey/engine/ast/py_ast.py
··· 289 289 def transform_JoinedStr(self, node: ast.JoinedStr) -> FormatString: 290 290 format_string_parts: ListT[str] = [] 291 291 names = [] 292 + # Tracks the unescaped length of the parts seen so far, so error carets stay positioned 293 + # the same way even though literal braces are doubled in `format_string_parts` below. 294 + unescaped_length = 0 292 295 293 296 for value in node.values: 294 297 if isinstance(value, ast.Str): 295 298 assert isinstance(value.s, str) 296 - format_string_parts.append(value.s) 299 + # Escape literal braces so the reconstructed `format_string` is a valid 300 + # `str.format()` template; interpolated names are added as `{name}` fields below. 301 + format_string_parts.append(value.s.replace('{', '{{').replace('}', '}}')) 302 + unescaped_length += len(value.s) 297 303 continue 298 304 299 305 if isinstance(value, ast.FormattedValue): 300 - offset_by = sum(len(n) for n in format_string_parts) + 3 306 + offset_by = unescaped_length + 3 301 307 self.invariant( 302 308 value.format_spec is None and value.conversion == -1, 303 309 'a format string cannot have a format spec', ··· 328 334 node=node, 329 335 ) 330 336 names.append(name) 331 - format_string_parts.append(f'{{{name.identifier}}}') 337 + placeholder = f'{{{name.identifier}}}' 338 + format_string_parts.append(placeholder) 339 + unescaped_length += len(placeholder) 332 340 333 341 format_string = ''.join(format_string_parts) 334 342 return FormatString(span=self.span_for(node), format_string=format_string, names=names)
+12
osprey_worker/src/osprey/engine/executor/tests/test_literals.py
··· 93 93 assert data == {'Bar': 'world', 'Baz': '[hello - world]', 'Foo': 'hello'} 94 94 95 95 96 + def test_fstring_with_literal_braces(execute: ExecuteFunction) -> None: 97 + # Regression: literal braces in an f-string must render as literal braces, not be 98 + # reinterpreted as str.format() substitution fields (which raised KeyError). 99 + data = execute( 100 + """ 101 + Name: ExtractLiteral[str] = "world" 102 + Greeting: ExtractLiteral[str] = f"{{hello}} {Name} {{{{}}}}" 103 + """ 104 + ) 105 + assert data == {'Name': 'world', 'Greeting': '{hello} world {{}}'} 106 + 107 + 96 108 def test_export_fails_on_non_literal(check_failure: CheckFailureFunction, execute: ExecuteFunction) -> None: 97 109 with check_failure(): 98 110 execute(