diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index da36514186..1a00e54f7a 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -85,9 +85,11 @@ ``pack_untracked_polytomies`` allows large polytomies involving untracked samples to be summarised as a dotted line (:user:`hyanwong`, :issue:`3011` :pr:`3010`, :pr:`3012`) - - Added a ``title`` parameter to ``.draw_svg()`` methods (:user:`hyanwong`, :pr:`3015`) +- Add comma separation to all display numbers. (:user:`benjeffery`, :issue:`3017`, :pr:`3018`) + + -------------------- [0.5.8] - 2024-06-27 -------------------- diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py index ce1bda61a2..e560fbfe40 100644 --- a/python/tests/test_highlevel.py +++ b/python/tests/test_highlevel.py @@ -39,7 +39,6 @@ import random import re import tempfile -import textwrap import unittest import uuid as _uuid import warnings @@ -2191,7 +2190,7 @@ def test_html_repr(self, ts): # Parse to check valid ElementTree.fromstring(html) assert len(html) > 5000 - assert f"Trees{ts.num_trees}" in html + assert f"Trees{ts.num_trees:,}" in html assert f"Time Units{ts.time_units}" in html for table in ts.tables.table_name_map: assert f"{table.capitalize()}" in html @@ -3759,32 +3758,29 @@ def test_complex_mutations(self): def test_str(self, ts_fixture): t = ts_fixture.first() assert isinstance(str(t), str) - assert re.match( - textwrap.dedent( - r""" - ╔═════════════════════════════╗ - ║Tree ║ - ╠═══════════════════╤═════════╣ - ║Index │ 0║ - ╟───────────────────┼─────────╢ - ║Interval │ 0-1 \(1\)║ - ╟───────────────────┼─────────╢ - ║Roots │[0-9 ]*║ - ╟───────────────────┼─────────╢ - ║Nodes │[0-9 ]*║ - ╟───────────────────┼─────────╢ - ║Sites │[0-9 ]*║ - ╟───────────────────┼─────────╢ - ║Mutations │[0-9 ]*║ - ╟───────────────────┼─────────╢ - ║Total Branch Length│[0-9\. ]*║ - ╚═══════════════════╧═════════╝ - """[ - 1: - ] - ), - str(t), + pattern = re.compile( + r""" + ╔═+╗\s* + ║Tree.*?║\s* + ╠═+╤═+╣\s* + ║Index.*?│\s*\d+║\s* + ╟─+┼─+╢\s* + ║Interval.*?│\s*\d+-\d+\s*\(\d+\)║\s* + ╟─+┼─+╢\s* + ║Roots.*?│\s*\d+║\s* + ╟─+┼─+╢\s* + ║Nodes.*?│\s*\d+║\s* + ╟─+┼─+╢\s* + ║Sites.*?│\s*\d+║\s* + ╟─+┼─+╢\s* + ║Mutations.*?│\s*\d+║\s* + ╟─+┼─+╢\s* + ║Total\s*Branch\s*Length.*?│\s*[\d,]+\.\d+║\s* + ╚═+╧═+╝\s* + """, + re.VERBOSE | re.DOTALL, ) + assert pattern.search(str(t)) def test_html_repr(self, ts_fixture): html = ts_fixture.first()._repr_html_() diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py index 19fd0f104d..3b1bd50bad 100644 --- a/python/tests/test_tables.py +++ b/python/tests/test_tables.py @@ -697,7 +697,7 @@ def test_str_pos_time_integer(self): table.set_columns(**input_data) _, rows = table._text_header_and_rows() for row in rows: - assert f"{identifiable_integers[0]}" in row + assert f"{identifiable_integers[0]:,}" in row assert f"{identifiable_floats[0]:.8f}" not in row def test_repr_html(self): diff --git a/python/tskit/genotypes.py b/python/tskit/genotypes.py index 239e135777..cf82ca8902 100644 --- a/python/tskit/genotypes.py +++ b/python/tskit/genotypes.py @@ -1,7 +1,7 @@ # # MIT License # -# Copyright (c) 2018-2023 Tskit Developers +# Copyright (c) 2018-2024 Tskit Developers # Copyright (c) 2015-2018 University of Oxford # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -311,16 +311,16 @@ def __str__(self) -> str: freqs = self.frequencies() rows = ( [ - ["Site id", str(site_id)], - ["Site position", str(site_position)], - ["Number of samples", str(len(self.samples))], - ["Number of alleles", str(self.num_alleles)], + ["Site id", f"{site_id:,}"], + ["Site position", f"{site_position:,}"], + ["Number of samples", f"{len(self.samples):,}"], + ["Number of alleles", f"{self.num_alleles:,}"], ] + [ [ - f"""Samples with allele {'missing' if k is None - else "'" + k + "'"}""", - f"{counts[k]} ({freqs[k] * 100:.2g}%)", + f"Samples with allele " + f"""{'missing' if k is None else "'" + k + "'"}""", + f"{counts[k]:,} ({freqs[k] * 100:.2g}%)", ] for k in self.alleles ] diff --git a/python/tskit/tables.py b/python/tskit/tables.py index 0aade18c66..d873cf0462 100644 --- a/python/tskit/tables.py +++ b/python/tskit/tables.py @@ -864,13 +864,13 @@ def _text_header_and_rows(self, limit=None): row_indexes = util.truncate_rows(self.num_rows, limit) for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] location_str = ", ".join(map(str, row.location)) - parents_str = ", ".join(map(str, row.parents)) + parents_str = ", ".join([f"{p:,}" for p in row.parents]) rows.append( - "{}\t{}\t{}\t{}\t{}".format( + "{:,}\t{}\t{}\t{}\t{}".format( j, row.flags, location_str, @@ -1139,10 +1139,10 @@ def _text_header_and_rows(self, limit=None): for j in row_indexes: row = self[j] if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: rows.append( - "{}\t{}\t{}\t{}\t{:.{dp}f}\t{}".format( + "{:,}\t{}\t{:,}\t{:,}\t{:,.{dp}f}\t{}".format( j, row.flags, row.population, @@ -1332,11 +1332,11 @@ def _text_header_and_rows(self, limit=None): decimal_places = 0 if self._columns_all_integer("left", "right") else 8 for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] rows.append( - "{}\t{:.{dp}f}\t{:.{dp}f}\t{}\t{}\t{}".format( + "{:,}\t{:,.{dp}f}\t{:,.{dp}f}\t{:,}\t{:,}\t{}".format( j, row.left, row.right, @@ -1548,11 +1548,11 @@ def _text_header_and_rows(self, limit=None): decimal_places_times = 0 if self._columns_all_integer("time") else 8 for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] rows.append( - "{}\t{:.{dp_c}f}\t{:.{dp_c}f}\t{}\t{}\t{}\t{:.{dp_t}f}\t{}".format( + "{:,}\t{:,.{dp_c}f}\t{:,.{dp_c}f}\t{:,}\t{:,}\t{:,}\t{:,.{dp_t}f}\t{}".format( j, row.left, row.right, @@ -1563,7 +1563,9 @@ def _text_header_and_rows(self, limit=None): util.render_metadata(row.metadata), dp_c=decimal_places_coords, dp_t=decimal_places_times, - ).split("\t") + ).split( + "\t" + ) ) return headers, rows @@ -1760,11 +1762,11 @@ def _text_header_and_rows(self, limit=None): decimal_places = 0 if self._columns_all_integer("position") else 8 for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] rows.append( - "{}\t{:.{dp}f}\t{}\t{}".format( + "{:,}\t{:,.{dp}f}\t{}\t{}".format( j, row.position, row.ancestral_state, @@ -1977,11 +1979,11 @@ def _text_header_and_rows(self, limit=None): decimal_places_times = 0 if self._columns_all_integer("time") else 8 for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] rows.append( - "{}\t{}\t{}\t{:.{dp}f}\t{}\t{}\t{}".format( + "{:,}\t{:,}\t{:,}\t{:,.{dp}f}\t{}\t{:,}\t{}".format( j, row.site, row.node, @@ -2256,7 +2258,7 @@ def _text_header_and_rows(self, limit=None): row_indexes = util.truncate_rows(self.num_rows, limit) for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: rows.append((str(j), util.render_metadata(self[j].metadata, length=70))) return headers, rows @@ -2507,7 +2509,7 @@ def _text_header_and_rows(self, limit=None): row_indexes = util.truncate_rows(self.num_rows, limit) for j in row_indexes: if j == -1: - rows.append(f"__skipped__{self.num_rows-limit}") + rows.append(f"__skipped__{self.num_rows - limit}") else: row = self[j] rows.append( diff --git a/python/tskit/trees.py b/python/tskit/trees.py index fee125caa7..79ca182d67 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -2797,16 +2797,17 @@ def __str__(self): Return a plain text summary of a tree in a tree sequence """ tree_rows = [ - ["Index", str(self.index)], + ["Index", f"{self.index:,}"], [ "Interval", - f"{self.interval.left:.8g}-{self.interval.right:.8g} ({self.span:.8g})", + f"{self.interval.left:,.8g}-{self.interval.right:,.8g}" + f"({self.span:,.8g})", ], - ["Roots", str(self.num_roots)], - ["Nodes", str(len(self.preorder()))], - ["Sites", str(self.num_sites)], - ["Mutations", str(self.num_mutations)], - ["Total Branch Length", f"{self.total_branch_length:.8g}"], + ["Roots", f"{self.num_roots:,}"], + ["Nodes", f"{len(self.preorder()):,}"], + ["Sites", f"{self.num_sites:,}"], + ["Mutations", f"{self.num_mutations:,}"], + ["Total Branch Length", f"{self.total_branch_length:,.8g}"], ] return util.unicode_table(tree_rows, title="Tree") @@ -4369,17 +4370,14 @@ def __str__(self): for name, table in self.tables.table_name_map.items(): table_rows.append( [ - str(s) - for s in [ - name.capitalize(), - table.num_rows, - util.naturalsize(table.nbytes), - ( - "Yes" - if hasattr(table, "metadata") and len(table.metadata) > 0 - else "No" - ), - ] + name.capitalize(), + f"{table.num_rows:,}", + util.naturalsize(table.nbytes), + ( + "Yes" + if hasattr(table, "metadata") and len(table.metadata) > 0 + else "No" + ), ] ) return util.unicode_table(ts_rows, title="TreeSequence") + util.unicode_table( diff --git a/python/tskit/util.py b/python/tskit/util.py index 7f7a358fec..90dbe1720b 100644 --- a/python/tskit/util.py +++ b/python/tskit/util.py @@ -518,7 +518,7 @@ def tree_sequence_html(ts): f""" {name.capitalize()} - {table.num_rows} + {table.num_rows:,} {naturalsize(table.nbytes)} {'✅' if hasattr(table, "metadata") and len(table.metadata) > 0 @@ -598,10 +598,10 @@ def tree_sequence_html(ts): - Trees{ts.num_trees} - Sequence Length{ts.sequence_length} + Trees{ts.num_trees:,} + Sequence Length{ts.sequence_length:,} Time Units{ts.time_units} - Sample Nodes{ts.num_samples} + Sample Nodes{ts.num_samples:,} Total Size{naturalsize(ts.nbytes)} Metadata{md} @@ -670,13 +670,13 @@ def tree_html(tree): - Index{tree.index} - Interval{tree.interval.left:.8g}-{tree.interval.right:.8g} ({tree.span:.8g}) - Roots{tree.num_roots} - Nodes{len(tree.preorder())} - Sites{tree.num_sites} - Mutations{tree.num_mutations} - Total Branch Length{tree.total_branch_length:.8g} + Index{tree.index:,} + Interval{tree.interval.left:,.8g}-{tree.interval.right:,.8g} ({tree.span:,.8g}) + Roots{tree.num_roots:,} + Nodes{len(tree.preorder()):,} + Sites{tree.num_sites:,} + Mutations{tree.num_mutations:,} + Total Branch Length{tree.total_branch_length:,.8g} @@ -745,18 +745,18 @@ def variant_html(variant): return ( html_body_head + f""" - Site Id{site_id} - Site Position{site_position} - Number of Samples{num_samples} - Number of Alleles{num_alleles} + Site Id{site_id:,} + Site Position{site_position:,.8g} + Number of Samples{num_samples:,} + Number of Alleles{num_alleles:,} """ + "\n".join( [ f"""Samples with Allele {'missing' if k is None else "'" + k + "'"}""" - + f"{counts[k]}" + + f"{counts[k]:,}" + " " - + f"({freqs[k] * 100:.2g}%)" + + f"({freqs[k] * 100:,.2g}%)" + "" for k in variant.alleles ]