def joined_string(fmt_call: ast.Call) -> ast.JoinedStr: """ Transform a "...".format() call node into a f-string node. """ string = fmt_call.func.value.s var_map = {kw.arg: kw.value for kw in fmt_call.keywords} for i, val in enumerate(fmt_call.args): var_map[i] = val splits = deque(stdlib_parse(string)) seq_ctr = 0 new_segments = [] manual_field_ordering = False for raw, var_name, fmt_str, conversion in splits: if raw: new_segments.append(ast_string_node(raw)) if var_name is None: continue if "[" in var_name: raise FlyntException( f"Skipping f-stringify of a fmt call with indexed name {var_name}" ) suffix = "" if "." in var_name: idx = var_name.find(".") var_name, suffix = var_name[:idx], var_name[idx + 1 :] if var_name.isdigit(): manual_field_ordering = True identifier = int(var_name) elif len(var_name) == 0: assert not manual_field_ordering identifier = seq_ctr seq_ctr += 1 else: identifier = var_name try: ast_name = var_map.pop(identifier) except KeyError as e: raise ConversionRefused( f"A variable {identifier} is used multiple times - better not to replace it." ) from e if suffix: ast_name = ast.Attribute(value=ast_name, attr=suffix) new_segments.append(ast_formatted_value(ast_name, fmt_str, conversion)) if var_map: raise FlyntException( f"Some variables were never used: {var_map} - skipping conversion, it's a risk of bug." ) def is_literal_string(node): if sys.version_info < (3, 8): return isinstance(node, ast.Str) else: return (isinstance(node, ast.Constant) and isinstance(node.value, str)) def literal_string_value(node): if sys.version_info < (3, 8): return node.s else: return node.value def fix_literals(segment): if (isinstance(segment, ast.FormattedValue) and segment.format_spec is None and is_literal_string(segment.value)): return segment.value return segment new_segments = [fix_literals(e) for e in new_segments] if all(is_literal_string(segment) for segment in new_segments): return ast.Str(''.join(literal_string_value(segment) for segment in new_segments)) return ast.JoinedStr(new_segments)