prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  BUG: Graphviz repr should escape HTML
  The current notebook graphviz repr breaks when there are unintentional HTML characters in column names or types. An example of this is array types, which includes angle brackets, so a type like `array<string>` fails to render because it produces invalid HTML.
  
  The fix is fairly straightforward: names and columns should be escaped. I should be able to submit a PR.
  
  </issue>
  <code>
  [start of ibis/expr/visualize.py]
  1 import tempfile
  2 
  3 import graphviz as gv
  4 
  5 import ibis
  6 import ibis.common.exceptions as com
  7 import ibis.expr.operations as ops
  8 import ibis.expr.types as ir
  9 
  10 
  11 def get_type(expr):
  12     try:
  13         return str(expr.type())
  14     except (AttributeError, NotImplementedError):
  15         pass
  16 
  17     try:
  18         schema = expr.schema()
  19     except (AttributeError, NotImplementedError):
  20         try:
  21             # As a last resort try get the name of the output_type class
  22             return expr.op().output_type().__name__
  23         except (AttributeError, NotImplementedError):
  24             return '\u2205'  # empty set character
  25     except com.IbisError:
  26         op = expr.op()
  27         assert isinstance(op, ops.Join)
  28         left_table_name = getattr(op.left.op(), 'name', None) or ops.genname()
  29         left_schema = op.left.schema()
  30         right_table_name = (
  31             getattr(op.right.op(), 'name', None) or ops.genname()
  32         )
  33         right_schema = op.right.schema()
  34         pairs = [
  35             ('{}.{}'.format(left_table_name, left_column), type)
  36             for left_column, type in left_schema.items()
  37         ] + [
  38             ('{}.{}'.format(right_table_name, right_column), type)
  39             for right_column, type in right_schema.items()
  40         ]
  41         schema = ibis.schema(pairs)
  42 
  43     return (
  44         ''.join(
  45             '<BR ALIGN="LEFT" />  <I>{}</I>: {}'.format(name, type)
  46             for name, type in zip(schema.names, schema.types)
  47         )
  48         + '<BR ALIGN="LEFT" />'
  49     )
  50 
  51 
  52 def get_label(expr, argname=None):
  53     import ibis.expr.operations as ops
  54 
  55     node = expr.op()
  56     typename = get_type(expr)
  57     name = type(node).__name__
  58     nodename = getattr(node, 'name', argname)
  59     if nodename is not None:
  60         if isinstance(node, ops.TableNode):
  61             label_fmt = '<<I>{}</I>: <B>{}</B>{}>'
  62         else:
  63             label_fmt = '<<I>{}</I>: <B>{}</B> \u27f6 {}>'
  64         label = label_fmt.format(nodename, name, typename)
  65     else:
  66         if isinstance(node, ops.TableNode):
  67             label_fmt = '<<B>{}</B>{}>'
  68         else:
  69             label_fmt = '<<B>{}</B> \u27f6 {}>'
  70         label = label_fmt.format(name, typename)
  71     return label
  72 
  73 
  74 DEFAULT_NODE_ATTRS = {'shape': 'box', 'fontname': 'Deja Vu Sans Mono'}
  75 
  76 
  77 def to_graph(expr, node_attr=None, edge_attr=None):
  78     stack = [(expr, expr._safe_name)]
  79     seen = set()
  80     g = gv.Digraph(
  81         node_attr=node_attr or DEFAULT_NODE_ATTRS, edge_attr=edge_attr or {}
  82     )
  83 
  84     g.attr(rankdir='BT')
  85 
  86     while stack:
  87         e, ename = stack.pop()
  88         vkey = e._key, ename
  89 
  90         if vkey not in seen:
  91             seen.add(vkey)
  92 
  93             vlabel = get_label(e, argname=ename)
  94             vhash = str(hash(vkey))
  95             g.node(vhash, label=vlabel)
  96 
  97             node = e.op()
  98             args = node.args
  99             for arg, name in zip(args, node.signature.names()):
  100                 if isinstance(arg, ir.Expr):
  101                     u = arg, name
  102                     ukey = arg._key, name
  103                     uhash = str(hash(ukey))
  104                     ulabel = get_label(arg, argname=name)
  105                     g.node(uhash, label=ulabel)
  106                     g.edge(uhash, vhash)
  107                     stack.append(u)
  108     return g
  109 
  110 
  111 def draw(graph, path=None, format='png'):
  112     piped_source = graph.pipe(format=format)
  113 
  114     if path is None:
  115         with tempfile.NamedTemporaryFile(
  116             delete=False, suffix='.{}'.format(format), mode='wb'
  117         ) as f:
  118             f.write(piped_source)
  119         return f.name
  120     else:
  121         with open(path, mode='wb') as f:
  122             f.write(piped_source)
  123         return path
  124 
  125 
  126 if __name__ == '__main__':
  127     t = ibis.table(
  128         [('a', 'int64'), ('b', 'double'), ('c', 'string')], name='t'
  129     )
  130     left = ibis.table([('a', 'int64'), ('b', 'string')])
  131     right = ibis.table([('b', 'string'), ('c', 'int64'), ('d', 'string')])
  132     joined = left.inner_join(right, left.b == right.b)
  133     df = joined[left.a, right.c.name('b'), right.d.name('c')]
  134     a = df.a
  135     b = df.b
  136     filt = df[(a + b * 2 * b / b ** 3 > 4) & (b > 5)]
  137     expr = filt.groupby(filt.c).aggregate(
  138         amean=filt.a.mean(), bsum=filt.b.sum()
  139     )
  140     expr.visualize()
  141
  [end of ibis/expr/visualize.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/ibis/expr/visualize.py b/ibis/expr/visualize.py
  --- a/ibis/expr/visualize.py
  +++ b/ibis/expr/visualize.py
  @@ -1,4 +1,5 @@
   import tempfile
  +from html import escape
   
   import graphviz as gv
   
  @@ -42,7 +43,9 @@
   
       return (
           ''.join(
  -            '<BR ALIGN="LEFT" />  <I>{}</I>: {}'.format(name, type)
  +            '<BR ALIGN="LEFT" />  <I>{}</I>: {}'.format(
  +                escape(name), escape(str(type))
  +            )
               for name, type in zip(schema.names, schema.types)
           )
           + '<BR ALIGN="LEFT" />'
  @@ -53,7 +56,7 @@
       import ibis.expr.operations as ops
   
       node = expr.op()
  -    typename = get_type(expr)
  +    typename = get_type(expr)  # Already an escaped string
       name = type(node).__name__
       nodename = getattr(node, 'name', argname)
       if nodename is not None:
  @@ -61,13 +64,13 @@
               label_fmt = '<<I>{}</I>: <B>{}</B>{}>'
           else:
               label_fmt = '<<I>{}</I>: <B>{}</B> \u27f6 {}>'
  -        label = label_fmt.format(nodename, name, typename)
  +        label = label_fmt.format(escape(nodename), escape(name), typename)
       else:
           if isinstance(node, ops.TableNode):
               label_fmt = '<<B>{}</B>{}>'
           else:
               label_fmt = '<<B>{}</B> \u27f6 {}>'
  -        label = label_fmt.format(name, typename)
  +        label = label_fmt.format(escape(name), typename)
       return label
  
verification_info: {"golden_diff": "diff --git a/ibis/expr/visualize.py b/ibis/expr/visualize.py\n--- a/ibis/expr/visualize.py\n+++ b/ibis/expr/visualize.py\n@@ -1,4 +1,5 @@\n import tempfile\n+from html import escape\n \n import graphviz as gv\n \n@@ -42,7 +43,9 @@\n \n     return (\n         ''.join(\n-            '<BR ALIGN=\"LEFT\" />  <I>{}</I>: {}'.format(name, type)\n+            '<BR ALIGN=\"LEFT\" />  <I>{}</I>: {}'.format(\n+                escape(name), escape(str(type))\n+            )\n             for name, type in zip(schema.names, schema.types)\n         )\n         + '<BR ALIGN=\"LEFT\" />'\n@@ -53,7 +56,7 @@\n     import ibis.expr.operations as ops\n \n     node = expr.op()\n-    typename = get_type(expr)\n+    typename = get_type(expr)  # Already an escaped string\n     name = type(node).__name__\n     nodename = getattr(node, 'name', argname)\n     if nodename is not None:\n@@ -61,13 +64,13 @@\n             label_fmt = '<<I>{}</I>: <B>{}</B>{}>'\n         else:\n             label_fmt = '<<I>{}</I>: <B>{}</B> \\u27f6 {}>'\n-        label = label_fmt.format(nodename, name, typename)\n+        label = label_fmt.format(escape(nodename), escape(name), typename)\n     else:\n         if isinstance(node, ops.TableNode):\n             label_fmt = '<<B>{}</B>{}>'\n         else:\n             label_fmt = '<<B>{}</B> \\u27f6 {}>'\n-        label = label_fmt.format(name, typename)\n+        label = label_fmt.format(escape(name), typename)\n     return label\n", "issue": "BUG: Graphviz repr should escape HTML\nThe current notebook graphviz repr breaks when there are unintentional HTML characters in column names or types. An example of this is array types, which includes angle brackets, so a type like `array<string>` fails to render because it produces invalid HTML.\r\n\r\nThe fix is fairly straightforward: names and columns should be escaped. I should be able to submit a PR.\n", "before_files": [{"content": "import tempfile\n\nimport graphviz as gv\n\nimport ibis\nimport ibis.common.exceptions as com\nimport ibis.expr.operations as ops\nimport ibis.expr.types as ir\n\n\ndef get_type(expr):\n    try:\n        return str(expr.type())\n    except (AttributeError, NotImplementedError):\n        pass\n\n    try:\n        schema = expr.schema()\n    except (AttributeError, NotImplementedError):\n        try:\n            # As a last resort try get the name of the output_type class\n            return expr.op().output_type().__name__\n        except (AttributeError, NotImplementedError):\n            return '\\u2205'  # empty set character\n    except com.IbisError:\n        op = expr.op()\n        assert isinstance(op, ops.Join)\n        left_table_name = getattr(op.left.op(), 'name', None) or ops.genname()\n        left_schema = op.left.schema()\n        right_table_name = (\n            getattr(op.right.op(), 'name', None) or ops.genname()\n        )\n        right_schema = op.right.schema()\n        pairs = [\n            ('{}.{}'.format(left_table_name, left_column), type)\n            for left_column, type in left_schema.items()\n        ] + [\n            ('{}.{}'.format(right_table_name, right_column), type)\n            for right_column, type in right_schema.items()\n        ]\n        schema = ibis.schema(pairs)\n\n    return (\n        ''.join(\n            '<BR ALIGN=\"LEFT\" />  <I>{}</I>: {}'.format(name, type)\n            for name, type in zip(schema.names, schema.types)\n        )\n        + '<BR ALIGN=\"LEFT\" />'\n    )\n\n\ndef get_label(expr, argname=None):\n    import ibis.expr.operations as ops\n\n    node = expr.op()\n    typename = get_type(expr)\n    name = type(node).__name__\n    nodename = getattr(node, 'name', argname)\n    if nodename is not None:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<I>{}</I>: <B>{}</B>{}>'\n        else:\n            label_fmt = '<<I>{}</I>: <B>{}</B> \\u27f6 {}>'\n        label = label_fmt.format(nodename, name, typename)\n    else:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<B>{}</B>{}>'\n        else:\n            label_fmt = '<<B>{}</B> \\u27f6 {}>'\n        label = label_fmt.format(name, typename)\n    return label\n\n\nDEFAULT_NODE_ATTRS = {'shape': 'box', 'fontname': 'Deja Vu Sans Mono'}\n\n\ndef to_graph(expr, node_attr=None, edge_attr=None):\n    stack = [(expr, expr._safe_name)]\n    seen = set()\n    g = gv.Digraph(\n        node_attr=node_attr or DEFAULT_NODE_ATTRS, edge_attr=edge_attr or {}\n    )\n\n    g.attr(rankdir='BT')\n\n    while stack:\n        e, ename = stack.pop()\n        vkey = e._key, ename\n\n        if vkey not in seen:\n            seen.add(vkey)\n\n            vlabel = get_label(e, argname=ename)\n            vhash = str(hash(vkey))\n            g.node(vhash, label=vlabel)\n\n            node = e.op()\n            args = node.args\n            for arg, name in zip(args, node.signature.names()):\n                if isinstance(arg, ir.Expr):\n                    u = arg, name\n                    ukey = arg._key, name\n                    uhash = str(hash(ukey))\n                    ulabel = get_label(arg, argname=name)\n                    g.node(uhash, label=ulabel)\n                    g.edge(uhash, vhash)\n                    stack.append(u)\n    return g\n\n\ndef draw(graph, path=None, format='png'):\n    piped_source = graph.pipe(format=format)\n\n    if path is None:\n        with tempfile.NamedTemporaryFile(\n            delete=False, suffix='.{}'.format(format), mode='wb'\n        ) as f:\n            f.write(piped_source)\n        return f.name\n    else:\n        with open(path, mode='wb') as f:\n            f.write(piped_source)\n        return path\n\n\nif __name__ == '__main__':\n    t = ibis.table(\n        [('a', 'int64'), ('b', 'double'), ('c', 'string')], name='t'\n    )\n    left = ibis.table([('a', 'int64'), ('b', 'string')])\n    right = ibis.table([('b', 'string'), ('c', 'int64'), ('d', 'string')])\n    joined = left.inner_join(right, left.b == right.b)\n    df = joined[left.a, right.c.name('b'), right.d.name('c')]\n    a = df.a\n    b = df.b\n    filt = df[(a + b * 2 * b / b ** 3 > 4) & (b > 5)]\n    expr = filt.groupby(filt.c).aggregate(\n        amean=filt.a.mean(), bsum=filt.b.sum()\n    )\n    expr.visualize()\n", "path": "ibis/expr/visualize.py"}]}