Fix weekly summary crash on open entries
print_summary_weekly, print_summary_weekly_totals and _aggregate summed raw duration_hours, which is None for open (unfinished) entries, raising TypeError: float + NoneType. Skip open entries from the weekly totals, matching to_csv_entries and parser.aggregate. Add regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b2b45fd4e1
commit
8f5232d1fe
2 changed files with 50 additions and 2 deletions
|
|
@ -163,7 +163,12 @@ def print_summary_weekly(
|
||||||
day_sections: list[tuple[date, list[dict]]], project_map: dict
|
day_sections: list[tuple[date, list[dict]]], project_map: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Print a full summary for each day of the week."""
|
"""Print a full summary for each day of the week."""
|
||||||
week_total = sum(e["duration_hours"] for _, rows in day_sections for e in rows)
|
week_total = sum(
|
||||||
|
e["duration_hours"]
|
||||||
|
for _, rows in day_sections
|
||||||
|
for e in rows
|
||||||
|
if e["duration_hours"] is not None
|
||||||
|
)
|
||||||
for day, rows in day_sections:
|
for day, rows in day_sections:
|
||||||
aggregated = _aggregate(rows)
|
aggregated = _aggregate(rows)
|
||||||
day_total = sum(e["quantity"] for e in aggregated)
|
day_total = sum(e["quantity"] for e in aggregated)
|
||||||
|
|
@ -221,7 +226,9 @@ def print_summary_weekly_totals(
|
||||||
rows_out = []
|
rows_out = []
|
||||||
|
|
||||||
for day, rows in day_sections:
|
for day, rows in day_sections:
|
||||||
day_total = sum(e["duration_hours"] for e in rows)
|
day_total = sum(
|
||||||
|
e["duration_hours"] for e in rows if e["duration_hours"] is not None
|
||||||
|
)
|
||||||
week_total += day_total
|
week_total += day_total
|
||||||
rows_out.append((day.strftime("%A"), day.strftime("%Y-%m-%d"), day_total))
|
rows_out.append((day.strftime("%A"), day.strftime("%Y-%m-%d"), day_total))
|
||||||
|
|
||||||
|
|
@ -245,6 +252,8 @@ def _aggregate(rows: list[dict]) -> list[dict]:
|
||||||
key_order: list[tuple] = []
|
key_order: list[tuple] = []
|
||||||
totals: dict[tuple, float] = defaultdict(float)
|
totals: dict[tuple, float] = defaultdict(float)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
if row["duration_hours"] is None:
|
||||||
|
continue
|
||||||
description = build_description(row["story"], row["note"])
|
description = build_description(row["story"], row["note"])
|
||||||
key = (row["project"].strip(), description)
|
key = (row["project"].strip(), description)
|
||||||
if key not in totals:
|
if key not in totals:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import csv
|
import csv
|
||||||
|
import datetime
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -6,6 +7,9 @@ import pytest
|
||||||
from timesheets.output import (
|
from timesheets.output import (
|
||||||
print_stories,
|
print_stories,
|
||||||
print_summary,
|
print_summary,
|
||||||
|
print_summary_weekly,
|
||||||
|
print_summary_weekly_short,
|
||||||
|
print_summary_weekly_totals,
|
||||||
to_csv_entries,
|
to_csv_entries,
|
||||||
write_csv,
|
write_csv,
|
||||||
write_csv_weekly,
|
write_csv_weekly,
|
||||||
|
|
@ -330,3 +334,38 @@ class TestPrintStories:
|
||||||
print_stories([])
|
print_stories([])
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert out == ""
|
assert out == ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Weekly summaries with open (unfinished) entries
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeeklySummaryOpenEntries:
|
||||||
|
"""An open entry (start present, end absent) has duration_hours=None and
|
||||||
|
must be skipped from totals, not crash the weekly summary commands."""
|
||||||
|
|
||||||
|
def _sections(self):
|
||||||
|
rows = [
|
||||||
|
_raw_row("bugs", "ticket 1", "", 1.0),
|
||||||
|
_raw_row("bugs", "ticket 2", "", None), # open entry
|
||||||
|
]
|
||||||
|
return [(datetime.date(2026, 1, 6), rows)]
|
||||||
|
|
||||||
|
def test_weekly_totals_skips_open_entry(self, capsys):
|
||||||
|
print_summary_weekly_totals(self._sections(), {})
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
# only the 1.0h closed entry counts
|
||||||
|
assert "01:00" in out
|
||||||
|
assert "WEEK TOTAL" in out
|
||||||
|
|
||||||
|
def test_weekly_full_skips_open_entry(self, capsys):
|
||||||
|
print_summary_weekly(self._sections(), {})
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "WEEK TOTAL" in out
|
||||||
|
assert "01:00" in out
|
||||||
|
|
||||||
|
def test_weekly_short_skips_open_entry(self, capsys):
|
||||||
|
print_summary_weekly_short(self._sections(), {})
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "01:00" in out
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue