diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst
index cec1f6e50c..b09b9af8e3 100644
--- a/python/CHANGELOG.rst
+++ b/python/CHANGELOG.rst
@@ -27,6 +27,9 @@
- Printing ``tskit.MetadataSchema(schema=None)`` now shows ``"Null_schema"`` rather
than ``None``, to avoid confusion (:user:`hyanwong`, :pr:`2720`)
+- Limit output HTML when a tree sequence is displayed that has a large amount of metadata.
+ (:user:`benjeffery`, :pr:`2999`)
+
**Features**
- Add ``TreeSequence.extend_haplotypes`` method that extends ancestral haplotypes
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index 72b3e4f205..54737ea83c 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -2196,6 +2196,15 @@ def test_html_repr(self, ts):
for table in ts.tables.table_name_map:
assert f"
{table.capitalize()} | " in html
+ def test_html_repr_limit(self, ts_fixture):
+ tables = ts_fixture.tables
+ d = {n: n for n in range(50)}
+ d[0] = "N" * 200
+ tables.metadata = d
+ ts = tables.tree_sequence()
+ assert "... and 20 more" in ts._repr_html_()
+ assert "NN..." in ts._repr_html_()
+
@pytest.mark.parametrize("ts", get_example_tree_sequences())
def test_str(self, ts):
s = str(ts)
diff --git a/python/tskit/util.py b/python/tskit/util.py
index fa0ce1ecb9..893c035c60 100644
--- a/python/tskit/util.py
+++ b/python/tskit/util.py
@@ -28,6 +28,7 @@
import json
import numbers
import os
+import textwrap
from typing import Union
import numpy as np
@@ -323,43 +324,65 @@ def naturalsize(value):
return (format_ + " %s") % ((base * bytes_ / unit), s)
-def obj_to_collapsed_html(d, name=None, open_depth=0):
+def obj_to_collapsed_html(d, name=None, open_depth=0, max_items=30, max_item_len=100):
"""
Recursively make an HTML representation of python objects.
:param str name: Name for this object
:param int open_depth: By default sub-sections are collapsed. If this number is
non-zero the first layers up to open_depth will be opened.
+ :param int max_items: Maximum number of items to display per collection
:return: The HTML as a string
:rtype: str
"""
opened = "open" if open_depth > 0 else ""
open_depth -= 1
- name = str(name) + ":" if name is not None else ""
- if type(d) is dict:
+ name = f"{str(name)}:" if name is not None else ""
+ if isinstance(d, dict):
+ items = list(d.items())
+ more = len(items) - max_items
+ display_items = items[:max_items] if more > 0 else items
+ inner_html = "".join(
+ f"{obj_to_collapsed_html(val, key, open_depth, max_items)}
"
+ for key, val in display_items
+ )
+ if more > 0:
+ inner_html += f"... and {more} more"
return f"""
-
-
{name}
-
+
+ {name}
+
dict
- {"".join(f"{obj_to_collapsed_html(val, key, open_depth)}
"
- for key, val in d.items())}
-
-
- """
- elif type(d) is list:
+ {inner_html}
+
+
+ """
+ elif isinstance(d, list):
+ items = d
+ more = len(items) - max_items
+ display_items = items[:max_items] if more > 0 else items
+ inner_html = "".join(
+ f"{obj_to_collapsed_html(val, None, open_depth, max_items)}
"
+ for val in display_items
+ )
+ if more > 0:
+ inner_html += f"... and {more} more"
return f"""
-
-
{name}
-
+
+ {name}
+
list
- {"".join(f"{obj_to_collapsed_html(val, None, open_depth)}
"
- for val in d)}
-
-
- """
+ {inner_html}
+
+
+ """
else:
- return f"{name} {d}"
+ d_str = str(d)
+ if len(d_str) > max_item_len:
+ d_str = d_str[:max_item_len] + "..."
+ d_str = textwrap.fill(d_str, width=30)
+ d_str = d_str.replace("\n", "
")
+ return f"{name} {d_str}"
def truncate_string_end(string, length):