Repository: pimutils/khal
Pull request #1041 (2021-05-12 17:12:05)

Additional test for color_wrap()
This is an attempt to reproduce #906

geier: This actually fixes a bug, that somehow never occurred before.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 35a51c1..b116617 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,7 +13,6 @@ may want to subscribe to `GitHub's tag feed
 not released
 
 * DROPPED support for Python 3.5
-* CHANGE: ikhal: tab (and shift tab) jump from the events back to the calendar
 
 0.10.3
 ======
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index 42dfa40..2dcd68f 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -218,18 +218,9 @@ class Event:
 
     @property
     def recurring(self):
-        try:
-            rval = 'RRULE' in self._vevents[self.ref] or \
-                'RECURRENCE-ID' in self._vevents[self.ref] or \
-                'RDATE' in self._vevents[self.ref]
-        except KeyError:
-            logger.fatal(
-                f"The event at {self.href} might be broken. You might want to "
-                "file an issue at https://github.com/pimutils/khal/issues"
-            )
-            raise
-        else:
-            return rval
+        return 'RRULE' in self._vevents[self.ref] or \
+            'RECURRENCE-ID' in self._vevents[self.ref] or \
+            'RDATE' in self._vevents[self.ref]
 
     @property
     def recurpattern(self):
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index 466b7fd..d1ef1c8 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -31,7 +31,7 @@ from .. import icalendar as icalendar_helpers, utils
 from ..khalendar.event import Event
 from ..khalendar.exceptions import ReadOnlyCalendarError
 from . import colors
-from .widgets import ExtendedEdit as Edit, NPile, NColumns, linebox
+from .widgets import ExtendedEdit as Edit, NPile, NColumns, NListBox, linebox
 from .base import Pane, Window
 from .editor import EventEditor, ExportDialog
 from .calendarwidget import CalendarWidget
@@ -555,7 +555,7 @@ class StaticDayWalker(DayWalker):
         return urwid.SimpleFocusListWalker.set_focus(self, position)
 
 
-class DateListBox(urwid.ListBox):
+class DateListBox(NListBox):
     """A ListBox container for a SimpleFocusListWalker, that contains one day
     worth of events"""
 
diff --git a/khal/utils.py b/khal/utils.py
index 9cd207f..dede681 100644
--- a/khal/utils.py
+++ b/khal/utils.py
@@ -29,9 +29,10 @@ import re
 import string
 from calendar import month_abbr, timegm
 from textwrap import wrap
+from typing import Iterator, List, Tuple, Optional
 
 
-def generate_random_uid():
+def generate_random_uid() -> str:
     """generate a random uid
 
     when random isn't broken, getting a random UID from a pool of roughly 10^56
@@ -49,7 +50,7 @@ ansi_sgr = re.compile(r'\x1b\['
                       'm')
 
 
-def find_last_reset(string):
+def find_last_reset(string: str) -> Tuple[int, int, str]:
     for match in re.finditer(ansi_reset, string):  # noqa B007: this is actually used below.
         pass
     try:
@@ -58,7 +59,7 @@ def find_last_reset(string):
         return -2, -1, ''
 
 
-def find_last_sgr(string):
+def find_last_sgr(string: str) -> Tuple[int, int, str]:
     for match in re.finditer(ansi_sgr, string):  # noqa B007: this is actually used below.
         pass
     try:
@@ -67,16 +68,16 @@ def find_last_sgr(string):
         return -2, -1, ''
 
 
-def find_unmatched_sgr(string):
+def find_unmatched_sgr(string: str) -> Optional[str]:
     reset_pos, _, _ = find_last_reset(string)
     sgr_pos, _, sgr = find_last_sgr(string)
     if sgr_pos > reset_pos:
         return sgr
     else:
-        return False
+        return None
 
 
-def color_wrap(text, width=70):
+def color_wrap(text: str, width: int = 70) -> List[str]:
     """A variant of wrap that takes SGR codes (somewhat) into account.
 
     This doesn't actually adjust the length, but makes sure that
@@ -87,32 +88,30 @@ def color_wrap(text, width=70):
     lines = wrap(text, width)
     for num, _ in enumerate(lines):
         sgr = find_unmatched_sgr(lines[num])
-        if sgr:
+        if sgr is not None:
             lines[num] += RESET
-            if num != len(lines):
+            if (num + 1) < len(lines):
                 lines[num + 1] = sgr + lines[num + 1]
     return lines
 
 
-def get_weekday_occurrence(day):
+def get_weekday_occurrence(day: dt.date) -> Tuple[int, int]:
     """Calculate how often this weekday has already occurred in a given month.
 
-    :type day: datetime.date
     :returns: weekday (0=Monday, ..., 6=Sunday), occurrence
-    :rtype: tuple(int, int)
     """
     xthday = 1 + (day.day - 1) // 7
     return day.weekday(), xthday
 
 
-def get_month_abbr_len():
+def get_month_abbr_len() -> int:
     """Calculate the number of characters we need to display the month
     abbreviated name. It depends on the locale.
     """
     return max(len(month_abbr[i]) for i in range(1, 13)) + 1
 
 
-def localize_strip_tz(dates, timezone):
+def localize_strip_tz(dates: List[dt.datetime], timezone: dt.tzinfo) -> Iterator[dt.datetime]:
     """converts a list of dates to timezone, than removes tz info"""
     for one_date in dates:
         if getattr(one_date, 'tzinfo', None) is not None:
@@ -129,7 +128,7 @@ def to_unix_time(dtime: dt.datetime) -> float:
     return unix_time
 
 
-def to_naive_utc(dtime):
+def to_naive_utc(dtime: dt.datetime) -> dt.datetime:
     """convert a datetime object to UTC and than remove the tzinfo, if
     datetime is naive already, return it
     """
@@ -141,7 +140,7 @@ def to_naive_utc(dtime):
     return dtime_naive
 
 
-def is_aware(dtime):
+def is_aware(dtime: dt.datetime) -> bool:
     """test if a datetime instance is timezone aware"""
     if dtime.tzinfo is not None and dtime.tzinfo.utcoffset(dtime) is not None:
         return True
@@ -149,11 +148,8 @@ def is_aware(dtime):
         return False
 
 
-def relative_timedelta_str(day):
+def relative_timedelta_str(day: dt.date) -> str:
     """Converts the timespan from `day` to today into a human readable string.
-
-    :type day: datetime.date
-    :rtype: str
     """
     days = (day - dt.date.today()).days
     if days < 0:


Test patch:
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 98074ac..ac1d153 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -36,9 +36,9 @@ def test_last_sgr():
 
 
 def test_find_unmatched_sgr():
-    assert utils.find_unmatched_sgr(weekheader) is False
-    assert utils.find_unmatched_sgr(today_line) is False
-    assert utils.find_unmatched_sgr(calendarline) is False
+    assert utils.find_unmatched_sgr(weekheader) is None
+    assert utils.find_unmatched_sgr(today_line) is None
+    assert utils.find_unmatched_sgr(calendarline) is None
     assert utils.find_unmatched_sgr('\x1b[31mHello World') == '\x1b[31m'
     assert utils.find_unmatched_sgr('\x1b[31mHello\x1b[0m \x1b[32mWorld') == '\x1b[32m'
     assert utils.find_unmatched_sgr('foo\x1b[1;31mbar') == '\x1b[1;31m'
@@ -74,6 +74,31 @@ def test_color_wrap_256():
     assert utils.color_wrap(text, 30) == expected
 
 
+def test_color_wrap_multiple_colors_and_tabs():
+    text = (
+        "\x1b[31m14:00-14:50    AST-1002-102 INTRO AST II/STAR GALAX (R) Classes",
+        "15:30-16:45    PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R) Classes",
+        "\x1b[38;2;255;0m17:00-18:00    Pay Ticket Deadline Calendar",
+        "09:30-10:45    PHL-1501-101 MIND, KNOWLEDGE & REALITY (R) Classes",
+        "\x1b[38;2;255;0m11:00-14:00    Rivers Street (noodles and pizza) (R) Calendar",
+    )
+    expected = [
+      '\x1b[31m14:00-14:50    AST-1002-102 INTRO AST II/STAR GALAX (R)\x1b[0m',
+      '\x1b[31mClasses\x1b[0m',
+      '15:30-16:45    PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R)',
+      'Classes',
+      '\x1b[38;2;255;0m17:00-18:00    Pay Ticket Deadline Calendar\x1b[0m',
+      '09:30-10:45    PHL-1501-101 MIND, KNOWLEDGE & REALITY (R)',
+      'Classes',
+      '\x1b[38;2;255;0m11:00-14:00    Rivers Street (noodles and\x1b[0m',
+      '\x1b[38;2;255;0mpizza) (R) Calendar\x1b[0m'
+    ]
+    actual = list()
+    for line in text:
+        actual += utils.color_wrap(line, 60)
+    assert actual == expected
+
+
 def test_get_weekday_occurrence():
     assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 1)) == (2, 1)
     assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 2)) == (3, 1)