Skip to content

Commit d6e29dd

Browse files
committed
Refactor backend and introduce type hinting
Some logic gets moved out of the SQLite backend. Until now, the SQLiteDb class accepted VEVENTs as strings, but returned our own Event instances. Now, it accepts and returns strings. Some reshuffling of tests was needed as well. Also, this commit introduces PEP484 type hinting for all methods and functions in khalendar.py and backend.py. This is to be expanded to other parts.
1 parent 62ff885 commit d6e29dd

15 files changed

+601
-557
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ dist/
1212
.cache
1313
htmlcov
1414
doc/source/configspec.rst
15+
.mypy_cache/

.travis.yml

+6
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ matrix:
1616
env: BUILD=style
1717
- python: 3.6
1818
env: BUILD=docs
19+
- python: 3.6
20+
env: BUILD=mypy
1921
- python: 3.6
2022
env: BUILD=pytz201610
23+
- python: 3.6
24+
env: BUILD=pytz201702
25+
- python: 3.6
26+
env: BUILD=pytz_latest
2127

2228
addons:
2329
apt:

CHANGELOG.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ may want to subscribe to `GitHub's tag feed
1212
not released yet
1313

1414
* NEW DEPENDENCY added click_log >= 0.2.0
15+
* NEW DEPENDENCY for Python 3.4: typing
16+
* DROPPED support for Python 3.3
1517
* vdirsyncer is still a test dependency (and always has been)
1618

1719
* FIX ordinal numbers in birthday entries (before, all number would end on `th`)
@@ -27,7 +29,6 @@ not released yet
2729
* CHANGE `search` will now print one line for every different event in a
2830
recurrence set, that is one line for the master event, and one line for every
2931
different overwritten event
30-
* CHANGE drop support for Python 3.3.
3132

3233
* NEW khal learned the ``--logfile/-l LOGFILE`` flag which allows logging to a
3334
file
@@ -57,7 +58,7 @@ released 2017-06-13
5758
present in an .ics file
5859
* FIX .ics files containing only overwritten instances are not expanded anymore,
5960
even if they contain a RRULE or RDATE
60-
* FIX valid UNTIL entry for recurring datetime events
61+
* FIX valid UNTIL entry for recurring datetime events
6162

6263
* CHANGE the symbol used for indicating a recurring event now has a space in
6364
front of it, also the ascii version changed to `(R)`

khal/khalendar/backend.py

+146-177
Large diffs are not rendered by default.

khal/khalendar/event.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self, vevents, ref=None, **kwargs):
6969
self.href = kwargs.pop('href', None)
7070
self.etag = kwargs.pop('etag', None)
7171
self.calendar = kwargs.pop('calendar', None)
72+
self.color = kwargs.pop('color', None)
7273
self.ref = ref
7374

7475
start = kwargs.pop('start', None)
@@ -846,11 +847,3 @@ def _create_timezone_static(tz):
846847
subcomp.add('TZOFFSETFROM', tz._utcoffset)
847848
timezone.add_component(subcomp)
848849
return timezone
849-
850-
851-
class EventStandIn():
852-
def __init__(self, calendar):
853-
self.calendar = calendar
854-
self.color = None
855-
self.unicode_symbols = None
856-
self.readonly = None

khal/khalendar/exceptions.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2020
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

22+
from typing import Optional # noqa
23+
2224
from ..exceptions import Error, FatalError, UnsupportedFeatureError
2325

2426

@@ -57,4 +59,4 @@ class UpdateFailed(Error):
5759
class DuplicateUid(Error):
5860

5961
"""an event with this UID already exists"""
60-
existing_href = None
62+
existing_href = None # type: Optional[str]

khal/khalendar/khalendar.py

+88-65
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import logging
3131
import os
3232
import os.path
33+
from typing import Dict, Iterable, List, Optional, Tuple, Union # noqa
3334

3435
from . import backend
3536
from .event import Event
@@ -42,14 +43,14 @@
4243
logger = logging.getLogger(__name__)
4344

4445

45-
def create_directory(path):
46+
def create_directory(path: str):
4647
if not os.path.isdir(path):
4748
if os.path.exists(path):
4849
raise RuntimeError('{0} is not a directory.'.format(path))
4950
try:
5051
os.makedirs(path, mode=0o750)
5152
except OSError as error:
52-
logger.fatal('failed to create {0}: {1}'.format(path, error))
53+
logger.critical('failed to create {0}: {1}'.format(path, error))
5354
raise CouldNotCreateDbDir()
5455

5556

@@ -60,19 +61,19 @@ class CalendarCollection(object):
6061

6162
def __init__(self,
6263
calendars=None,
63-
hmethod='fg',
64-
default_color='',
65-
multiple='',
66-
color='',
67-
highlight_event_days=0,
68-
locale=None,
69-
dbpath=None,
70-
):
64+
hmethod: str='fg',
65+
default_color: str='',
66+
multiple: str='',
67+
color: str='',
68+
highlight_event_days: bool=False,
69+
locale: Optional[dict]=None,
70+
dbpath: Optional[str]=None,
71+
) -> None:
7172
assert dbpath is not None
7273
assert calendars is not None
7374
self._calendars = calendars
74-
self._default_calendar_name = None
75-
self._storages = dict()
75+
self._default_calendar_name = None # type: Optional[str]
76+
self._storages = dict() # type: Dict[str, Vdir]
7677
for name, calendar in self._calendars.items():
7778
ctype = calendar.get('ctype', 'calendar')
7879
if ctype == 'calendar':
@@ -94,29 +95,28 @@ def __init__(self,
9495
self.color = color
9596
self.highlight_event_days = highlight_event_days
9697
self._locale = locale
97-
self._backend = backend.SQLiteDb(
98-
calendars=self.names, db_path=dbpath, locale=self._locale)
99-
self._last_ctags = dict()
98+
self._backend = backend.SQLiteDb(self.names, dbpath, self._locale)
99+
self._last_ctags = dict() # type: Dict[str, str]
100100
self.update_db()
101101

102102
@property
103-
def writable_names(self):
103+
def writable_names(self) -> List[str]:
104104
return [c for c in self._calendars if not self._calendars[c].get('readonly', False)]
105105

106106
@property
107-
def calendars(self):
107+
def calendars(self) -> Iterable[str]:
108108
return self._calendars.values()
109109

110110
@property
111-
def names(self):
111+
def names(self) -> Iterable[str]:
112112
return self._calendars.keys()
113113

114114
@property
115-
def default_calendar_name(self):
115+
def default_calendar_name(self) -> str:
116116
return self._default_calendar_name
117117

118118
@default_calendar_name.setter
119-
def default_calendar_name(self, default):
119+
def default_calendar_name(self, default: str):
120120
if default is None:
121121
self._default_calendar_name = default
122122
elif default not in self.names:
@@ -130,38 +130,37 @@ def default_calendar_name(self, default):
130130
raise ValueError(
131131
'Calendar "{0}" is read-only and cannot be used as default'.format(default))
132132

133-
def _local_ctag(self, calendar):
133+
def _local_ctag(self, calendar: str) -> str:
134134
return get_etag_from_file(self._calendars[calendar]['path'])
135135

136-
def _cover_event(self, event):
137-
event.color = self._calendars[event.calendar]['color']
138-
event.readonly = self._calendars[event.calendar]['readonly']
139-
event.unicode_symbols = self._locale['unicode_symbols']
140-
return event
141-
142-
def get_floating(self, start, end, minimal=False):
143-
events = self._backend.get_floating(start, end, minimal)
144-
return (self._cover_event(event) for event in events)
145-
146-
def get_localized(self, start, end, minimal=False):
147-
events = self._backend.get_localized(start, end, minimal)
148-
return (self._cover_event(event) for event in events)
136+
def get_floating(self, start: dt.datetime, end: dt.datetime) -> Iterable[Event]:
137+
for args in self._backend.get_floating(start, end):
138+
yield self._construct_event(*args)
149139

150-
def get_events_on(self, day, minimal=False):
151-
"""return all events on `day`
140+
def get_localized(self, start: dt.datetime, end: dt.datetime) -> Iterable[Event]:
141+
for args in self._backend.get_localized(start, end):
142+
yield self._construct_event(*args)
152143

153-
:param day: datetime.date
154-
:rtype: list()
155-
"""
144+
def get_events_on(self, day: dt.date) -> Iterable[Event]:
145+
"""return all events on `day`"""
156146
start = dt.datetime.combine(day, dt.time.min)
157147
end = dt.datetime.combine(day, dt.time.max)
158-
floating_events = self.get_floating(start, end, minimal)
148+
floating_events = self.get_floating(start, end)
159149
localize = self._locale['local_timezone'].localize
160-
localized_events = self.get_localized(localize(start), localize(end), minimal)
161-
150+
localized_events = self.get_localized(localize(start), localize(end))
162151
return itertools.chain(floating_events, localized_events)
163152

164-
def update(self, event):
153+
def get_calendars_on(self, day: dt.date) -> List[str]:
154+
start = dt.datetime.combine(day, dt.time.min)
155+
end = dt.datetime.combine(day, dt.time.max)
156+
localize = self._locale['local_timezone'].localize
157+
calendars = itertools.chain(
158+
self._backend.get_floating_calendars(start, end),
159+
self._backend.get_localized_calendars(localize(start), localize(end)),
160+
)
161+
return list(calendars)
162+
163+
def update(self, event: Event):
165164
"""update `event` in vdir and db"""
166165
assert event.etag
167166
if self._calendars[event.calendar]['readonly']:
@@ -171,7 +170,7 @@ def update(self, event):
171170
self._backend.update(event.raw, event.href, event.etag, calendar=event.calendar)
172171
self._backend.set_ctag(self._local_ctag(event.calendar), calendar=event.calendar)
173172

174-
def force_update(self, event, collection=None):
173+
def force_update(self, event: Event, collection: Optional[str]=None):
175174
"""update `event` even if an event with the same uid/href already exists"""
176175
calendar = collection if collection is not None else event.calendar
177176
if self._calendars[calendar]['readonly']:
@@ -187,7 +186,7 @@ def force_update(self, event, collection=None):
187186
self._backend.update(event.raw, href, etag, calendar=calendar)
188187
self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar)
189188

190-
def new(self, event, collection=None):
189+
def new(self, event: Event, collection: Optional[str]=None):
191190
"""save a new event to the vdir and the database
192191
193192
param event: the event that should be updated, will get a new href and
@@ -210,22 +209,48 @@ def new(self, event, collection=None):
210209
self._backend.update(event.raw, event.href, event.etag, calendar=calendar)
211210
self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar)
212211

213-
def delete(self, href, etag, calendar):
212+
def delete(self, href: str, etag: str, calendar: str):
214213
if self._calendars[calendar]['readonly']:
215214
raise ReadOnlyCalendarError()
216215
self._storages[calendar].delete(href, etag)
217216
self._backend.delete(href, calendar=calendar)
218217

219-
def get_event(self, href, calendar):
220-
return self._cover_event(self._backend.get(href, calendar=calendar))
218+
def get_event(self, href: str, calendar: str) -> Event:
219+
"""get an event by its href from the datatbase"""
220+
return self._construct_event(
221+
self._backend.get(href, calendar), href=href, calendar=calendar,
222+
)
223+
224+
def _construct_event(self,
225+
item: str,
226+
href: str,
227+
start: dt.datetime = None,
228+
end: dt.datetime = None,
229+
ref: str='PROTO',
230+
etag: str=None,
231+
calendar: str=None,
232+
) -> Event:
233+
event = Event.fromString(
234+
item,
235+
locale=self._locale,
236+
href=href,
237+
calendar=calendar,
238+
etag=etag,
239+
start=start,
240+
end=end,
241+
ref=ref,
242+
color=self._calendars[calendar]['color'],
243+
readonly=self._calendars[calendar]['readonly'],
244+
)
245+
return event
221246

222-
def change_collection(self, event, new_collection):
247+
def change_collection(self, event: Event, new_collection: str):
223248
href, etag, calendar = event.href, event.etag, event.calendar
224249
event.etag = None
225250
self.new(event, new_collection)
226251
self.delete(href, etag, calendar=calendar)
227252

228-
def new_event(self, ical, collection):
253+
def new_event(self, ical: str, collection: str):
229254
"""creates and returns (but does not insert) new event from ical
230255
string"""
231256
calendar = collection or self.writable_names[0]
@@ -240,7 +265,7 @@ def update_db(self):
240265
if self._needs_update(calendar, remember=True):
241266
self._db_update(calendar)
242267

243-
def needs_update(self):
268+
def needs_update(self) -> bool:
244269
"""Check if you need to call update_db.
245270
246271
This could either be the case because the vdirs were changed externally,
@@ -266,14 +291,14 @@ def needs_update(self):
266291
return True
267292
return False
268293

269-
def _needs_update(self, calendar, remember=False):
294+
def _needs_update(self, calendar: str, remember: bool=False) -> bool:
270295
"""checks if the db for the given calendar needs an update"""
271296
local_ctag = self._local_ctag(calendar)
272297
if remember:
273298
self._last_ctags[calendar] = local_ctag
274299
return local_ctag != self._backend.get_ctag(calendar)
275300

276-
def _db_update(self, calendar):
301+
def _db_update(self, calendar: str):
277302
"""implements the actual db update on a per calendar base"""
278303
local_ctag = self._local_ctag(calendar)
279304
db_hrefs = set(href for href, etag in self._backend.list(calendar))
@@ -291,7 +316,7 @@ def _db_update(self, calendar):
291316
self._backend.set_ctag(local_ctag, calendar=calendar)
292317
self._last_ctags[calendar] = local_ctag
293318

294-
def _update_vevent(self, href, calendar):
319+
def _update_vevent(self, href: str, calendar: str) -> bool:
295320
"""should only be called during db_update, only updates the db,
296321
does not check for readonly"""
297322
event, etag = self._storages[calendar].get(href)
@@ -301,7 +326,6 @@ def _update_vevent(self, href, calendar):
301326
else:
302327
update = self._backend.update
303328
update(event.raw, href=href, etag=etag, calendar=calendar)
304-
305329
return True
306330
except Exception as e:
307331
if not isinstance(e, (UpdateFailed, UnsupportedFeatureError)):
@@ -311,24 +335,23 @@ def _update_vevent(self, href, calendar):
311335
'This event will not be available in khal.'.format(calendar, href, str(e)))
312336
return False
313337

314-
def search(self, search_string):
338+
def search(self, search_string: str) -> Iterable[Event]:
315339
"""search for the db for events matching `search_string`"""
316-
return (self._cover_event(event) for event in self._backend.search(search_string))
340+
return (self._construct_event(*args) for args in self._backend.search(search_string))
317341

318-
def get_day_styles(self, day, focus):
319-
devents = list(self.get_events_on(day, minimal=True))
320-
if len(devents) == 0:
342+
def get_day_styles(self, day: dt.date, focus: bool) -> Union[str, Tuple[str, str]]:
343+
calendars = self.get_calendars_on(day)
344+
if len(calendars) == 0:
321345
return None
322346
if self.color != '':
323347
return 'highlight_days_color'
324-
dcalendars = list(set(map(lambda event: event.calendar, devents)))
325-
if len(dcalendars) == 1:
326-
return 'calendar ' + dcalendars[0]
348+
if len(calendars) == 1:
349+
return 'calendar ' + calendars[0]
327350
if self.multiple != '':
328351
return 'highlight_days_multiple'
329-
return ('calendar ' + dcalendars[0], 'calendar ' + dcalendars[1])
352+
return ('calendar ' + calendars[0], 'calendar ' + calendars[1])
330353

331-
def get_styles(self, date, focus):
354+
def get_styles(self, date: dt.date, focus: bool) -> Union[str, None, Tuple[str, str]]:
332355
if focus:
333356
if date == date.today():
334357
return 'today focus'

0 commit comments

Comments
 (0)