Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -447,6 +447,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)
10 changes: 5 additions & 5 deletions test/unit/test_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
("9:15",),
("2015-1-1 9:15",),
("2014-03-23 An activity",),
("2025-27-27 17:00 misc: testing",),
]


Expand All @@ -64,18 +65,17 @@ 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])
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.components.report_args import parse_absolute_date, parse_absolute_month, parse_date
from utt.exceptions import UttError

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
@@ -1,6 +1,7 @@
from typing import Generator, List, Optional, Tuple

from ..data_structures.entry import Entry
from ..exceptions import UttError
from .entry_lines import EntryLines
from .entry_parser import EntryParser

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 as e:
raise UttError(f"Invalid entry at line {line_number}: {line}") from e

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 as e:
raise UttError(f"Invalid date: {datestring} (expected YYYY-MM-DD)") from e


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 as e:
raise UttError(f"Invalid month: {monthstring} (expected YYYY-MM)") from e


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