def visit_JoinedStr(self, node): self.prefix(node) content = fmt.get(node, 'content') if content is None: parts = [] for val in node.values: if isinstance(val, ast.Str): parts.append(val.s) else: parts.append(fstring_utils.placeholder(len(parts))) content = repr(''.join(parts)) values = [to_str(v) for v in fstring_utils.get_formatted_values(node)] self.code += fstring_utils.perform_replacements(content, values) self.suffix(node)
def fstr_parser(): # Reads the whole fstring as a string, then parses it char by char if self.peek_non_whitespace().type == TOKENS.STRING: # Normal fstrings are one ore more STRING tokens, maybe mixed with # spaces, e.g.: f"Hello, {name}" str_content = self.str() else: # Format specifiers in fstrings are also JoinedStr nodes, but these are # arbitrary expressions, e.g. in: f"{value:{width}.{precision}}", the # format specifier is an fstring: "{width}.{precision}" but these are # not STRING tokens. def fstr_eater(tok): if tok.type == TOKENS.OP and tok.src == '}': if fstr_eater.level <= 0: return False fstr_eater.level -= 1 if tok.type == TOKENS.OP and tok.src == '{': fstr_eater.level += 1 return True fstr_eater.level = 0 str_content = self.eat_tokens(fstr_eater) indexed_chars = enumerate(str_content) val_idx = 0 i = -1 result = '' in_fstring = False string_quote = None while i < len(str_content) - 1: i, c = next(indexed_chars) result += c # If we haven't actually parsing string content yet, check if a string # (with or without fstring prefix) has started if string_quote is None: if str_content[i:i+4] in ('f"""', "f'''"): string_quote = str_content[i+1:i+4] in_fstring = True elif str_content[i:i+3] in ('"""', "'''"): string_quote = str_content[i:i+3] in_fstring = False elif str_content[i:+2] in ('f"', "f'"): string_quote = str_content[i+1] in_fstring = True elif c in ('"', "'"): string_quote = c in_fstring = False if string_quote: # Skip uneaten quote characters for _ in range(len(string_quote) + (1 if in_fstring else 0) - 1): i, c = next(indexed_chars) result += c continue # If we are still not parsing characters in a string, no extra # processing is needed if string_quote is None: continue # If we ARE in a string, check if the next characters are the # close-quote for that string if (str_content[i:i+len(string_quote)] == string_quote and str_content[i-1] != '\\'): # Skip uneaten quote characters for _ in range(len(string_quote) - 1): i, c = next(indexed_chars) result += c string_quote = None in_fstring = False continue # If we are NOT in an fstring, skip all FormattedValue processing. if not in_fstring: continue # When an open bracket is encountered, start parsing a subexpression if c == '{': # First check if this is part of an escape sequence # (f"{{" is used to escape a bracket literal) nexti, nextc = next(indexed_chars) if nextc == '{': result += c continue indexed_chars = itertools.chain([(nexti, nextc)], indexed_chars) # Add a placeholder onto the result result += fstring_utils.placeholder(val_idx) + '}' val_idx += 1 # Yield a new token generator to parse the subexpression only tg = TokenGenerator(str_content[i+1:], ignore_error_token=True) yield (result, tg) result = '' # Skip the number of characters consumed by the subexpression for tg_i in range(tg.chars_consumed()): i, c = next(indexed_chars) # Eat up to and including the close bracket i, c = next(indexed_chars) while c != '}': i, c = next(indexed_chars) # Yield the rest of the fstring, when done yield (result, None)
def fstr_parser(): # Reads the whole fstring as a string, then parses it char by char if self.peek_non_whitespace().type == TOKENS.STRING: # Normal fstrings are one ore more STRING tokens, maybe mixed with # spaces, e.g.: f"Hello, {name}" str_content = self.str() else: # Format specifiers in fstrings are also JoinedStr nodes, but these are # arbitrary expressions, e.g. in: f"{value:{width}.{precision}}", the # format specifier is an fstring: "{width}.{precision}" but these are # not STRING tokens. def fstr_eater(tok): if tok.type == TOKENS.OP and tok.src == '}': if fstr_eater.level <= 0: return False fstr_eater.level -= 1 if tok.type == TOKENS.OP and tok.src == '{': fstr_eater.level += 1 return True fstr_eater.level = 0 str_content = self.eat_tokens(fstr_eater) indexed_chars = enumerate(str_content) val_idx = 0 i = -1 result = '' while i < len(str_content) - 1: i, c = next(indexed_chars) result += c # When an open bracket is encountered, start parsing a subexpression if c == '{': # First check if this is part of an escape sequence # (f"{{" is used to escape a bracket literal) nexti, nextc = next(indexed_chars) if nextc == '{': result += c continue indexed_chars = itertools.chain([(nexti, nextc)], indexed_chars) # Add a placeholder onto the result result += fstring_utils.placeholder(val_idx) + '}' val_idx += 1 # Yield a new token generator to parse the subexpression only tg = TokenGenerator(str_content[i + 1:], ignore_error_token=True) yield (result, tg) result = '' # Skip the number of characters consumed by the subexpression for tg_i in range(tg.chars_consumed()): i, c = next(indexed_chars) # Eat up to and including the close bracket i, c = next(indexed_chars) while c != '}': i, c = next(indexed_chars) # Yield the rest of the fstring, when done yield (result, None)