# Buggy code
def _format_string(self, string, rest, allow_recursion=True):
    values = []
    ret = Result()

    while True:
        # Look for the next replacement field, and get the
        # plain text before it.
        match = re.search(r"\{\{?|\}\}?", rest)
        if match:
            literal_chars = rest[: match.start()]
            if match.group() == "}":
                raise self._syntax_error(string, "f-string: single '}' is not allowed")
            if match.group() in ("{{", "}}"):
                # Doubled braces just add a single brace to the text.
                literal_chars += match.group()[0]
            rest = rest[match.end() :]
        else:
            literal_chars = rest
            rest = ""
        if literal_chars:
            values.append(asty.Str(string, s=literal_chars))
        if not rest:
            break
        if match.group() != "{":
            continue

        # Look for the end of the replacement field, allowing
        # one more level of matched braces, but no deeper, and only
        # if we can recurse.
        match = re.match(
            r"(?: \{ [^{}]* \} | [^{}]+ )* \}" if allow_recursion else r"[^{}]* \}",
            rest,
            re.VERBOSE,
        )
        if not match:
            raise self._syntax_error(string, "f-string: mismatched braces")
        item = rest[: match.end() - 1]
        rest = rest[match.end() :]

        # Parse the first form.
        try:
            model, item = parse_one_thing(item)
        except (ValueError, LexException) as e:
            raise self._syntax_error(string, "f-string: " + str(e))

        # Look for a conversion character.
        item = item.lstrip()
        conversion = None
        if item.startswith("!"):
            conversion = item[1]
            item = item[2:].lstrip()

        # Look for a format specifier.
        format_spec = asty.Str(string, s="")
        if item.startswith(":"):
            if allow_recursion:
                ret += self._format_string(string, item[1:], allow_recursion=False)
                format_spec = ret.force_expr
            else:
                format_spec = asty.Str(string, s=item[1:])
        elif item:
            raise self._syntax_error(string, "f-string: trailing junk in field")

        # Now, having finished compiling any recursively included
        # forms, we can compile the first form that we parsed.
        ret += self.compile(model)

        if PY36:
            values.append(
                asty.FormattedValue(
                    string,
                    conversion=-1 if conversion is None else ord(conversion),
                    format_spec=format_spec,
                    value=ret.force_expr,
                )
            )
        else:
            # Make an expression like:
            #    "{!r:{}}".format(value, format_spec)
            values.append(
                asty.Call(
                    string,
                    func=asty.Attribute(
                        string,
                        value=asty.Str(
                            string,
                            s="{" + ("!" + conversion if conversion else "") + ":{}}",
                        ),
                        attr="format",
                        ctx=ast.Load(),
                    ),
                    args=[ret.force_expr, format_spec],
                    keywords=[],
                    starargs=None,
                    kwargs=None,
                )
            )

    return ret + (
        asty.JoinedStr(string, values=values)
        if PY36
        else reduce(
            lambda x, y: asty.BinOp(string, left=x, op=ast.Add(), right=y), values
        )
    )


# Traceback (AttributeError)
(hylearn) # hy2py minor.hy
Traceback (most recent call last):
File "/home/diego/hylearn/bin/hy2py", line 12, in <module>
sys.exit(hy2py_main())
File "/home/diego/hylearn/lib/python3.7/site-packages/hy/cmdline.py", line 690, in hy2py_main
print(astor.code_gen.to_source(_ast))
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 62, in to_source
generator.visit(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 876, in visit_Module
self.write(*node.body)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 177, in write
visit(item)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 370, in visit_If
self.body(node.body)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 225, in body
self.write(*statements)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 177, in write
visit(item)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 334, in visit_FunctionDef
self.body(node.body)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 225, in body
self.write(*statements)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 177, in write
visit(item)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 487, in visit_Return
self.conditional_write(' ', node.value)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 213, in conditional_write
self.write(*stuff)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 177, in write
visit(item)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 532, in visit_Call
write(write_comma, arg)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 177, in write
visit(item)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/node_util.py", line 143, in visit
return visitor(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 564, in visit_JoinedStr
self._handle_string_constant(node, None, is_joined=True)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 620, in _handle_string_constant
recurse(node)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 612, in recurse
recurse(value.format_spec)
File "/home/diego/hylearn/lib/python3.7/site-packages/astor/code_gen.py", line 597, in recurse
for value in node.values:
AttributeError: 'Str' object has no attribute 'values'

# Fixed code
def _format_string(self, string, rest, allow_recursion=True):
    values = []
    ret = Result()

    while True:
        # Look for the next replacement field, and get the
        # plain text before it.
        match = re.search(r"\{\{?|\}\}?", rest)
        if match:
            literal_chars = rest[: match.start()]
            if match.group() == "}":
                raise self._syntax_error(string, "f-string: single '}' is not allowed")
            if match.group() in ("{{", "}}"):
                # Doubled braces just add a single brace to the text.
                literal_chars += match.group()[0]
            rest = rest[match.end() :]
        else:
            literal_chars = rest
            rest = ""
        if literal_chars:
            values.append(asty.Str(string, s=literal_chars))
        if not rest:
            break
        if match.group() != "{":
            continue

        # Look for the end of the replacement field, allowing
        # one more level of matched braces, but no deeper, and only
        # if we can recurse.
        match = re.match(
            r"(?: \{ [^{}]* \} | [^{}]+ )* \}" if allow_recursion else r"[^{}]* \}",
            rest,
            re.VERBOSE,
        )
        if not match:
            raise self._syntax_error(string, "f-string: mismatched braces")
        item = rest[: match.end() - 1]
        rest = rest[match.end() :]

        # Parse the first form.
        try:
            model, item = parse_one_thing(item)
        except (ValueError, LexException) as e:
            raise self._syntax_error(string, "f-string: " + str(e))

        # Look for a conversion character.
        item = item.lstrip()
        conversion = None
        if item.startswith("!"):
            conversion = item[1]
            item = item[2:].lstrip()

        # Look for a format specifier.
        format_spec = None
        if item.startswith(":"):
            if allow_recursion:
                ret += self._format_string(string, item[1:], allow_recursion=False)
                format_spec = ret.force_expr
            else:
                format_spec = asty.Str(string, s=item[1:])
                if PY36:
                    format_spec = asty.JoinedStr(string, values=[format_spec])
        elif item:
            raise self._syntax_error(string, "f-string: trailing junk in field")

        # Now, having finished compiling any recursively included
        # forms, we can compile the first form that we parsed.
        ret += self.compile(model)

        if PY36:
            values.append(
                asty.FormattedValue(
                    string,
                    conversion=-1 if conversion is None else ord(conversion),
                    format_spec=format_spec,
                    value=ret.force_expr,
                )
            )
        else:
            # Make an expression like:
            #    "{!r:{}}".format(value, format_spec)
            values.append(
                asty.Call(
                    string,
                    func=asty.Attribute(
                        string,
                        value=asty.Str(
                            string,
                            s="{" + ("!" + conversion if conversion else "") + ":{}}",
                        ),
                        attr="format",
                        ctx=ast.Load(),
                    ),
                    args=[ret.force_expr, format_spec or asty.Str(string, s="")],
                    keywords=[],
                    starargs=None,
                    kwargs=None,
                )
            )

    return ret + (
        asty.JoinedStr(string, values=values)
        if PY36
        else reduce(
            lambda x, y: asty.BinOp(string, left=x, op=ast.Add(), right=y), values
        )
    )