apscheduler: Standard tzinfo is not enough for apscheduler

Expected Behavior

apscheduler should work with next_run_time specified with standard tzinfo.

Current Behavior

apscheduler crashes.

Steps to Reproduce

from datetime import datetime, timedelta
from dateutil.tz import tzutc

next_run_time = datetime.now(tzutc()) + timedelta(seconds=30)
scheduler.add_job(
    lambda: print("Hello"), trigger=IntervalTrigger(minutes=1), next_run_time=next_run_time
)

Context (Environment)

apscheduler works only with pytz objects, the problematic call is there : https://github.com/agronholm/apscheduler/blob/18b50d9ee9ff14e816b557e34d7d3abc861d57e5/apscheduler/triggers/interval.py#L66

If next_fire_time is not stamped with a pytz.tzinfo.BaseTzInfo, it crashes.

self.timezone is a pytz object, normalize() is a pytz function which expects a pytz.tzinfo.BaseTzInfo as argument (see https://github.com/stub42/pytz/blob/master/src/pytz/tzinfo.py#L66) with an internal (and absent from the standard tzinfo) _utcoffset there : https://github.com/stub42/pytz/blob/master/src/pytz/tzinfo.py#L252.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 25 (13 by maintainers)

Most upvoted comments

v4.0.0a1 accepts a wider range of tzinfo objects. If you still have trouble, open a new issue against v4.0.0a1.

It already does, in master.

This has been open quite a long time now. I realise v4.0 refactor may still be ongoing.

Yes, it is, and progress is sporadic since I am also working on several other projects. That said, the triggers part is more or less done, and it supports a far wider variety of tzinfo objects. Date-time arithmetic is done by converting the starting datetime to a timestamp, adding the desired number of seconds and then converting it back. That is the only way to get the semantics we want.

I’m thinking of just bypassing conversion by storing timezone database names and avoiding conversion to python’s standard dateutil timezone altogether.

Dateutil is a third party library, just as pytz is. Neither is considered any more “standard” than the other.

Is there a way to convert from standard python timezones to pytz ones while keeping DST information?

I tried playing around with dateutil timezones. It should be noted that, unlike pytz which comes with its own copy of the Olsen database, dateutil relies on system-provided timezone data and thus will not work the same way on Windows.

The following snippet should go some way towards demonstrating why I stick with pytz and will not bother with dateutil:

>>> from datetime import datetime
>>> from dateutil.tz import gettz
>>> tz = gettz('Europe/Helsinki')
>>> tz.tzname(datetime.now())
'EEST'
>>> tz2.tzname(datetime(2020, 1, 1))
'EET'

As you can see, the official way to get the timezone name is not helpful.

>>> dt1 = datetime(2020, 3, 29, 2, 59, tzinfo=tz)
>>> dt1.isoformat()
'2020-03-29T02:59:00+02:00'

We get the correct UTC offset here. Let’s try moving it 2 minutes forward which, taking the DST shift into account, should arrive at 2020-03-29T04:01:00+03:00:

>>> dt2 = dt1 + timedelta(minutes=2)
>>> dt2
datetime.datetime(2020, 3, 29, 3, 1, tzinfo=tzfile('/usr/share/zoneinfo/Europe/Helsinki'))

Nonexistent time, but doesn’t matter if we normalize, let’s try to do that:

>>> datetime.fromtimestamp(dt2.timestamp(), tz).isoformat()
'2020-03-29T02:01:00+02:00'

Uh, what? How on Earth did it go backwards when time was added to the timestamp? Let’s try this another way – just add the minutes to the timestamp and make a new datetime from that:

>>> datetime.fromtimestamp(dt1.timestamp() + 2*60, tz).isoformat()
'2020-03-29T04:01:00+03:00'

Now we get the correct answer.

The biggest problem, however, is serialization. There is no way to reliably recreate instances of the datetime.tzinfo class without knowing where they came from unless pickle is used. Some library specific logic could be used, like checking what file a dateutil timezone points to. Additionally, local-bound timezones are problematic in that if the project is moved to another server, it will start using that server’s local timezone, whatever it is. This may be what you want, or not. Either way this should be explicit.

Even relying on pickle might have been borderline acceptable on APScheduler 3.x, but 4.0+ will offer a variety of serializers so this approach will not work.

If you want a more comprehensive explanation of the issue, have a look at the pytz documentation.

To precise my comment (and issue) the fact that when giving explicit timezone to apscheduler you have to use pytz stuff is not a problem (and I apologize for not reading the docs about it - tbh I don’t use this feature).

My issue is that my standard datetimes with standard tzinfo does not work when given to next_run_time.