3838logger = logging .getLogger ('khal' )
3939
4040
41- def timefstr (dtime_list : List [str ], timeformat : str ) -> dt .datetime :
41+ def timefstr (dtime_list : List [str ], timeformat : str , timezone : dt . tzinfo ) -> dt .datetime :
4242 """converts the first item of a list (a time as a string) to a datetimeobject
4343
4444 where the date is today and the time is given by a string
@@ -48,8 +48,8 @@ def timefstr(dtime_list: List[str], timeformat: str) -> dt.datetime:
4848 raise ValueError ()
4949 datetime_start = dt .datetime .strptime (dtime_list [0 ], timeformat )
5050 time_start = dt .time (* datetime_start .timetuple ()[3 :5 ])
51- day_start = dt .date . today ()
52- dtstart = dt .datetime .combine (day_start , time_start )
51+ today = dt .datetime . now ( timezone ). date ()
52+ dtstart = dt .datetime .combine (today , time_start ). replace ( tzinfo = timezone )
5353 dtime_list .pop (0 )
5454 return dtstart
5555
@@ -60,10 +60,11 @@ def datetimefstr(
6060 default_day : Optional [dt .date ]= None ,
6161 infer_year : bool = True ,
6262 in_future : bool = True ,
63+ timezone : dt .tzinfo = pytz .UTC ,
6364) -> dt .datetime :
6465 """converts a datetime (as one or several string elements of a list) to
6566 a datetimeobject, if infer_year is True, use the `default_day`'s year as
66- the year of the return datetimeobject,
67+ the year of the returned datetimeobject,
6768
6869 removes "used" elements of list
6970
@@ -73,7 +74,7 @@ def datetimefstr(
7374 dateformat = '%d.%m. %H:%M'
7475 """
7576 # if now() is called as default param, mocking with freezegun won't work
76- now = dt .datetime .now ()
77+ now = dt .datetime .now (timezone )
7778 if default_day is None :
7879 default_day = now .date ()
7980 parts = dateformat .count (' ' ) + 1
@@ -92,13 +93,17 @@ def datetimefstr(
9293
9394 if infer_year :
9495 dtstart = dt .datetime (* (default_day .timetuple ()[:1 ] + dtstart_struct [1 :5 ]))
96+ dtstart = dtstart .astimezone (timezone )
9597 if in_future and dtstart < now :
9698 dtstart = dtstart .replace (year = dtstart .year + 1 )
9799 if dtstart .date () < default_day :
98100 dtstart = dtstart .replace (year = default_day .year + 1 )
101+ assert dtstart .tzinfo is not None
99102 return dtstart
100103 else :
101- return dt .datetime (* dtstart_struct [:5 ])
104+ rdt = dt .datetime (* dtstart_struct [:5 ]).replace (tzinfo = timezone )
105+ assert rdt .tzinfo is not None
106+ return rdt
102107
103108
104109def weekdaypstr (dayname : str ) -> int :
@@ -125,12 +130,12 @@ def weekdaypstr(dayname: str) -> int:
125130 raise ValueError ('invalid weekday name `%s`' % dayname )
126131
127132
128- def construct_daynames (date_ : dt .date , local_timezone ) -> str :
133+ def construct_daynames (date_ : dt .date , timezone : dt . tzinfo ) -> str :
129134 """converts datetime.date into a string description
130135
131136 either `Today`, `Tomorrow` or name of weekday.
132137 """
133- today = dt .datetime .now (local_timezone ).date ()
138+ today = dt .datetime .now (timezone ).date ()
134139 if date_ == today :
135140 return 'Today'
136141 elif date_ == today + dt .timedelta (days = 1 ):
@@ -139,13 +144,14 @@ def construct_daynames(date_: dt.date, local_timezone) -> str:
139144 return date_ .strftime ('%A' )
140145
141146
142- def calc_day (dayname : str ) -> dt .datetime :
147+ def calc_day (dayname : str , timezone : dt . tzinfo ) -> dt .datetime :
143148 """converts a relative date's description to a datetime object
144149
145150 :param dayname: relative day name (like 'today' or 'monday')
151+ :param timezone: timezone to use for the calculation
146152 :returns: date
147153 """
148- today = dt .datetime .combine (dt .date .today (), dt .time .min )
154+ today = dt .datetime .combine (dt .date .today (), dt .time .min ). replace ( tzinfo = timezone )
149155 dayname = dayname .lower ()
150156 if dayname == 'today' :
151157 return today
@@ -161,7 +167,7 @@ def calc_day(dayname: str) -> dt.datetime:
161167 return day
162168
163169
164- def datefstr_weekday (dtime_list : List [str ], timeformat : str , infer_year : bool ) -> dt .datetime :
170+ def datefstr_weekday (dtime_list : List [str ], timeformat : str , infer_year : bool , timezone : dt . tzinfo ) -> dt .datetime :
165171 """interprets first element of a list as a relative date and removes that
166172 element
167173
@@ -172,22 +178,22 @@ def datefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool) -
172178 """
173179 if len (dtime_list ) == 0 :
174180 raise ValueError ()
175- day = calc_day (dtime_list [0 ])
181+ day = calc_day (dtime_list [0 ], timezone = timezone )
176182 dtime_list .pop (0 )
177183 return day
178184
179185
180- def datetimefstr_weekday (dtime_list : List [str ], timeformat : str , infer_year : bool ) -> dt .datetime :
186+ def datetimefstr_weekday (dtime_list : List [str ], timeformat : str , infer_year : bool , timezone : dt . tzinfo ) -> dt .datetime :
181187 """
182188 :param infer_year: only here for compat reasons (having the same function signature)
183189 """
184190 if len (dtime_list ) == 0 :
185191 raise ValueError ()
186- day = calc_day (dtime_list [0 ])
187- this_time = timefstr (dtime_list [1 :], timeformat )
192+ day = calc_day (dtime_list [0 ], timezone = timezone )
193+ this_time = timefstr (dtime_list [1 :], timeformat , timezone )
188194 dtime_list .pop (0 )
189- dtime_list .pop (0 ) # we need to pop twice as timefstr gets a copy
190- dtime = dt .datetime .combine (day , this_time .time ())
195+ dtime_list .pop (0 ) # we need to pop twice as timefstr gets a copy and does't remove a used element
196+ dtime = dt .datetime .combine (day , this_time .time ()). replace ( tzinfo = timezone )
191197 return dtime
192198
193199
@@ -200,27 +206,33 @@ def guessdatetimefstr(
200206 """
201207 :param in_future: if set, shortdate(time) events will be set in the future
202208 """
209+ orig = list (dtime_list ) # TODO remove this line -- only for debugging
203210 # if now() is called as default param, mocking with freezegun won't work
204- day = default_day or dt .datetime .now ().date ()
211+ day = default_day or dt .datetime .now (locale [ 'local_timezone' ] ).date ()
205212 # TODO rename in guessdatetimefstrLIST or something saner altogether
206213
207- def timefstr_day (dtime_list : List [str ], timeformat : str , infer_year : bool ) -> dt .datetime :
214+ def timefstr_day (dtime_list : List [str ], timeformat : str , infer_year : bool , timezone : dt . tzinfo ) -> dt .datetime :
208215 if locale ['timeformat' ] == '%H:%M' and dtime_list [0 ] == '24:00' :
209- a_date = dt .datetime .combine (day , dt .time (0 ))
216+ a_date = dt .datetime .combine (day , dt .time (0 )). replace ( tzinfo = timezone )
210217 dtime_list .pop (0 )
211218 else :
212- a_date = timefstr (dtime_list , timeformat )
213- a_date = dt .datetime (* (day .timetuple ()[:3 ] + a_date .timetuple ()[3 :5 ]))
219+ a_date = timefstr (dtime_list , timeformat , timezone = timezone )
220+ a_date = dt .datetime (* (day .timetuple ()[:3 ] + a_date .timetuple ()[3 :5 ])).replace (tzinfo = timezone )
221+ assert a_date .tzinfo is not None
214222 return a_date
215223
216- def datetimefwords (dtime_list : List [str ], _ : str , infer_year : bool ) -> dt .datetime :
224+ def datetimefwords (dtime_list : List [str ], _ : str , infer_year : bool , timezone : dt .tzinfo ) -> dt .datetime :
225+ """converts words to datetimes
226+
227+ for now, this only knows "now"
228+ """
217229 if len (dtime_list ) > 0 and dtime_list [0 ].lower () == 'now' :
218230 dtime_list .pop (0 )
219- return dt .datetime .now ()
231+ return dt .datetime .now (timezone )
220232 raise ValueError
221233
222- def datefstr_year (dtime_list : List [str ], dtformat : str , infer_year : bool ) -> dt .datetime :
223- return datetimefstr (dtime_list , dtformat , day , infer_year , in_future )
234+ def datefstr_year (dtime_list : List [str ], dtformat : str , infer_year : bool , timezone : dt . tzinfo ) -> dt .datetime :
235+ return datetimefstr (dtime_list , dtformat , day , infer_year , in_future , timezone )
224236
225237 dtstart = None
226238 fun : Callable [[List [str ], str , bool ], dt .datetime ]
@@ -241,10 +253,12 @@ def datefstr_year(dtime_list: List[str], dtformat: str, infer_year: bool) -> dt.
241253 if infer_year and '97' in dt .datetime (1997 , 10 , 11 ).strftime (dtformat ):
242254 infer_year = False
243255 try :
244- dtstart = fun (dtime_list , dtformat , infer_year = infer_year )
256+ timezone = locale ['local_timezone' ]
257+ dtstart = fun (dtime_list , dtformat , infer_year = infer_year , timezone = timezone )
245258 except (ValueError , DateTimeParseError ):
246259 pass
247260 else :
261+ assert dtstart .tzinfo is not None
248262 return dtstart , all_day
249263 raise DateTimeParseError (
250264 f"Could not parse \" { dtime_list } \" .\n Please check your configuration "
@@ -341,9 +355,13 @@ def guessrangefstr(daterange: Union[str, List[str]],
341355 range_list = daterange .split (' ' )
342356 assert isinstance (range_list , list )
343357
358+ orig = list (range_list ) # TODO remove this line -- only for debugging
359+
344360 if range_list == ['week' ]:
345361 today_weekday = dt .datetime .today ().weekday ()
346- startdt = dt .datetime .today () - dt .timedelta (days = (today_weekday - locale ['firstweekday' ]))
362+ today = dt .datetime .now (locale ['local_timezone' ]).date ()
363+ today = dt .datetime .combine (today , dt .time .min ).replace (tzinfo = locale ['local_timezone' ])
364+ startdt = today - dt .timedelta (days = (today_weekday - locale ['firstweekday' ]))
347365 enddt = startdt + dt .timedelta (days = 8 )
348366 return startdt , enddt , True
349367
@@ -365,7 +383,7 @@ def guessrangefstr(daterange: Union[str, List[str]],
365383 else :
366384 end = start + default_timedelta_datetime
367385 elif endstr .lower () == 'eod' :
368- end = dt .datetime .combine (start .date (), dt .time .max )
386+ end = dt .datetime .combine (start .date (), dt .time .max ). replace ( tzinfo = locale [ 'local_timezone' ])
369387 elif endstr .lower () == 'week' :
370388 start -= dt .timedelta (days = (start .weekday () - locale ['firstweekday' ]))
371389 end = start + dt .timedelta (days = 8 )
@@ -397,17 +415,22 @@ def guessrangefstr(daterange: Union[str, List[str]],
397415 if adjust_reasonably :
398416 if allday :
399417 # test if end's year is this year, but start's year is not
400- today = dt .datetime .today ()
418+ today = dt .datetime .now ( locale [ 'default_timezone' ]). date ()
401419 if end .year == today .year and start .year != today .year :
402420 end = dt .datetime (start .year , * end .timetuple ()[1 :6 ])
403421
404422 if end < start :
405423 end = dt .datetime (end .year + 1 , * end .timetuple ()[1 :6 ])
406424
407425 if end < start :
408- end = dt .datetime (* start .timetuple ()[0 :3 ] + end .timetuple ()[3 :5 ])
426+ # if end is before start date we are using the year, month and day of start
427+ # and the time of end to create a new end date
428+ end = dt .datetime (* start .timetuple ()[0 :3 ] + end .timetuple ()[3 :5 ]).replace (tzinfo = locale ['local_timezone' ])
409429 if end < start :
430+ # if end is still before start date we are adding a day
410431 end = end + dt .timedelta (days = 1 )
432+ assert start .tzinfo is not None
433+ assert end .tzinfo is not None
411434 return start , end , allday
412435 except (ValueError , DateTimeParseError ):
413436 pass
@@ -423,18 +446,13 @@ def guessrangefstr(daterange: Union[str, List[str]],
423446def rrulefstr (repeat : str ,
424447 until : str ,
425448 locale : LocaleConfiguration ,
426- timezone : Optional [dt .tzinfo ],
427449 ) -> RRuleMapType :
428450 if repeat in ["daily" , "weekly" , "monthly" , "yearly" ]:
429451 rrule_settings : RRuleMapType = {'freq' : repeat }
430452 if until :
431453 until_dt , _ = guessdatetimefstr (until .split (' ' ), locale )
432- if timezone :
433- rrule_settings ['until' ] = until_dt .\
434- replace (tzinfo = timezone ).\
435- astimezone (pytz .UTC )
436- else :
437- rrule_settings ['until' ] = until_dt
454+ assert until_dt .tzinfo is not None
455+ rrule_settings ['until' ] = until_dt
438456 return rrule_settings
439457 else :
440458 logger .fatal ("Invalid value for the repeat option. \
0 commit comments