scripts: Tweaked table renderer to not hide conflicting results

I think this is currently only possible with overlapping by/field
fields, but hiding results with conflicting by fields is not ideal.
Especially since this function is central to so many scripts:

  cat test.csv
  a,b,c
  x,2,1
  x,1,2
  x,1,3

Before:

  ./scripts/csv.py test.csv -ba -bb -fb -fc
  warning: by fields are unstable
  a,b            b        c
  x,2            2        1
  TOTAL          4        6

After:

  ./scripts/csv.py test.csv -ba -bb -fb -fc
  a,b            b        c
  x,2            2        5
  x,2            2        1
  TOTAL          4        6

This solves the main issue with unstable by fields, so no more warning.

Note that some features rely on by being unique to work (added/removed
numbers, compare fields, etc). They shouldn't error, but may be
incorrect/unintuitive with conflicting by fields, so avoiding
conflicting by fields is still a good idea.
This commit is contained in:
Christopher Haster
2026-02-02 00:41:51 -06:00
parent a3082437df
commit 8a35b9870b
9 changed files with 342 additions and 522 deletions
+38 -58
View File
@@ -503,30 +503,13 @@ def table(Result, results, diff_results=None, *,
types = Result._types
# organize by name
table = {
','.join(str(getattr(r, k)
if getattr(r, k) is not None
else '')
for k in by): r
for r in results}
diff_table = {
','.join(str(getattr(r, k)
if getattr(r, k) is not None
else '')
for k in by): r
for r in diff_results or []}
# lost results? note this can happen if a by field references the
# same field as a field field, and the field field changes during
# folding
#
# it's not an _error_, but can lead to really confusing results, so
# at least warn
if (len(table) != len(results)
or (diff_results is not None
and len(diff_table) != len(diff_results))):
print("warning: by fields are unstable",
file=sys.stderr)
def table_name(r):
return ','.join(str(getattr(r, k)
if getattr(r, k) is not None
else '')
for k in by)
table = {table_name(r): r for r in results}
diff_table = {table_name(r): r for r in diff_results or []}
# find compare entry if there is one
if compare:
@@ -568,9 +551,9 @@ def table(Result, results, diff_results=None, *,
del diff_table
# entry helper
def table_entry(name, r, diff_r=None):
def table_entry(n, r, diff_r=None):
# prepend name
entry = [name]
entry = [n]
# normal entry?
if ((compare is None or r == compare_r)
@@ -645,30 +628,26 @@ def table(Result, results, diff_results=None, *,
depth_,
prefixes=('', '', '', '')):
# build the children table at each layer
table_ = {
','.join(str(getattr(r, k)
if getattr(r, k) is not None
else '')
for k in by): r
for r in results_}
diff_table_ = {
','.join(str(getattr(r, k)
if getattr(r, k) is not None
else '')
for k in by): r
for r in diff_results_ or []}
names_ = [n
for n in table_.keys() | diff_table_.keys()
table_ = {table_name(r): r for r in results_}
diff_table_ = {table_name(r): r for r in diff_results_ or []}
# this gets a bit tricky, we want to merge both result and diff
# result names, while preserving duplicates in the result list
results__ = [(n, r)
for n, r in it.chain(
((table_name(r), r) for r in results_),
((table_name(r), None)
for r in diff_results_ or []
if table_name(r) not in table_))
if diff_results is None
or all_
or any(
types[k].ratio(
getattr(table_.get(n), k, None),
getattr(r, k, None),
getattr(diff_table_.get(n), k, None))
for k in fields)]
# sort again, now with diff info, note that python's sort is stable
names_.sort(key=lambda n: (
results__.sort(key=lambda nr: (lambda n, r: (
# sort by explicit sort fields
next(
tuple((Rev
@@ -679,35 +658,34 @@ def table(Result, results, diff_results=None, *,
else ()
for k_ in ([k] if k else Result._sort)))
for k, reverse in (sort or []))
for r_ in [table_.get(n), diff_table_.get(n)]
for r_ in [r, diff_table_.get(n)]
if r_ is not None),
# sort by ratio if diffing
Rev(tuple(types[k].ratio(
getattr(table_.get(n), k, None),
getattr(r, k, None),
getattr(diff_table_.get(n), k, None))
for k in fields))
if diff_results is not None
else (),
# move compare entry to the top, note this can be
# overridden by explicitly sorting by fields
(table_.get(n) != compare_r,
(r != compare_r,
# sort by ratio if comparing
Rev(tuple(
types[k].ratio(
getattr(table_.get(n), k, None),
getattr(r, k, None),
getattr(compare_r, k, None))
for k in fields)))
if compare
else (),
# sort by result
(table_[n],) if n in table_ else (),
(r,) if r is not None else (),
# and finally by name (diffs may be missing results)
n))
n))(*nr))
for i, name in enumerate(names_):
for i, (n, r) in enumerate(results__):
# find comparable results
r = table_.get(name)
diff_r = diff_table_.get(name)
diff_r = diff_table_.get(n)
# figure out a good label
if labels is not None:
@@ -719,26 +697,28 @@ def table(Result, results, diff_results=None, *,
for r_ in [r, diff_r]
if r_ is not None)
else:
label = name
label = n
# build line
line = table_entry(label, r, diff_r)
# add prefixes
line = [x if isinstance(x, tuple) else (x, []) for x in line]
line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1])
line[0] = (
prefixes[0+(i==len(results__)-1)] + line[0][0],
line[0][1])
lines.append(line)
# recurse?
if name in table_ and depth_ > 1:
if r is not None and depth_ > 1:
table_recurse(
getattr(r, Result._children),
getattr(diff_r, Result._children, None),
depth_-1,
(prefixes[2+(i==len(names_)-1)] + "|-> ",
prefixes[2+(i==len(names_)-1)] + "'-> ",
prefixes[2+(i==len(names_)-1)] + "| ",
prefixes[2+(i==len(names_)-1)] + " "))
(prefixes[2+(i==len(results__)-1)] + "|-> ",
prefixes[2+(i==len(results__)-1)] + "'-> ",
prefixes[2+(i==len(results__)-1)] + "| ",
prefixes[2+(i==len(results__)-1)] + " "))
# build entries
if not summary: