Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for how to contribute to utt.
- Stephan Gross <<stephangross6@gmail.com>>
- Kent Martin <<kentaasvang@gmail.com>>
- fighterpoul <<fighter.poul@gmail.com>>
- Logan Thomas <<logan@datacentriq.net>>


## License
Expand Down
10 changes: 10 additions & 0 deletions test/integration/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ all: \
report-details \
report-comments \
report-week-current \
error-invalid-date \
version

$(UTT):
Expand Down Expand Up @@ -368,6 +369,15 @@ report-truncate-current-activity: $(UTT)
@echo "<< REPORT-TRUNCATE-CURRENT-ACTIVITY"


.PHONY: error-invalid-date
error-invalid-date: $(UTT)
@echo
@echo ">> ERROR-INVALID-DATE"

bash -c 'diff <(utt report not-a-date 2>&1; true) data/utt-error-invalid-date.stderr'

@echo "<< ERROR-INVALID-DATE"

.PHONY: shell
shell:
bash
1 change: 1 addition & 0 deletions test/integration/data/utt-error-invalid-date.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
error: Invalid date: not-a-date (expected YYYY-MM-DD)
15 changes: 10 additions & 5 deletions test/unit/test_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,23 @@ def test_valid_entries(self):
with self.subTest(name=test_case["name"]):
entry_parser = EntryParser()
entry = entry_parser.parse(test_case["name"])
if entry is None:
self.fail("EntryParser returned None for valid entry")

self.assertEqual(entry.datetime, test_case["expected_datetime"])
self.assertEqual(entry.name, test_case["expected_name"])
self.assertEqual(entry.comment, test_case["expected_comment"])


class InvalidEntry(unittest.TestCase):
def test_invalid_entries(self):
def test_invalid_entries_raise_value_error(self):
"""Test that invalid entries raise ValueError."""
for test_case in INVALID_ENTRIES:
with self.subTest(text=test_case[0]):
entry_parser = EntryParser()
entry = entry_parser.parse(test_case[0])
self.assertIsNone(entry)
with self.assertRaises(ValueError):
entry_parser.parse(test_case[0])

def test_invalid_date_raises_value_error(self):
"""Test that entries with invalid dates raise ValueError."""
entry_parser = EntryParser()
with self.assertRaises(ValueError):
entry_parser.parse("2025-27-27 17:00 misc: testing")
Comment thread
loganthomas marked this conversation as resolved.
Outdated
19 changes: 18 additions & 1 deletion test/unit/test_parse_date.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import datetime
import unittest

from utt.components.report_args import parse_date
from utt.exceptions import UttError
from utt.components.report_args import parse_absolute_date, parse_absolute_month, parse_date

VALID_ENTRIES = [
("monday", datetime.date(2015, 2, 11), datetime.date(2015, 2, 9), True),
Expand Down Expand Up @@ -29,3 +30,19 @@ def test_parse_date(self):
with self.subTest(report_date=report_date, today=today, is_past=is_past):
actual_report_date = parse_date(today, report_date, is_past)
self.assertEqual(actual_report_date, expected_report_date)

def test_invalid_date_raises_utt_error(self):
with self.assertRaises(UttError):
parse_absolute_date("invalid-date")

def test_invalid_month_raises_utt_error(self):
with self.assertRaises(UttError):
parse_absolute_month("invalid-month")

def test_valid_absolute_date(self):
result = parse_absolute_date("2024-01-15")
self.assertEqual(result, datetime.date(2024, 1, 15))

def test_valid_absolute_month(self):
result = parse_absolute_month("2024-01")
self.assertEqual(result, datetime.date(2024, 1, 1))
7 changes: 6 additions & 1 deletion utt/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import utt.plugins
from utt.api import _v1
from utt.components.commands import Commands
from utt.exceptions import UttError


def iter_namespace(ns_pkg):
Expand Down Expand Up @@ -34,7 +35,11 @@ def main():
commands: Commands = _v1._private.container[Commands]
for command in commands:
if command.name == command_name:
_v1._private.container[command.handler_class]()
try:
_v1._private.container[command.handler_class]()
except UttError as e:
print(f"error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
Expand Down
10 changes: 6 additions & 4 deletions utt/components/entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ..data_structures.entry import Entry
from .entry_lines import EntryLines
from .entry_parser import EntryParser
from ..exceptions import UttError

Entries = List[Entry]

Expand All @@ -26,12 +27,13 @@ def _parse_line(previous_entry: Optional[Entry], line_number: int, line: str, en
if not line:
return None

new_entry = entry_parser.parse(line)
if new_entry is None:
raise SyntaxError("Invalid syntax at line %d: %s" % (line_number, line))
try:
new_entry = entry_parser.parse(line)
except ValueError:
raise UttError(f"Invalid entry at line {line_number}: {line}")

if previous_entry is not None and previous_entry.datetime > new_entry.datetime:
raise Exception("Error line %d. Not in chronological order: %s > %s" % (line_number, previous_entry, new_entry))
raise UttError(f"Line {line_number} not in chronological order: {line}")

previous_entry = new_entry
return previous_entry, new_entry
12 changes: 8 additions & 4 deletions utt/components/entry_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
import re
from typing import Optional

from ..data_structures.entry import Entry

Expand All @@ -12,16 +11,21 @@


class EntryParser:
def parse(self, string: str) -> Optional[Entry]:
def parse(self, string: str) -> Entry:
"""Parse a log line into an Entry.

Raises:
ValueError: If the line cannot be parsed.
"""
match = ENTRY_REGEX.match(string)

if match is None:
return None
raise ValueError(f"Invalid syntax: {string}")

groupdict = match.groupdict()

if "date" not in groupdict or "name" not in groupdict:
return None
raise ValueError(f"Invalid syntax: {string}")

date_str = groupdict["date"]
date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M")
Expand Down
11 changes: 9 additions & 2 deletions utt/components/report_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from enum import Enum, auto
from typing import NamedTuple, Optional

from ..exceptions import UttError
from .now import Now


Expand Down Expand Up @@ -76,7 +77,10 @@ def parse_date(today: datetime.date, datestring: str, is_past: bool):


def parse_absolute_date(datestring):
return datetime.datetime.strptime(datestring, "%Y-%m-%d").date()
try:
return datetime.datetime.strptime(datestring, "%Y-%m-%d").date()
except ValueError:
raise UttError(f"Invalid date: {datestring} (expected YYYY-MM-DD)")


def parse_relative_day(today, datestring):
Expand Down Expand Up @@ -168,7 +172,10 @@ def parse_integer_month(today, monthstring):


def parse_absolute_month(monthstring):
return datetime.datetime.strptime(monthstring, "%Y-%m").date()
try:
return datetime.datetime.strptime(monthstring, "%Y-%m").date()
except ValueError:
raise UttError(f"Invalid month: {monthstring} (expected YYYY-MM)")


def parse_month(today, monthstring):
Expand Down
7 changes: 7 additions & 0 deletions utt/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""General utt exceptions and warnings."""


class UttError(Exception):
"""User-facing error with a friendly message."""

pass
Loading