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:
Jef Roosens 2026-05-30 19:57:36 +02:00
parent b2b45fd4e1
commit 8f5232d1fe
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
2 changed files with 50 additions and 2 deletions

View file

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

View file

@ -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