prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  decide on `${ var }` vs `${{ var }}`
  We have to decide on single braces vs double braces syntax. Both of them work right now, but the former is most tested internally.
  
  1. Single braces
  - For: Easier on eyes
  - Against: Might affect existing users using environment variables
  
  2. Double braces
  - For: compatible with 1.0 users that were using environment variables
  - Against: looks ugly
  
  cc @dmpetrov @shcheklein 
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `dvc/parsing/interpolate.py`
  Content:
  ```
  1 import re
  2 import typing
  3 from collections.abc import Mapping
  4 from functools import singledispatch
  5 
  6 from funcy import memoize, rpartial
  7 
  8 from dvc.exceptions import DvcException
  9 
  10 if typing.TYPE_CHECKING:
  11     from typing import List, Match
  12 
  13     from pyparsing import ParseException
  14     from typing_extensions import NoReturn
  15 
  16     from .context import Context
  17 
  18 BRACE_OPEN = "${"
  19 BRACE_CLOSE = "}"
  20 LBRACK = "["
  21 RBRACK = "]"
  22 PERIOD = "."
  23 KEYCRE = re.compile(
  24     r"""
  25     (?<!\\)                   # escape \${} or ${{}}
  26     \$                        # starts with $
  27     (?:({{)|({))              # either starts with double braces or single
  28     (.*?)                     # match every char inside
  29     (?(1)}})(?(2)})           # end with same kinds of braces it opened with
  30 """,
  31     re.VERBOSE,
  32 )
  33 
  34 
  35 @memoize
  36 def get_parser():
  37     from pyparsing import CharsNotIn, ParserElement, Suppress, ZeroOrMore
  38 
  39     ParserElement.enablePackrat()
  40 
  41     word = CharsNotIn(f"{PERIOD}{LBRACK}{RBRACK}")
  42     idx = Suppress(LBRACK) + word + Suppress(RBRACK)
  43     attr = Suppress(PERIOD) + word
  44     parser = word + ZeroOrMore(attr ^ idx)
  45     parser.setParseAction(PERIOD.join)
  46 
  47     return parser
  48 
  49 
  50 class ParseError(DvcException):
  51     pass
  52 
  53 
  54 def get_matches(template: str):
  55     return list(KEYCRE.finditer(template))
  56 
  57 
  58 def is_interpolated_string(val):
  59     return isinstance(val, str) and bool(get_matches(val))
  60 
  61 
  62 def normalize_key(key: str):
  63     return key.replace(LBRACK, PERIOD).replace(RBRACK, "")
  64 
  65 
  66 def format_and_raise_parse_error(exc) -> "NoReturn":
  67     raise ParseError(_format_exc_msg(exc))
  68 
  69 
  70 def embrace(s: str):
  71     return BRACE_OPEN + s + BRACE_CLOSE
  72 
  73 
  74 @singledispatch
  75 def to_str(obj) -> str:
  76     return str(obj)
  77 
  78 
  79 @to_str.register(bool)
  80 def _(obj: bool):
  81     return "true" if obj else "false"
  82 
  83 
  84 def _format_exc_msg(exc: "ParseException"):
  85     from pyparsing import ParseException
  86 
  87     from dvc.utils import colorize
  88 
  89     exc.loc += 2  # 2 because we append `${` at the start of expr below
  90 
  91     expr = exc.pstr
  92     exc.pstr = embrace(exc.pstr)
  93     error = ParseException.explain(exc, depth=0)
  94 
  95     _, pointer, *explains = error.splitlines()
  96     pstr = "{brace_open}{expr}{brace_close}".format(
  97         brace_open=colorize(BRACE_OPEN, color="blue"),
  98         expr=colorize(expr, color="magenta"),
  99         brace_close=colorize(BRACE_CLOSE, color="blue"),
  100     )
  101     msg = "\n".join(explains)
  102     pointer = colorize(pointer, color="red")
  103     return "\n".join([pstr, pointer, colorize(msg, color="red", style="bold")])
  104 
  105 
  106 def recurse(f):
  107     seq = (list, tuple, set)
  108 
  109     def wrapper(data, *args):
  110         g = rpartial(wrapper, *args)
  111         if isinstance(data, Mapping):
  112             return {g(k): g(v) for k, v in data.items()}
  113         if isinstance(data, seq):
  114             return type(data)(map(g, data))
  115         if isinstance(data, str):
  116             return f(data, *args)
  117         return data
  118 
  119     return wrapper
  120 
  121 
  122 def check_recursive_parse_errors(data):
  123     func = recurse(check_expression)
  124     return func(data)
  125 
  126 
  127 def check_expression(s: str):
  128     matches = get_matches(s)
  129     for match in matches:
  130         get_expression(match)
  131 
  132 
  133 def parse_expr(s: str):
  134     from pyparsing import ParseException
  135 
  136     try:
  137         result = get_parser().parseString(s, parseAll=True)
  138     except ParseException as exc:
  139         format_and_raise_parse_error(exc)
  140 
  141     joined = result.asList()
  142     assert len(joined) == 1
  143     return joined[0]
  144 
  145 
  146 def get_expression(match: "Match", skip_checks: bool = False):
  147     _, _, inner = match.groups()
  148     return inner if skip_checks else parse_expr(inner)
  149 
  150 
  151 def str_interpolate(
  152     template: str,
  153     matches: "List[Match]",
  154     context: "Context",
  155     skip_checks: bool = False,
  156 ):
  157     from .context import PRIMITIVES
  158 
  159     index, buf = 0, ""
  160     for match in matches:
  161         start, end = match.span(0)
  162         expr = get_expression(match, skip_checks=skip_checks)
  163         value = context.select(expr, unwrap=True)
  164         if value is not None and not isinstance(value, PRIMITIVES):
  165             raise ParseError(
  166                 f"Cannot interpolate data of type '{type(value).__name__}'"
  167             )
  168         buf += template[index:start] + to_str(value)
  169         index = end
  170     buf += template[index:]
  171     # regex already backtracks and avoids any `${` starting with
  172     # backslashes(`\`). We just need to replace those by `${`.
  173     return buf.replace(r"\${", BRACE_OPEN)
  174 
  175 
  176 def is_exact_string(src: str, matches: "List[Match]"):
  177     return len(matches) == 1 and src == matches[0].group(0)
  178
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/dvc/parsing/interpolate.py b/dvc/parsing/interpolate.py
  --- a/dvc/parsing/interpolate.py
  +++ b/dvc/parsing/interpolate.py
  @@ -22,11 +22,10 @@
   PERIOD = "."
   KEYCRE = re.compile(
       r"""
  -    (?<!\\)                   # escape \${} or ${{}}
  -    \$                        # starts with $
  -    (?:({{)|({))              # either starts with double braces or single
  -    (.*?)                     # match every char inside
  -    (?(1)}})(?(2)})           # end with same kinds of braces it opened with
  +    (?<!\\)                            # escape \${}
  +    \${                                # starts with ${
  +    (?P<inner>.*?)                     # match every char inside
  +    }                                  # end with {
   """,
       re.VERBOSE,
   )
  @@ -137,6 +136,7 @@
           result = get_parser().parseString(s, parseAll=True)
       except ParseException as exc:
           format_and_raise_parse_error(exc)
  +        raise AssertionError("unreachable")
   
       joined = result.asList()
       assert len(joined) == 1
  @@ -144,7 +144,7 @@
   
   
   def get_expression(match: "Match", skip_checks: bool = False):
  -    _, _, inner = match.groups()
  +    inner = match["inner"]
       return inner if skip_checks else parse_expr(inner)
  
verification_info: {"golden_diff": "diff --git a/dvc/parsing/interpolate.py b/dvc/parsing/interpolate.py\n--- a/dvc/parsing/interpolate.py\n+++ b/dvc/parsing/interpolate.py\n@@ -22,11 +22,10 @@\n PERIOD = \".\"\n KEYCRE = re.compile(\n     r\"\"\"\n-    (?<!\\\\)                   # escape \\${} or ${{}}\n-    \\$                        # starts with $\n-    (?:({{)|({))              # either starts with double braces or single\n-    (.*?)                     # match every char inside\n-    (?(1)}})(?(2)})           # end with same kinds of braces it opened with\n+    (?<!\\\\)                            # escape \\${}\n+    \\${                                # starts with ${\n+    (?P<inner>.*?)                     # match every char inside\n+    }                                  # end with {\n \"\"\",\n     re.VERBOSE,\n )\n@@ -137,6 +136,7 @@\n         result = get_parser().parseString(s, parseAll=True)\n     except ParseException as exc:\n         format_and_raise_parse_error(exc)\n+        raise AssertionError(\"unreachable\")\n \n     joined = result.asList()\n     assert len(joined) == 1\n@@ -144,7 +144,7 @@\n \n \n def get_expression(match: \"Match\", skip_checks: bool = False):\n-    _, _, inner = match.groups()\n+    inner = match[\"inner\"]\n     return inner if skip_checks else parse_expr(inner)\n", "issue": "decide on `${ var }` vs `${{ var }}`\nWe have to decide on single braces vs double braces syntax. Both of them work right now, but the former is most tested internally.\r\n\r\n1. Single braces\r\n- For: Easier on eyes\r\n- Against: Might affect existing users using environment variables\r\n\r\n2. Double braces\r\n- For: compatible with 1.0 users that were using environment variables\r\n- Against: looks ugly\r\n\r\ncc @dmpetrov @shcheklein \n", "before_files": [{"content": "import re\nimport typing\nfrom collections.abc import Mapping\nfrom functools import singledispatch\n\nfrom funcy import memoize, rpartial\n\nfrom dvc.exceptions import DvcException\n\nif typing.TYPE_CHECKING:\n    from typing import List, Match\n\n    from pyparsing import ParseException\n    from typing_extensions import NoReturn\n\n    from .context import Context\n\nBRACE_OPEN = \"${\"\nBRACE_CLOSE = \"}\"\nLBRACK = \"[\"\nRBRACK = \"]\"\nPERIOD = \".\"\nKEYCRE = re.compile(\n    r\"\"\"\n    (?<!\\\\)                   # escape \\${} or ${{}}\n    \\$                        # starts with $\n    (?:({{)|({))              # either starts with double braces or single\n    (.*?)                     # match every char inside\n    (?(1)}})(?(2)})           # end with same kinds of braces it opened with\n\"\"\",\n    re.VERBOSE,\n)\n\n\n@memoize\ndef get_parser():\n    from pyparsing import CharsNotIn, ParserElement, Suppress, ZeroOrMore\n\n    ParserElement.enablePackrat()\n\n    word = CharsNotIn(f\"{PERIOD}{LBRACK}{RBRACK}\")\n    idx = Suppress(LBRACK) + word + Suppress(RBRACK)\n    attr = Suppress(PERIOD) + word\n    parser = word + ZeroOrMore(attr ^ idx)\n    parser.setParseAction(PERIOD.join)\n\n    return parser\n\n\nclass ParseError(DvcException):\n    pass\n\n\ndef get_matches(template: str):\n    return list(KEYCRE.finditer(template))\n\n\ndef is_interpolated_string(val):\n    return isinstance(val, str) and bool(get_matches(val))\n\n\ndef normalize_key(key: str):\n    return key.replace(LBRACK, PERIOD).replace(RBRACK, \"\")\n\n\ndef format_and_raise_parse_error(exc) -> \"NoReturn\":\n    raise ParseError(_format_exc_msg(exc))\n\n\ndef embrace(s: str):\n    return BRACE_OPEN + s + BRACE_CLOSE\n\n\n@singledispatch\ndef to_str(obj) -> str:\n    return str(obj)\n\n\n@to_str.register(bool)\ndef _(obj: bool):\n    return \"true\" if obj else \"false\"\n\n\ndef _format_exc_msg(exc: \"ParseException\"):\n    from pyparsing import ParseException\n\n    from dvc.utils import colorize\n\n    exc.loc += 2  # 2 because we append `${` at the start of expr below\n\n    expr = exc.pstr\n    exc.pstr = embrace(exc.pstr)\n    error = ParseException.explain(exc, depth=0)\n\n    _, pointer, *explains = error.splitlines()\n    pstr = \"{brace_open}{expr}{brace_close}\".format(\n        brace_open=colorize(BRACE_OPEN, color=\"blue\"),\n        expr=colorize(expr, color=\"magenta\"),\n        brace_close=colorize(BRACE_CLOSE, color=\"blue\"),\n    )\n    msg = \"\\n\".join(explains)\n    pointer = colorize(pointer, color=\"red\")\n    return \"\\n\".join([pstr, pointer, colorize(msg, color=\"red\", style=\"bold\")])\n\n\ndef recurse(f):\n    seq = (list, tuple, set)\n\n    def wrapper(data, *args):\n        g = rpartial(wrapper, *args)\n        if isinstance(data, Mapping):\n            return {g(k): g(v) for k, v in data.items()}\n        if isinstance(data, seq):\n            return type(data)(map(g, data))\n        if isinstance(data, str):\n            return f(data, *args)\n        return data\n\n    return wrapper\n\n\ndef check_recursive_parse_errors(data):\n    func = recurse(check_expression)\n    return func(data)\n\n\ndef check_expression(s: str):\n    matches = get_matches(s)\n    for match in matches:\n        get_expression(match)\n\n\ndef parse_expr(s: str):\n    from pyparsing import ParseException\n\n    try:\n        result = get_parser().parseString(s, parseAll=True)\n    except ParseException as exc:\n        format_and_raise_parse_error(exc)\n\n    joined = result.asList()\n    assert len(joined) == 1\n    return joined[0]\n\n\ndef get_expression(match: \"Match\", skip_checks: bool = False):\n    _, _, inner = match.groups()\n    return inner if skip_checks else parse_expr(inner)\n\n\ndef str_interpolate(\n    template: str,\n    matches: \"List[Match]\",\n    context: \"Context\",\n    skip_checks: bool = False,\n):\n    from .context import PRIMITIVES\n\n    index, buf = 0, \"\"\n    for match in matches:\n        start, end = match.span(0)\n        expr = get_expression(match, skip_checks=skip_checks)\n        value = context.select(expr, unwrap=True)\n        if value is not None and not isinstance(value, PRIMITIVES):\n            raise ParseError(\n                f\"Cannot interpolate data of type '{type(value).__name__}'\"\n            )\n        buf += template[index:start] + to_str(value)\n        index = end\n    buf += template[index:]\n    # regex already backtracks and avoids any `${` starting with\n    # backslashes(`\\`). We just need to replace those by `${`.\n    return buf.replace(r\"\\${\", BRACE_OPEN)\n\n\ndef is_exact_string(src: str, matches: \"List[Match]\"):\n    return len(matches) == 1 and src == matches[0].group(0)\n", "path": "dvc/parsing/interpolate.py"}], "after_files": [{"content": "import re\nimport typing\nfrom collections.abc import Mapping\nfrom functools import singledispatch\n\nfrom funcy import memoize, rpartial\n\nfrom dvc.exceptions import DvcException\n\nif typing.TYPE_CHECKING:\n    from typing import List, Match\n\n    from pyparsing import ParseException\n    from typing_extensions import NoReturn\n\n    from .context import Context\n\nBRACE_OPEN = \"${\"\nBRACE_CLOSE = \"}\"\nLBRACK = \"[\"\nRBRACK = \"]\"\nPERIOD = \".\"\nKEYCRE = re.compile(\n    r\"\"\"\n    (?<!\\\\)                            # escape \\${}\n    \\${                                # starts with ${\n    (?P<inner>.*?)                     # match every char inside\n    }                                  # end with {\n\"\"\",\n    re.VERBOSE,\n)\n\n\n@memoize\ndef get_parser():\n    from pyparsing import CharsNotIn, ParserElement, Suppress, ZeroOrMore\n\n    ParserElement.enablePackrat()\n\n    word = CharsNotIn(f\"{PERIOD}{LBRACK}{RBRACK}\")\n    idx = Suppress(LBRACK) + word + Suppress(RBRACK)\n    attr = Suppress(PERIOD) + word\n    parser = word + ZeroOrMore(attr ^ idx)\n    parser.setParseAction(PERIOD.join)\n\n    return parser\n\n\nclass ParseError(DvcException):\n    pass\n\n\ndef get_matches(template: str):\n    return list(KEYCRE.finditer(template))\n\n\ndef is_interpolated_string(val):\n    return isinstance(val, str) and bool(get_matches(val))\n\n\ndef normalize_key(key: str):\n    return key.replace(LBRACK, PERIOD).replace(RBRACK, \"\")\n\n\ndef format_and_raise_parse_error(exc) -> \"NoReturn\":\n    raise ParseError(_format_exc_msg(exc))\n\n\ndef embrace(s: str):\n    return BRACE_OPEN + s + BRACE_CLOSE\n\n\n@singledispatch\ndef to_str(obj) -> str:\n    return str(obj)\n\n\n@to_str.register(bool)\ndef _(obj: bool):\n    return \"true\" if obj else \"false\"\n\n\ndef _format_exc_msg(exc: \"ParseException\"):\n    from pyparsing import ParseException\n\n    from dvc.utils import colorize\n\n    exc.loc += 2  # 2 because we append `${` at the start of expr below\n\n    expr = exc.pstr\n    exc.pstr = embrace(exc.pstr)\n    error = ParseException.explain(exc, depth=0)\n\n    _, pointer, *explains = error.splitlines()\n    pstr = \"{brace_open}{expr}{brace_close}\".format(\n        brace_open=colorize(BRACE_OPEN, color=\"blue\"),\n        expr=colorize(expr, color=\"magenta\"),\n        brace_close=colorize(BRACE_CLOSE, color=\"blue\"),\n    )\n    msg = \"\\n\".join(explains)\n    pointer = colorize(pointer, color=\"red\")\n    return \"\\n\".join([pstr, pointer, colorize(msg, color=\"red\", style=\"bold\")])\n\n\ndef recurse(f):\n    seq = (list, tuple, set)\n\n    def wrapper(data, *args):\n        g = rpartial(wrapper, *args)\n        if isinstance(data, Mapping):\n            return {g(k): g(v) for k, v in data.items()}\n        if isinstance(data, seq):\n            return type(data)(map(g, data))\n        if isinstance(data, str):\n            return f(data, *args)\n        return data\n\n    return wrapper\n\n\ndef check_recursive_parse_errors(data):\n    func = recurse(check_expression)\n    return func(data)\n\n\ndef check_expression(s: str):\n    matches = get_matches(s)\n    for match in matches:\n        get_expression(match)\n\n\ndef parse_expr(s: str):\n    from pyparsing import ParseException\n\n    try:\n        result = get_parser().parseString(s, parseAll=True)\n    except ParseException as exc:\n        format_and_raise_parse_error(exc)\n        raise AssertionError(\"unreachable\")\n\n    joined = result.asList()\n    assert len(joined) == 1\n    return joined[0]\n\n\ndef get_expression(match: \"Match\", skip_checks: bool = False):\n    inner = match[\"inner\"]\n    return inner if skip_checks else parse_expr(inner)\n\n\ndef str_interpolate(\n    template: str,\n    matches: \"List[Match]\",\n    context: \"Context\",\n    skip_checks: bool = False,\n):\n    from .context import PRIMITIVES\n\n    index, buf = 0, \"\"\n    for match in matches:\n        start, end = match.span(0)\n        expr = get_expression(match, skip_checks=skip_checks)\n        value = context.select(expr, unwrap=True)\n        if value is not None and not isinstance(value, PRIMITIVES):\n            raise ParseError(\n                f\"Cannot interpolate data of type '{type(value).__name__}'\"\n            )\n        buf += template[index:start] + to_str(value)\n        index = end\n    buf += template[index:]\n    # regex already backtracks and avoids any `${` starting with\n    # backslashes(`\\`). We just need to replace those by `${`.\n    return buf.replace(r\"\\${\", BRACE_OPEN)\n\n\ndef is_exact_string(src: str, matches: \"List[Match]\"):\n    return len(matches) == 1 and src == matches[0].group(0)\n", "path": "dvc/parsing/interpolate.py"}]}