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):