summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Grönholm <alex.gronholm@nextday.fi>2020-09-28 09:19:21 +0300
committerAlex Grönholm <alex.gronholm@nextday.fi>2020-09-28 09:35:20 +0300
commitefe16602580d47ef5cb9787f977a65a5791ea024 (patch)
tree3f88e9e778eace056141a86c97365aeaa119071b
parent3205a400a5f0ee4677cef082f6c50570c7004edf (diff)
downloadapscheduler-efe16602580d47ef5cb9787f977a65a5791ea024.tar.gz
Converted triggers to use zoneinfo instead of pytz timezones
-rw-r--r--apscheduler/datastores/memory.py9
-rw-r--r--apscheduler/marshalling.py123
-rw-r--r--apscheduler/schedulers/async_.py38
-rw-r--r--apscheduler/serializers/cbor.py2
-rw-r--r--apscheduler/serializers/json.py2
-rw-r--r--apscheduler/triggers/calendarinterval.py55
-rw-r--r--apscheduler/triggers/combining.py2
-rw-r--r--apscheduler/triggers/cron/__init__.py42
-rw-r--r--apscheduler/triggers/date.py15
-rw-r--r--apscheduler/triggers/interval.py38
-rw-r--r--apscheduler/util.py227
-rw-r--r--apscheduler/validators.py41
-rw-r--r--setup.cfg1
-rw-r--r--tests/conftest.py9
-rw-r--r--tests/test_marshalling.py88
-rw-r--r--tests/test_util.py194
-rw-r--r--tests/triggers/test_calendarinterval.py8
-rw-r--r--tests/triggers/test_combining.py26
-rw-r--r--tests/triggers/test_cron.py124
-rw-r--r--tests/triggers/test_date.py4
-rw-r--r--tests/triggers/test_interval.py16
21 files changed, 442 insertions, 622 deletions
diff --git a/apscheduler/datastores/memory.py b/apscheduler/datastores/memory.py
index 8c5d7eb..3ad76c4 100644
--- a/apscheduler/datastores/memory.py
+++ b/apscheduler/datastores/memory.py
@@ -1,12 +1,12 @@
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from datetime import MAXYEAR, datetime, timezone
-from typing import Any, AsyncContextManager, Callable, Dict, Iterable, List, Mapping, Optional, Set
+from typing import Any, AsyncGenerator, Callable, Dict, Iterable, List, Mapping, Optional, Set
from apscheduler.abc import DataStore, Event, EventHub, Schedule
from apscheduler.eventhubs.local import LocalEventHub
-from apscheduler.events import SchedulesAdded, SchedulesRemoved, SchedulesUpdated
-from sortedcontainers import SortedSet
+from apscheduler.events import DataStoreEvent, SchedulesAdded, SchedulesRemoved, SchedulesUpdated
+from sortedcontainers import SortedSet # type: ignore
max_datetime = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc)
@@ -36,6 +36,7 @@ class MemoryScheduleStore(DataStore):
self._schedules_by_id[schedule.id] = schedule
self._schedules.add(schedule)
+ event: DataStoreEvent
if added_schedule_ids:
earliest_fire_time = min(
(schedule.next_fire_time for schedule in schedules
@@ -99,7 +100,7 @@ class MemoryScheduleStore(DataStore):
@asynccontextmanager
async def acquire_due_schedules(
self, scheduler_id: str,
- max_scheduled_time: datetime) -> AsyncContextManager[List[Schedule]]:
+ max_scheduled_time: datetime) -> AsyncGenerator[List[Schedule], None]:
pending: List[Schedule] = []
for schedule in self._schedules:
if not schedule.next_fire_time or schedule.next_fire_time > max_scheduled_time:
diff --git a/apscheduler/marshalling.py b/apscheduler/marshalling.py
new file mode 100644
index 0000000..4c5e660
--- /dev/null
+++ b/apscheduler/marshalling.py
@@ -0,0 +1,123 @@
+import sys
+from datetime import date, datetime, tzinfo
+from functools import partial
+from typing import Any, Callable, Tuple, overload
+
+from .exceptions import DeserializationError, SerializationError
+
+if sys.version_info >= (3, 9):
+ from zoneinfo import ZoneInfo
+else:
+ from backports.zoneinfo import ZoneInfo
+
+
+def marshal_object(obj) -> Tuple[str, Any]:
+ return f'{obj.__class__.__module__}:{obj.__class__.__qualname__}', obj.__getstate__()
+
+
+def unmarshal_object(ref: str, state):
+ cls = callable_from_ref(ref)
+ instance = cls.__new__(cls)
+ instance.__setstate__(state)
+ return instance
+
+
+@overload
+def marshal_date(value: None) -> None:
+ ...
+
+
+@overload
+def marshal_date(value: date) -> str:
+ ...
+
+
+def marshal_date(value):
+ return value.isoformat() if value is not None else None
+
+
+@overload
+def unmarshal_date(value: None) -> None:
+ ...
+
+
+@overload
+def unmarshal_date(value: str) -> date:
+ ...
+
+
+def unmarshal_date(value):
+ if value is None:
+ return None
+ elif len(value) == 10:
+ return date.fromisoformat(value)
+ else:
+ return datetime.fromisoformat(value)
+
+
+def marshal_timezone(value: tzinfo) -> str:
+ if isinstance(value, ZoneInfo):
+ return value.key
+ elif hasattr(value, 'zone'): # pytz timezones
+ return value.zone
+
+ raise SerializationError(
+ f'Unserializable time zone: {value!r}\n'
+ f'Only time zones from the zoneinfo or pytz modules can be serialized.')
+
+
+def unmarshal_timezone(value: str) -> ZoneInfo:
+ return ZoneInfo(value)
+
+
+def callable_to_ref(func: Callable) -> str:
+ """
+ Return a reference to the given callable.
+
+ :raises SerializationError: if the given object is not callable, is a partial(), lambda or
+ local function or does not have the ``__module__`` and ``__qualname__`` attributes
+
+ """
+ if isinstance(func, partial):
+ raise SerializationError('Cannot create a reference to a partial()')
+
+ if not hasattr(func, '__module__'):
+ raise SerializationError('Callable has no __module__ attribute')
+ if not hasattr(func, '__qualname__'):
+ raise SerializationError('Callable has no __qualname__ attribute')
+ if '<lambda>' in func.__qualname__:
+ raise SerializationError('Cannot create a reference to a lambda')
+ if '<locals>' in func.__qualname__:
+ raise SerializationError('Cannot create a reference to a nested function')
+
+ return f'{func.__module__}:{func.__qualname__}'
+
+
+def callable_from_ref(ref: str) -> Callable:
+ """
+ Return the callable pointed to by ``ref``.
+
+ :raises DeserializationError: if the reference could not be resolved or the looked up object is
+ not callable
+
+ """
+ if ':' not in ref:
+ raise ValueError(f'Invalid reference: {ref}')
+
+ modulename, rest = ref.split(':', 1)
+ try:
+ obj = __import__(modulename, fromlist=[rest])
+ except ImportError:
+ raise LookupError(f'Error resolving reference {ref!r}: could not import module')
+
+ try:
+ for name in rest.split('.'):
+ obj = getattr(obj, name)
+ except Exception:
+ raise DeserializationError(f'Error resolving reference {ref!r}: error looking up object')
+
+ if not callable(obj):
+ raise DeserializationError(f'{ref!r} points to an object of type '
+ f'{obj.__class__.__qualname__} which is not callable')
+
+ return obj
diff --git a/apscheduler/schedulers/async_.py b/apscheduler/schedulers/async_.py
index 18b6a77..d6f8faf 100644
--- a/apscheduler/schedulers/async_.py
+++ b/apscheduler/schedulers/async_.py
@@ -1,7 +1,6 @@
import logging
import os
import platform
-import sys
import threading
from contextlib import AsyncExitStack
from dataclasses import dataclass, field
@@ -13,20 +12,15 @@ from uuid import uuid4
import tzlocal
from anyio import create_event, create_task_group, move_on_after
from anyio.abc import Event
-from pytz.tzinfo import DstTzInfo, StaticTzInfo
from ..abc import DataStore, EventHub, EventSource, Executor, Job, Schedule, Task, Trigger
from ..datastores.memory import MemoryScheduleStore
from ..eventhubs.local import LocalEventHub
from ..events import JobSubmissionFailed, SchedulesAdded, SchedulesUpdated
-from ..util import obj_to_ref
+from ..marshalling import callable_to_ref
+from ..validators import as_timezone
from ..workers.local import LocalExecutor
-if sys.version_info >= (3, 9):
- import zoneinfo
-else:
- from backports import zoneinfo
-
@dataclass
class AsyncScheduler(EventSource):
@@ -43,16 +37,7 @@ class AsyncScheduler(EventSource):
_closed: bool = field(init=False, default=False)
def __post_init__(self):
- if self.timezone.tzname(None) == 'local':
- raise ValueError(
- 'Unable to determine the name of the local timezone -- you must explicitly '
- 'specify the name of the local timezone. Please refrain from using timezones '
- 'like EST to prevent problems with daylight saving time. Instead, use a '
- 'locale based timezone name (such as Europe/Helsinki).')
-
- # Convert pytz timezones to zoneinfo timezones
- if isinstance(self.timezone, (StaticTzInfo, DstTzInfo)):
- self.timezone = zoneinfo.ZoneInfo(self.timezone.zone)
+ self.timezone = as_timezone(self.timezone)
async def __aenter__(self):
await self._async_stack.__aenter__()
@@ -65,7 +50,7 @@ class AsyncScheduler(EventSource):
await self._async_stack.__aexit__(exc_type, exc_val, exc_tb)
def _get_taskdef(self, func_or_id: Union[str, Callable]) -> Task:
- task_id = func_or_id if isinstance(func_or_id, str) else obj_to_ref(func_or_id)
+ task_id = func_or_id if isinstance(func_or_id, str) else callable_to_ref(func_or_id)
taskdef = self._tasks.get(task_id)
if not taskdef:
if isinstance(func_or_id, str):
@@ -77,7 +62,7 @@ class AsyncScheduler(EventSource):
def define_task(self, func: Callable, task_id: Optional[str] = None, **kwargs):
if task_id is None:
- task_id = obj_to_ref(func)
+ task_id = callable_to_ref(func)
task = Task(id=task_id, **kwargs)
if self._tasks.setdefault(task_id, task) is not task:
@@ -115,12 +100,13 @@ class AsyncScheduler(EventSource):
if isinstance(event, (SchedulesAdded, SchedulesUpdated)):
# Wake up the scheduler if any schedule has an earlier next fire time than the one
# we're currently waiting for
- if (self._next_fire_time is None
- or self._next_fire_time > event.earliest_next_fire_time):
- self.logger.debug('Job store reported an updated next fire time that requires '
- 'the scheduler to wake up: %s',
- event.earliest_next_fire_time)
- await self.wakeup()
+ if event.earliest_next_fire_time:
+ if (self._next_fire_time is None
+ or self._next_fire_time > event.earliest_next_fire_time):
+ self.logger.debug('Job store reported an updated next fire time that requires '
+ 'the scheduler to wake up: %s',
+ event.earliest_next_fire_time)
+ await self.wakeup()
await self._event_hub.publish(event)
diff --git a/apscheduler/serializers/cbor.py b/apscheduler/serializers/cbor.py
index 6ae9575..9fdf17b 100644
--- a/apscheduler/serializers/cbor.py
+++ b/apscheduler/serializers/cbor.py
@@ -4,7 +4,7 @@ from typing import Any, Dict
from cbor2 import CBOREncodeTypeError, CBORTag, dumps, loads
from ..abc import Serializer
-from ..util import marshal_object, unmarshal_object
+from ..marshalling import marshal_object, unmarshal_object
@dataclass
diff --git a/apscheduler/serializers/json.py b/apscheduler/serializers/json.py
index f433b15..7c2513b 100644
--- a/apscheduler/serializers/json.py
+++ b/apscheduler/serializers/json.py
@@ -3,7 +3,7 @@ from json import dumps, loads
from typing import Any, Dict
from ..abc import Serializer
-from ..util import marshal_object, unmarshal_object
+from ..marshalling import marshal_object, unmarshal_object
@dataclass
diff --git a/apscheduler/triggers/calendarinterval.py b/apscheduler/triggers/calendarinterval.py
index 423ec11..c6d63da 100644
--- a/apscheduler/triggers/calendarinterval.py
+++ b/apscheduler/triggers/calendarinterval.py
@@ -1,11 +1,10 @@
from datetime import date, datetime, time, timedelta, tzinfo
from typing import Optional, Union
-from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
-
from ..abc import Trigger
-from ..validators import (
- as_aware_datetime, as_date, as_ordinal_date, as_timestamp, as_timezone, require_state_version)
+from ..marshalling import marshal_date, marshal_timezone, unmarshal_date, unmarshal_timezone
+from ..util import timezone_repr
+from ..validators import as_date, as_timezone, require_state_version
class CalendarIntervalTrigger(Trigger):
@@ -59,7 +58,7 @@ class CalendarIntervalTrigger(Trigger):
hour: int = 0, minute: int = 0, second: int = 0,
start_date: Union[date, str, None] = None,
end_date: Union[date, str, None] = None,
- timezone: Union[str, tzinfo, None] = None):
+ timezone: Union[str, tzinfo] = 'local'):
self.years = years
self.months = months
self.weeks = weeks
@@ -67,7 +66,7 @@ class CalendarIntervalTrigger(Trigger):
self.timezone = as_timezone(timezone)
self.start_date = as_date(start_date) or datetime.now(self.timezone).date()
self.end_date = as_date(end_date)
- self._time = time(hour, minute, second)
+ self._time = time(hour, minute, second, tzinfo=timezone)
self._last_fire_date: Optional[date] = None
if self.years == self.months == self.weeks == self.days == 0:
@@ -99,36 +98,36 @@ class CalendarIntervalTrigger(Trigger):
if self.end_date and next_date > self.end_date:
return None
- next_time = datetime.combine(next_date, self._time)
- try:
+ # Combine the date with the designated time and normalize the result
+ timestamp = datetime.combine(next_date, self._time).timestamp()
+ next_time = datetime.fromtimestamp(timestamp, self.timezone)
+
+ # Check if the time is off due to normalization and a forward DST shift
+ if next_time.time() != self._time:
+ previous_date = next_time.date()
+ else:
self._last_fire_date = next_date
- return self.timezone.localize(next_time, is_dst=None)
- except AmbiguousTimeError:
- # Return the daylight savings occurrence of the datetime
- return self.timezone.localize(next_time, is_dst=True)
- except NonExistentTimeError:
- # This datetime does not exist (the DST shift jumps over it)
- previous_date = next_date
+ return next_time
def __getstate__(self):
return {
'version': 1,
'interval': [self.years, self.months, self.weeks, self.days],
'time': [self._time.hour, self._time.minute, self._time.second],
- 'start_date': as_ordinal_date(self.start_date),
- 'end_date': as_ordinal_date(self.end_date),
- 'timezone': self.timezone.zone,
- 'last_fire_date': as_timestamp(self._last_fire_date)
+ 'start_date': marshal_date(self.start_date),
+ 'end_date': marshal_date(self.end_date),
+ 'timezone': marshal_timezone(self.timezone),
+ 'last_fire_date': marshal_date(self._last_fire_date)
}
def __setstate__(self, state):
require_state_version(self, state, 1)
self.years, self.months, self.weeks, self.days = state['interval']
- self.start_date = as_date(state['start_date'])
- self.end_date = as_date(state['end_date'])
- self.timezone = as_timezone(state['timezone'])
- self._time = time(*state['time'])
- self._last_fire_date = as_aware_datetime(state['last_fire_date'], self.timezone)
+ self.start_date = unmarshal_date(state['start_date'])
+ self.end_date = unmarshal_date(state['end_date'])
+ self.timezone = unmarshal_timezone(state['timezone'])
+ self._time = time(*state['time'], tzinfo=self.timezone)
+ self._last_fire_date = unmarshal_date(state['last_fire_date'])
def __repr__(self):
fields = []
@@ -138,9 +137,9 @@ class CalendarIntervalTrigger(Trigger):
fields.append(f'{field}={value}')
fields.append(f'time={self._time.isoformat()!r}')
- fields.append(f'start_date={self.start_date.isoformat()!r}')
+ fields.append(f"start_date='{self.start_date}'")
if self.end_date:
- fields.append(f'end_date={self.end_date.isoformat()!r}')
+ fields.append(f"end_date='{self.end_date}'")
- fields.append(f'timezone={self.timezone.tzname(None)!r}')
- return f'CalendarIntervalTrigger({", ".join(fields)})'
+ fields.append(f'timezone={timezone_repr(self.timezone)!r}')
+ return f'{self.__class__.__name__}({", ".join(fields)})'
diff --git a/apscheduler/triggers/combining.py b/apscheduler/triggers/combining.py
index ea78262..04363fd 100644
--- a/apscheduler/triggers/combining.py
+++ b/apscheduler/triggers/combining.py
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union
from ..abc import Trigger
from ..exceptions import MaxIterationsReached
-from ..util import marshal_object, unmarshal_object
+from ..marshalling import marshal_object, unmarshal_object
from ..validators import as_list, as_positive_integer, as_timedelta, require_state_version
diff --git a/apscheduler/triggers/cron/__init__.py b/apscheduler/triggers/cron/__init__.py
index df1e6c5..dc0848e 100644
--- a/apscheduler/triggers/cron/__init__.py
+++ b/apscheduler/triggers/cron/__init__.py
@@ -2,8 +2,9 @@ from datetime import datetime, timedelta, tzinfo
from typing import ClassVar, List, Optional, Sequence, Tuple, Union
from ...abc import Trigger
-from ...util import datetime_ceil
-from ...validators import as_aware_datetime, as_timestamp, as_timezone, require_state_version
+from ...marshalling import marshal_date, marshal_timezone, unmarshal_date, unmarshal_timezone
+from ...util import timezone_repr
+from ...validators import as_aware_datetime, as_timezone, require_state_version
from .fields import (
DEFAULT_VALUES, BaseField, DayOfMonthField, DayOfWeekField, MonthField, WeekField)
@@ -70,7 +71,7 @@ class CronTrigger(Trigger):
self._fields.append(field)
@classmethod
- def from_crontab(cls, expr: str, timezone: Union[str, tzinfo, None] = None) -> 'CronTrigger':
+ def from_crontab(cls, expr: str, timezone: Union[str, tzinfo] = 'local') -> 'CronTrigger':
"""
Create a :class:`~CronTrigger` from a standard crontab expression.
@@ -116,6 +117,7 @@ class CronTrigger(Trigger):
values[field.name] = field.get_min(dateval)
i += 1
else:
+ print('incrementing', field.name)
value = field.get_value(dateval)
maxval = field.get_max(dateval)
if value == maxval:
@@ -126,7 +128,10 @@ class CronTrigger(Trigger):
i += 1
difference = datetime(**values) - dateval.replace(tzinfo=None)
- return self.timezone.normalize(dateval + difference), fieldnum
+ dateval = datetime.fromtimestamp(dateval.timestamp() + difference.total_seconds(),
+ self.timezone)
+ return dateval, fieldnum
+ # return datetime_normalize(dateval + difference), fieldnum
def _set_field_value(self, dateval, fieldnum, new_value):
values = {}
@@ -139,7 +144,7 @@ class CronTrigger(Trigger):
else:
values[field.name] = new_value
- return self.timezone.localize(datetime(**values))
+ return datetime(**values, tzinfo=self.timezone)
def next(self) -> Optional[datetime]:
if self._last_fire_time:
@@ -153,6 +158,7 @@ class CronTrigger(Trigger):
field = self._fields[fieldnum]
curr_value = field.get_value(next_time)
next_value = field.get_next_value(next_time)
+ print(f'{field.name}: current value = {curr_value}, next_value = {next_value}')
if next_value is None:
# No valid value was found
@@ -179,19 +185,19 @@ class CronTrigger(Trigger):
def __getstate__(self):
return {
'version': 1,
- 'timezone': self.timezone.zone,
+ 'timezone': marshal_timezone(self.timezone),
'fields': [str(f) for f in self._fields],
- 'start_time': as_timestamp(self.start_time),
- 'end_time': as_timestamp(self.end_time),
- 'last_fire_time': as_timestamp(self._last_fire_time)
+ 'start_time': marshal_date(self.start_time),
+ 'end_time': marshal_date(self.end_time),
+ 'last_fire_time': marshal_date(self._last_fire_time)
}
def __setstate__(self, state):
require_state_version(self, state, 1)
- self.timezone = as_timezone(state['timezone'])
- self.start_time = as_aware_datetime(state['start_time'], self.timezone)
- self.end_time = as_aware_datetime(state['end_time'], self.timezone)
- self._last_fire_time = as_aware_datetime(state['last_fire_time'], self.timezone)
+ self.timezone = unmarshal_timezone(state['timezone'])
+ self.start_time = unmarshal_date(state['start_time'])
+ self.end_time = unmarshal_date(state['end_time'])
+ self._last_fire_time = unmarshal_date(state['last_fire_time'])
self._set_fields(state['fields'])
def __repr__(self):
@@ -200,5 +206,13 @@ class CronTrigger(Trigger):
if self.end_time:
fields.append(f'end_time={self.end_time.isoformat()!r}')
- fields.append(f'timezone={self.timezone.zone!r}')
+ fields.append(f'timezone={timezone_repr(self.timezone)!r}')
return f'CronTrigger({", ".join(fields)})'
+
+
+def datetime_ceil(dateval: datetime) -> datetime:
+ """Round the given datetime object upwards."""
+ if dateval.microsecond > 0:
+ return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)
+
+ return dateval
diff --git a/apscheduler/triggers/date.py b/apscheduler/triggers/date.py
index c20e5fe..636d978 100644
--- a/apscheduler/triggers/date.py
+++ b/apscheduler/triggers/date.py
@@ -1,9 +1,8 @@
from datetime import datetime, tzinfo
from typing import Optional, Union
-from dateutil.parser import parse
-
from ..abc import Trigger
+from ..marshalling import marshal_date, unmarshal_date
from ..validators import as_aware_datetime, as_timezone, require_state_version
@@ -13,13 +12,13 @@ class DateTrigger(Trigger):
:param run_time: the date/time to run the job at
:param timezone: time zone to use to convert ``run_time`` into a timezone aware datetime, if it
- isn't already (defaults to the local time zone)
+ isn't already
"""
__slots__ = 'run_time', '_completed'
- def __init__(self, run_time: datetime, timezone: Union[str, tzinfo, None] = None):
- timezone = as_timezone(timezone or run_time.tzinfo)
+ def __init__(self, run_time: datetime, timezone: Union[tzinfo, str] = 'local'):
+ timezone = as_timezone(timezone)
self.run_time = as_aware_datetime(run_time, timezone)
self._completed = False
@@ -33,14 +32,14 @@ class DateTrigger(Trigger):
def __getstate__(self):
return {
'version': 1,
- 'run_time': self.run_time.isoformat(),
+ 'run_time': marshal_date(self.run_time),
'completed': self._completed
}
def __setstate__(self, state):
require_state_version(self, state, 1)
- self.run_time = parse(state['run_time'])
+ self.run_time = unmarshal_date(state['run_time'])
self._completed = state['completed']
def __repr__(self):
- return f'DateTrigger({self.run_time.isoformat()!r})'
+ return f"{self.__class__.__name__}('{self.run_time}')"
diff --git a/apscheduler/triggers/interval.py b/apscheduler/triggers/interval.py
index e1e24a9..b18bc8c 100644
--- a/apscheduler/triggers/interval.py
+++ b/apscheduler/triggers/interval.py
@@ -2,7 +2,8 @@ from datetime import datetime, timedelta, tzinfo
from typing import Optional, Union
from ..abc import Trigger
-from ..validators import as_aware_datetime, as_timestamp, as_timezone, require_state_version
+from ..marshalling import marshal_date, unmarshal_date
+from ..validators import as_aware_datetime, as_timezone, require_state_version
class IntervalTrigger(Trigger):
@@ -22,25 +23,24 @@ class IntervalTrigger(Trigger):
:param microseconds: number of microseconds to wait
:param start_time: first trigger date/time
:param end_time: latest possible date/time to trigger on
- :param timezone: time zone to use for normalizing calculated datetimes (defaults to the local
- timezone)
+ :param timezone: time zone to use for converting any naive datetimes to timezone aware
"""
__slots__ = ('weeks', 'days', 'hours', 'minutes', 'seconds', 'microseconds', 'start_time',
- 'end_time', 'timezone', '_interval', '_last_fire_time')
+ 'end_time', '_interval', '_last_fire_time')
def __init__(self, *, weeks: int = 0, days: int = 0, hours: int = 0, minutes: int = 0,
seconds: int = 0, microseconds: int = 0, start_time: Optional[datetime] = None,
- end_time: Optional[datetime] = None, timezone: Union[str, tzinfo, None] = None):
+ end_time: Optional[datetime] = None, timezone: Union[tzinfo, str] = 'local'):
self.weeks = weeks
self.days = days
self.hours = hours
self.minutes = minutes
self.seconds = seconds
self.microseconds = microseconds
- self.timezone = as_timezone(timezone)
- self.start_time = as_aware_datetime(start_time or datetime.now(), self.timezone)
- self.end_time = as_aware_datetime(end_time, self.timezone)
+ timezone = as_timezone(timezone)
+ self.start_time = as_aware_datetime(start_time or datetime.now(), timezone)
+ self.end_time = as_aware_datetime(end_time, timezone)
self._interval = timedelta(weeks=self.weeks, days=self.days, hours=self.hours,
minutes=self.minutes, seconds=self.seconds,
microseconds=self.microseconds)
@@ -56,7 +56,7 @@ class IntervalTrigger(Trigger):
if self._last_fire_time is None:
self._last_fire_time = self.start_time
else:
- self._last_fire_time = self.timezone.normalize(self._last_fire_time + self._interval)
+ self._last_fire_time = self._last_fire_time + self._interval
if self.end_time is None or self._last_fire_time <= self.end_time:
return self._last_fire_time
@@ -68,23 +68,21 @@ class IntervalTrigger(Trigger):
'version': 1,
'interval': [self.weeks, self.days, self.hours, self.minutes, self.seconds,
self.microseconds],
- 'timezone': self.timezone.zone,
- 'start_time': as_timestamp(self.start_time),
- 'end_time': as_timestamp(self.end_time),
- 'last_fire_time': as_timestamp(self._last_fire_time)
+ 'start_time': marshal_date(self.start_time),
+ 'end_time': marshal_date(self.end_time),
+ 'last_fire_time': marshal_date(self._last_fire_time)
}
def __setstate__(self, state):
require_state_version(self, state, 1)
self.weeks, self.days, self.hours, self.minutes, self.seconds, self.microseconds = \
state['interval']
- self.timezone = as_timezone(state['timezone'])
- self.start_time = as_aware_datetime(state['start_time'], self.timezone)
- self.end_time = as_aware_datetime(state['end_time'], self.timezone)
+ self.start_time = unmarshal_date(state['start_time'])
+ self.end_time = unmarshal_date(state['end_time'])
+ self._last_fire_time = unmarshal_date(state['last_fire_time'])
self._interval = timedelta(weeks=self.weeks, days=self.days, hours=self.hours,
minutes=self.minutes, seconds=self.seconds,
microseconds=self.microseconds)
- self._last_fire_time = as_aware_datetime(state['last_fire_time'], self.timezone)
def __repr__(self):
fields = []
@@ -93,8 +91,8 @@ class IntervalTrigger(Trigger):
if value > 0:
fields.append(f'{field}={value}')
- fields.append(f'start_time={self.start_time.isoformat()!r}')
+ fields.append(f"start_time='{self.start_time}'")
if self.end_time:
- fields.append(f'end_time={self.end_time.isoformat()!r}')
+ fields.append(f"end_time='{self.end_time}'")
- return f'IntervalTrigger({", ".join(fields)})'
+ return f'{self.__class__.__name__}({", ".join(fields)})'
diff --git a/apscheduler/util.py b/apscheduler/util.py
index be1db38..38d6380 100644
--- a/apscheduler/util.py
+++ b/apscheduler/util.py
@@ -1,10 +1,11 @@
"""This module contains several handy functions primarily meant for internal use."""
+import sys
+from datetime import datetime, timedelta, tzinfo
-import re
-from datetime import datetime, timedelta
-from functools import partial
-from inspect import isclass, ismethod, signature
-from typing import Any, Tuple
+if sys.version_info >= (3, 9):
+ from zoneinfo import ZoneInfo
+else:
+ from backports.zoneinfo import ZoneInfo
class _Undefined:
@@ -18,216 +19,8 @@ class _Undefined:
undefined = _Undefined() #: a unique object that only signifies that no value is defined
-_DATE_REGEX = re.compile(
- r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
- r'(?:[ T](?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})'
- r'(?:\.(?P<microsecond>\d{1,6}))?'
- r'(?P<timezone>Z|[+-]\d\d:\d\d)?)?$')
-
-
-def datetime_ceil(dateval: datetime):
- """Round the given datetime object upwards."""
- if dateval.microsecond > 0:
- return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)
- return dateval
-
-
-def get_callable_name(func):
- """
- Returns the best available display name for the given function/callable.
-
- :rtype: str
-
- """
- # the easy case (on Python 3.3+)
- if hasattr(func, '__qualname__'):
- return func.__qualname__
-
- # class methods, bound and unbound methods
- f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None)
- if f_self and hasattr(func, '__name__'):
- f_class = f_self if isclass(f_self) else f_self.__class__
- else:
- f_class = getattr(func, 'im_class', None)
-
- if f_class and hasattr(func, '__name__'):
- return '%s.%s' % (f_class.__name__, func.__name__)
-
- # class or class instance
- if hasattr(func, '__call__'):
- # class
- if hasattr(func, '__name__'):
- return func.__name__
-
- # instance of a class with a __call__ method
- return func.__class__.__name__
-
- raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func)
-
-
-def obj_to_ref(obj):
- """
- Returns the path to the given callable.
-
- :rtype: str
- :raises TypeError: if the given object is not callable
- :raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested
- function
-
- """
- if isinstance(obj, partial):
- raise ValueError('Cannot create a reference to a partial()')
-
- name = get_callable_name(obj)
- if '<lambda>' in name:
- raise ValueError('Cannot create a reference to a lambda')
- if '<locals>' in name:
- raise ValueError('Cannot create a reference to a nested function')
-
- if ismethod(obj):
- if hasattr(obj, 'im_self') and obj.im_self:
- # bound method
- module = obj.im_self.__module__
- elif hasattr(obj, 'im_class') and obj.im_class:
- # unbound method
- module = obj.im_class.__module__
- else:
- module = obj.__module__
+def timezone_repr(timezone: tzinfo) -> str:
+ if isinstance(timezone, ZoneInfo):
+ return timezone.key
else:
- module = obj.__module__
- return '%s:%s' % (module, name)
-
-
-def ref_to_obj(ref):
- """
- Returns the object pointed to by ``ref``.
-
- :type ref: str
-
- """
- if not isinstance(ref, str):
- raise TypeError('References must be strings')
- if ':' not in ref:
- raise ValueError('Invalid reference')
-
- modulename, rest = ref.split(':', 1)
- try:
- obj = __import__(modulename, fromlist=[rest])
- except ImportError:
- raise LookupError('Error resolving reference %s: could not import module' % ref)
-
- try:
- for name in rest.split('.'):
- obj = getattr(obj, name)
- return obj
- except Exception:
- raise LookupError('Error resolving reference %s: error looking up object' % ref)
-
-
-def maybe_ref(ref):
- """
- Returns the object that the given reference points to, if it is indeed a reference.
- If it is not a reference, the object is returned as-is.
-
- """
- if not isinstance(ref, str):
- return ref
- return ref_to_obj(ref)
-
-
-def check_callable_args(func, args, kwargs):
- """
- Ensures that the given callable can be called with the given arguments.
-
- :type args: tuple
- :type kwargs: dict
-
- """
- pos_kwargs_conflicts = [] # parameters that have a match in both args and kwargs
- positional_only_kwargs = [] # positional-only parameters that have a match in kwargs
- unsatisfied_args = [] # parameters in signature that don't have a match in args or kwargs
- unsatisfied_kwargs = [] # keyword-only arguments that don't have a match in kwargs
- unmatched_args = list(args) # args that didn't match any of the parameters in the signature
- # kwargs that didn't match any of the parameters in the signature
- unmatched_kwargs = list(kwargs)
- # indicates if the signature defines *args and **kwargs respectively
- has_varargs = has_var_kwargs = False
-
- try:
- sig = signature(func)
- except ValueError:
- # signature() doesn't work against every kind of callable
- return
-
- for param in sig.parameters.values():
- if param.kind == param.POSITIONAL_OR_KEYWORD:
- if param.name in unmatched_kwargs and unmatched_args:
- pos_kwargs_conflicts.append(param.name)
- elif unmatched_args:
- del unmatched_args[0]
- elif param.name in unmatched_kwargs:
- unmatched_kwargs.remove(param.name)
- elif param.default is param.empty:
- unsatisfied_args.append(param.name)
- elif param.kind == param.POSITIONAL_ONLY:
- if unmatched_args:
- del unmatched_args[0]
- elif param.name in unmatched_kwargs:
- unmatched_kwargs.remove(param.name)
- positional_only_kwargs.append(param.name)
- elif param.default is param.empty:
- unsatisfied_args.append(param.name)
- elif param.kind == param.KEYWORD_ONLY:
- if param.name in unmatched_kwargs:
- unmatched_kwargs.remove(param.name)
- elif param.default is param.empty:
- unsatisfied_kwargs.append(param.name)
- elif param.kind == param.VAR_POSITIONAL:
- has_varargs = True
- elif param.kind == param.VAR_KEYWORD:
- has_var_kwargs = True
-
- # Make sure there are no conflicts between args and kwargs
- if pos_kwargs_conflicts:
- raise ValueError('The following arguments are supplied in both args and kwargs: %s' %
- ', '.join(pos_kwargs_conflicts))
-
- # Check if keyword arguments are being fed to positional-only parameters
- if positional_only_kwargs:
- raise ValueError('The following arguments cannot be given as keyword arguments: %s' %
- ', '.join(positional_only_kwargs))
-
- # Check that the number of positional arguments minus the number of matched kwargs matches the
- # argspec
- if unsatisfied_args:
- raise ValueError('The following arguments have not been supplied: %s' %
- ', '.join(unsatisfied_args))
-
- # Check that all keyword-only arguments have been supplied
- if unsatisfied_kwargs:
- raise ValueError(
- 'The following keyword-only arguments have not been supplied in kwargs: %s' %
- ', '.join(unsatisfied_kwargs))
-
- # Check that the callable can accept the given number of positional arguments
- if not has_varargs and unmatched_args:
- raise ValueError(
- 'The list of positional arguments is longer than the target callable can handle '
- '(allowed: %d, given in args: %d)' % (len(args) - len(unmatched_args), len(args)))
-
- # Check that the callable can accept the given keyword arguments
- if not has_var_kwargs and unmatched_kwargs:
- raise ValueError(
- 'The target callable does not accept the following keyword arguments: %s' %
- ', '.join(unmatched_kwargs))
-
-
-def marshal_object(obj) -> Tuple[str, Any]:
- return f'{obj.__class__.__module__}:{obj.__class__.__qualname__}', obj.__getstate__()
-
-
-def unmarshal_object(ref: str, state):
- cls = ref_to_obj(ref)
- instance = cls.__new__(cls)
- instance.__setstate__(state)
- return instance
+ return repr(timezone)
diff --git a/apscheduler/validators.py b/apscheduler/validators.py
index 5845bd3..b77da96 100644
--- a/apscheduler/validators.py
+++ b/apscheduler/validators.py
@@ -1,12 +1,17 @@
+import sys
from datetime import date, datetime, timedelta, timezone, tzinfo
from typing import Any, Dict, Optional, Union
-import pytz
from apscheduler.abc import Trigger
from apscheduler.exceptions import DeserializationError
from dateutil.parser import parse
from tzlocal import get_localzone
+if sys.version_info >= (3, 9):
+ from zoneinfo import ZoneInfo
+else:
+ from backports.zoneinfo import ZoneInfo
+
def as_int(value) -> Optional[int]:
"""Convert the value into an integer."""
@@ -18,26 +23,26 @@ def as_int(value) -> Optional[int]:
def as_timezone(value: Union[str, tzinfo, None]) -> tzinfo:
"""
- Convert the value into a pytz timezone.
+ Convert the value into a tzinfo object.
+
+ If ``value`` is ``None`` or ``'local'``, use the local timezone.
:param value: the value to be converted
- :return: a timezone object, or if ``None`` was given, the local timezone
+ :return: a timezone object
"""
if value is None or value == 'local':
return get_localzone()
elif isinstance(value, str):
- return pytz.timezone(value)
+ return ZoneInfo(value)
elif isinstance(value, tzinfo):
if value is timezone.utc:
- return pytz.utc
- elif not getattr(value, 'zone', None):
- raise TypeError('Only named pytz timezones are supported')
+ return ZoneInfo('UTC')
else:
return value
- raise TypeError(f'Expected pytz timezone or timezone.utc, got {value.__class__.__qualname__}'
- f'instead')
+ raise TypeError(f'Expected tzinfo instance or timezone name, got '
+ f'{value.__class__.__qualname__} instead')
def as_date(value: Union[date, str, None]) -> Optional[date]:
@@ -76,7 +81,7 @@ def as_ordinal_date(value: Optional[date]) -> Optional[int]:
return value.toordinal()
-def as_aware_datetime(value: Union[datetime, str, float, None], tz: tzinfo) -> Optional[datetime]:
+def as_aware_datetime(value: Union[datetime, str, None], tz: tzinfo) -> Optional[datetime]:
"""
Convert the value to a timezone aware datetime.
@@ -88,22 +93,14 @@ def as_aware_datetime(value: Union[datetime, str, float, None], tz: tzinfo) -> O
if value is None:
return None
- if isinstance(value, float):
- return datetime.fromtimestamp(value, tz)
-
if isinstance(value, str):
value = parse(value)
if isinstance(value, datetime):
- if value.tzinfo:
- return value.astimezone(tz)
-
- try:
- # Works with pytz timezones
- return tz.localize(value)
- except AttributeError:
- # Not a pytz timezone
- return value.astimezone(tz)
+ if not value.tzinfo:
+ return value.replace(tzinfo=tz)
+ else:
+ return value
raise TypeError(f'Expected string or datetime, got {value.__class__.__qualname__} instead')
diff --git a/setup.cfg b/setup.cfg
index ee5941d..1db6cca 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -40,6 +40,7 @@ test =
pytest >= 5.0
pytest-cov
pytest-mock
+ pytz
doc =
sphinx
sphinx-rtd-theme
diff --git a/tests/conftest.py b/tests/conftest.py
index 05f3f4b..62e552f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,16 +1,21 @@
+import sys
from datetime import datetime
from unittest.mock import Mock
import pytest
-import pytz
from apscheduler.serializers.cbor import CBORSerializer
from apscheduler.serializers.json import JSONSerializer
from apscheduler.serializers.pickle import PickleSerializer
+if sys.version_info >= (3, 9):
+ from zoneinfo import ZoneInfo
+else:
+ from backports.zoneinfo import ZoneInfo
+
@pytest.fixture(scope='session')
def timezone():
- return pytz.timezone('Europe/Berlin')
+ return ZoneInfo('Europe/Berlin')
@pytest.fixture
diff --git a/tests/test_marshalling.py b/tests/test_marshalling.py
new file mode 100644
index 0000000..541ea3e
--- /dev/null
+++ b/tests/test_marshalling.py
@@ -0,0 +1,88 @@
+import sys
+from datetime import timedelta
+from functools import partial
+from types import ModuleType
+
+import pytest
+from apscheduler.exceptions import SerializationError
+from apscheduler.marshalling import callable_from_ref, callable_to_ref
+
+
+class DummyClass:
+ def meth(self):
+ pass
+
+ @staticmethod
+ def staticmeth():
+ pass
+
+ @classmethod
+ def classmeth(cls):
+ pass
+
+ def __call__(self):
+ pass
+
+ class InnerDummyClass:
+ @classmethod
+ def innerclassmeth(cls):
+ pass
+
+
+class InheritedDummyClass(DummyClass):
+ @classmethod
+ def classmeth(cls):
+ pass
+
+
+class TestCallableToRef(object):
+ @pytest.mark.parametrize('obj, error', [
+ (partial(DummyClass.meth), 'Cannot create a reference to a partial()'),
+ (lambda: None, 'Cannot create a reference to a lambda')
+ ], ids=['partial', 'lambda'])
+ def test_errors(self, obj, error):
+ exc = pytest.raises(SerializationError, callable_to_ref, obj)
+ assert str(exc.value) == error
+
+ def test_nested_function_error(self):
+ def nested():
+ pass
+
+ exc = pytest.raises(SerializationError, callable_to_ref, nested)
+ assert str(exc.value) == 'Cannot create a reference to a nested function'
+
+ @pytest.mark.parametrize('input,expected', [
+ (DummyClass.meth, 'test_marshalling:DummyClass.meth'),
+ (DummyClass.classmeth, 'test_marshalling:DummyClass.classmeth'),
+ (DummyClass.InnerDummyClass.innerclassmeth,
+ 'test_marshalling:DummyClass.InnerDummyClass.innerclassmeth'),
+ (DummyClass.staticmeth, 'test_marshalling:DummyClass.staticmeth'),
+ (InheritedDummyClass.classmeth, 'test_marshalling:InheritedDummyClass.classmeth'),
+ (timedelta, 'datetime:timedelta'),
+ ], ids=['unbound method', 'class method', 'inner class method', 'static method',
+ 'inherited class method', 'timedelta'])
+ def test_valid_refs(self, input, expected):
+ assert callable_to_ref(input) == expected
+
+
+class TestCallableFromRef(object):
+ def test_valid_ref(self):
+ from logging.handlers import RotatingFileHandler
+ assert callable_from_ref('logging.handlers:RotatingFileHandler') is RotatingFileHandler
+
+ def test_complex_path(self):
+ pkg1 = ModuleType('pkg1')
+ pkg1.pkg2 = 'blah'
+ pkg2 = ModuleType('pkg1.pkg2')
+ pkg2.varname = lambda: None
+ sys.modules['pkg1'] = pkg1
+ sys.modules['pkg1.pkg2'] = pkg2
+ assert callable_from_ref('pkg1.pkg2:varname') == pkg2.varname
+
+ @pytest.mark.parametrize('input,error', [
+ (object(), TypeError),
+ ('module', ValueError),
+ ('module:blah', LookupError)
+ ], ids=['raw object', 'module', 'module attribute'])
+ def test_lookup_error(self, input, error):
+ pytest.raises(error, callable_from_ref, input)
diff --git a/tests/test_util.py b/tests/test_util.py
deleted file mode 100644
index 19ed988..0000000
--- a/tests/test_util.py
+++ /dev/null
@@ -1,194 +0,0 @@
-import platform
-import sys
-from datetime import datetime, timedelta
-from functools import partial
-from types import ModuleType
-
-import pytest
-from apscheduler.util import (
- check_callable_args, datetime_ceil, get_callable_name, maybe_ref, obj_to_ref, ref_to_obj)
-
-
-class DummyClass(object):
- def meth(self):
- pass
-
- @staticmethod
- def staticmeth():
- pass
-
- @classmethod
- def classmeth(cls):
- pass
-
- def __call__(self):
- pass
-
- class InnerDummyClass(object):
- @classmethod
- def innerclassmeth(cls):
- pass
-
-
-class InheritedDummyClass(DummyClass):
- @classmethod
- def classmeth(cls):
- pass
-
-
-@pytest.mark.parametrize('input,expected', [
- (datetime(2009, 4, 7, 2, 10, 16, 4000), datetime(2009, 4, 7, 2, 10, 17)),
- (datetime(2009, 4, 7, 2, 10, 16), datetime(2009, 4, 7, 2, 10, 16))
-], ids=['milliseconds', 'exact'])
-def test_datetime_ceil(input, expected):
- assert datetime_ceil(input) == expected
-
-
-class TestGetCallableName(object):
- @pytest.mark.parametrize('input,expected', [
- (open, 'open'),
- (DummyClass.staticmeth, 'DummyClass.staticmeth' if
- hasattr(DummyClass, '__qualname__') else 'staticmeth'),
- (DummyClass.classmeth, 'DummyClass.classmeth'),
- (DummyClass.meth, 'DummyClass.meth'),
- (DummyClass().meth, 'DummyClass.meth'),
- (DummyClass, 'DummyClass'),
- (DummyClass(), 'DummyClass')
- ], ids=['function', 'static method', 'class method', 'unbounded method', 'bounded method',
- 'class', 'instance'])
- def test_inputs(self, input, expected):
- assert get_callable_name(input) == expected
-
- def test_bad_input(self):
- pytest.raises(TypeError, get_callable_name, object())
-
-
-class TestObjToRef(object):
- @pytest.mark.parametrize('obj, error', [
- (partial(DummyClass.meth), 'Cannot create a reference to a partial()'),
- (lambda: None, 'Cannot create a reference to a lambda')
- ], ids=['partial', 'lambda'])
- def test_errors(self, obj, error):
- exc = pytest.raises(ValueError, obj_to_ref, obj)
- assert str(exc.value) == error
-
- def test_nested_function_error(self):
- def nested():
- pass
-
- exc = pytest.raises(ValueError, obj_to_ref, nested)
- assert str(exc.value) == 'Cannot create a reference to a nested function'
-
- @pytest.mark.parametrize('input,expected', [
- (DummyClass.meth, 'test_util:DummyClass.meth'),
- (DummyClass.classmeth, 'test_util:DummyClass.classmeth'),
- (DummyClass.InnerDummyClass.innerclassmeth,
- 'test_util:DummyClass.InnerDummyClass.innerclassmeth'),
- (DummyClass.staticmeth, 'test_util:DummyClass.staticmeth'),
- (InheritedDummyClass.classmeth, 'test_util:InheritedDummyClass.classmeth'),
- (timedelta, 'datetime:timedelta'),
- ], ids=['unbound method', 'class method', 'inner class method', 'static method',
- 'inherited class method', 'timedelta'])
- def test_valid_refs(self, input, expected):
- assert obj_to_ref(input) == expected
-
-
-class TestRefToObj(object):
- def test_valid_ref(self):
- from logging.handlers import RotatingFileHandler
- assert ref_to_obj('logging.handlers:RotatingFileHandler') is RotatingFileHandler
-
- def test_complex_path(self):
- pkg1 = ModuleType('pkg1')
- pkg1.pkg2 = 'blah'
- pkg2 = ModuleType('pkg1.pkg2')
- pkg2.varname = 'test'
- sys.modules['pkg1'] = pkg1
- sys.modules['pkg1.pkg2'] = pkg2
- assert ref_to_obj('pkg1.pkg2:varname') == 'test'
-
- @pytest.mark.parametrize('input,error', [
- (object(), TypeError),
- ('module', ValueError),
- ('module:blah', LookupError)
- ], ids=['raw object', 'module', 'module attribute'])
- def test_lookup_error(self, input, error):
- pytest.raises(error, ref_to_obj, input)
-
-
-@pytest.mark.parametrize('input,expected', [
- ('datetime:timedelta', timedelta),
- (timedelta, timedelta)
-], ids=['textref', 'direct'])
-def test_maybe_ref(input, expected):
- assert maybe_ref(input) == expected
-
-
-class TestCheckCallableArgs(object):
- def test_invalid_callable_args(self):
- """
- Tests that attempting to create a job with an invalid number of arguments raises an
- exception.
-
- """
- exc = pytest.raises(ValueError, check_callable_args, lambda x: None, [1, 2], {})
- assert str(exc.value) == (
- 'The list of positional arguments is longer than the target callable can handle '
- '(allowed: 1, given in args: 2)')
-
- def test_invalid_callable_kwargs(self):
- """
- Tests that attempting to schedule a job with unmatched keyword arguments raises an
- exception.
-
- """
- exc = pytest.raises(ValueError, check_callable_args, lambda x: None, [], {'x': 0, 'y': 1})
- assert str(exc.value) == ('The target callable does not accept the following keyword '
- 'arguments: y')
-
- def test_missing_callable_args(self):
- """Tests that attempting to schedule a job with missing arguments raises an exception."""
- exc = pytest.raises(ValueError, check_callable_args, lambda x, y, z: None, [1], {'y': 0})
- assert str(exc.value) == 'The following arguments have not been supplied: z'
-
- def test_default_args(self):
- """Tests that default values for arguments are properly taken into account."""
- exc = pytest.raises(ValueError, check_callable_args, lambda x, y, z=1: None, [1], {})
- assert str(exc.value) == 'The following arguments have not been supplied: y'
-
- def test_conflicting_callable_args(self):
- """
- Tests that attempting to schedule a job where the combination of args and kwargs are in
- conflict raises an exception.
-
- """
- exc = pytest.raises(ValueError, check_callable_args, lambda x, y: None, [1, 2], {'y': 1})
- assert str(exc.value) == 'The following arguments are supplied in both args and kwargs: y'
-
- def test_signature_positional_only(self):
- """Tests that a function where signature() fails is accepted."""
- check_callable_args(object().__setattr__, ('blah', 1), {})
-
- @pytest.mark.skipif(platform.python_implementation() == 'PyPy',
- reason='PyPy does not expose signatures of builtins')
- def test_positional_only_args(self):
- """
- Tests that an attempt to use keyword arguments for positional-only arguments raises an
- exception.
-
- """
- exc = pytest.raises(ValueError, check_callable_args, object.__setattr__, ['blah'],
- {'value': 1})
- assert str(exc.value) == ('The following arguments cannot be given as keyword arguments: '
- 'value')
-
- def test_unfulfilled_kwargs(self):
- """
- Tests that attempting to schedule a job where not all keyword-only arguments are fulfilled
- raises an exception.
-
- """
- func = eval("lambda x, *, y, z=1: None")
- exc = pytest.raises(ValueError, check_callable_args, func, [1], {})
- assert str(exc.value) == ('The following keyword-only arguments have not been supplied in '
- 'kwargs: y')
diff --git a/tests/triggers/test_calendarinterval.py b/tests/triggers/test_calendarinterval.py
index 079d014..96f9b7e 100644
--- a/tests/triggers/test_calendarinterval.py
+++ b/tests/triggers/test_calendarinterval.py
@@ -38,7 +38,7 @@ def test_missing_time(timezone, serializer):
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2016, 3, 28, 2, 30))
+ assert trigger.next() == datetime(2016, 3, 28, 2, 30, tzinfo=timezone)
def test_repeated_time(timezone, serializer):
@@ -52,7 +52,7 @@ def test_repeated_time(timezone, serializer):
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2016, 10, 30, 2, 30), is_dst=True)
+ assert trigger.next() == datetime(2016, 10, 30, 2, 30, tzinfo=timezone, fold=0)
def test_nonexistent_days(timezone, serializer):
@@ -61,8 +61,8 @@ def test_nonexistent_days(timezone, serializer):
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2016, 3, 31))
- assert trigger.next() == timezone.localize(datetime(2016, 5, 31))
+ assert trigger.next() == datetime(2016, 3, 31, tzinfo=timezone)
+ assert trigger.next() == datetime(2016, 5, 31, tzinfo=timezone)
def test_repr(timezone, serializer):
diff --git a/tests/triggers/test_combining.py b/tests/triggers/test_combining.py
index 73a5836..c7c409e 100644
--- a/tests/triggers/test_combining.py
+++ b/tests/triggers/test_combining.py
@@ -10,8 +10,8 @@ from apscheduler.triggers.interval import IntervalTrigger
class TestAndTrigger:
@pytest.mark.parametrize('threshold', [1, 0])
def test_two_datetriggers(self, timezone, serializer, threshold):
- date1 = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
- date2 = timezone.localize(datetime(2020, 5, 16, 14, 17, 31, 254212))
+ date1 = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
+ date2 = datetime(2020, 5, 16, 14, 17, 31, 254212, tzinfo=timezone)
trigger = AndTrigger([DateTrigger(date1), DateTrigger(date2)], threshold=threshold)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
@@ -23,7 +23,7 @@ class TestAndTrigger:
assert trigger.next() is None
def test_max_iterations(self, timezone, serializer):
- start_time = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
+ start_time = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
trigger = AndTrigger([
IntervalTrigger(seconds=4, start_time=start_time, timezone=timezone),
IntervalTrigger(seconds=4, start_time=start_time + timedelta(seconds=2),
@@ -35,7 +35,7 @@ class TestAndTrigger:
pytest.raises(MaxIterationsReached, trigger.next)
def test_repr(self, timezone, serializer):
- start_time = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
+ start_time = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
trigger = AndTrigger([
IntervalTrigger(seconds=4, start_time=start_time, timezone=timezone),
IntervalTrigger(seconds=4, start_time=start_time + timedelta(seconds=2),
@@ -46,15 +46,15 @@ class TestAndTrigger:
assert repr(trigger) == (
"AndTrigger([IntervalTrigger(seconds=4, "
- "start_time='2020-05-16T14:17:30.254212+02:00'), IntervalTrigger(seconds=4, "
- "start_time='2020-05-16T14:17:32.254212+02:00')], threshold=1.0, max_iterations=10000)"
+ "start_time='2020-05-16 14:17:30.254212+02:00'), IntervalTrigger(seconds=4, "
+ "start_time='2020-05-16 14:17:32.254212+02:00')], threshold=1.0, max_iterations=10000)"
)
class TestOrTrigger:
def test_two_datetriggers(self, timezone, serializer):
- date1 = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
- date2 = timezone.localize(datetime(2020, 5, 18, 15, 1, 53, 940564))
+ date1 = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
+ date2 = datetime(2020, 5, 18, 15, 1, 53, 940564, tzinfo=timezone)
trigger = OrTrigger([DateTrigger(date1), DateTrigger(date2)])
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
@@ -64,7 +64,7 @@ class TestOrTrigger:
assert trigger.next() is None
def test_two_interval_triggers(self, timezone, serializer):
- start_time = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
+ start_time = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
end_time1 = start_time + timedelta(seconds=16)
end_time2 = start_time + timedelta(seconds=18)
trigger = OrTrigger([
@@ -86,8 +86,8 @@ class TestOrTrigger:
assert trigger.next() is None
def test_repr(self, timezone):
- date1 = timezone.localize(datetime(2020, 5, 16, 14, 17, 30, 254212))
- date2 = timezone.localize(datetime(2020, 5, 18, 15, 1, 53, 940564))
+ date1 = datetime(2020, 5, 16, 14, 17, 30, 254212, tzinfo=timezone)
+ date2 = datetime(2020, 5, 18, 15, 1, 53, 940564, tzinfo=timezone)
trigger = OrTrigger([DateTrigger(date1), DateTrigger(date2)])
- assert repr(trigger) == ("OrTrigger([DateTrigger('2020-05-16T14:17:30.254212+02:00'), "
- "DateTrigger('2020-05-18T15:01:53.940564+02:00')])")
+ assert repr(trigger) == ("OrTrigger([DateTrigger('2020-05-16 14:17:30.254212+02:00'), "
+ "DateTrigger('2020-05-18 15:01:53.940564+02:00')])")
diff --git a/tests/triggers/test_cron.py b/tests/triggers/test_cron.py
index 8f26aa3..f79879b 100644
--- a/tests/triggers/test_cron.py
+++ b/tests/triggers/test_cron.py
@@ -1,9 +1,14 @@
+import sys
from datetime import datetime
import pytest
-import pytz
from apscheduler.triggers.cron import CronTrigger
+if sys.version_info >= (3, 9):
+ from zoneinfo import ZoneInfo
+else:
+ from backports.zoneinfo import ZoneInfo
+
def test_invalid_expression():
exc = pytest.raises(ValueError, CronTrigger, year='2009-fault')
@@ -52,61 +57,61 @@ def test_invalid_ranges(values, expected):
def test_cron_trigger_1(timezone, serializer):
- start_time = timezone.localize(datetime(2008, 12, 1))
+ start_time = datetime(2008, 12, 1, tzinfo=timezone)
trigger = CronTrigger(year='2009/2', month='1-4/3', day='5-6', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 1, 5))
- assert trigger.next() == timezone.localize(datetime(2009, 1, 6))
- assert trigger.next() == timezone.localize(datetime(2009, 4, 5))
- assert trigger.next() == timezone.localize(datetime(2009, 4, 6))
- assert trigger.next() == timezone.localize(datetime(2011, 1, 5))
+ assert trigger.next() == datetime(2009, 1, 5, tzinfo=timezone)
+ assert trigger.next() == datetime(2009, 1, 6, tzinfo=timezone)
+ assert trigger.next() == datetime(2009, 4, 5, tzinfo=timezone)
+ assert trigger.next() == datetime(2009, 4, 6, tzinfo=timezone)
+ assert trigger.next() == datetime(2011, 1, 5, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2009/2', month='1-4/3', day='5-6', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
"start_time='2008-12-01T00:00:00+01:00', timezone='Europe/Berlin')")
def test_cron_trigger_2(timezone, serializer):
- start_time = timezone.localize(datetime(2009, 10, 14))
+ start_time = datetime(2009, 10, 14, tzinfo=timezone)
trigger = CronTrigger(year='2009/2', month='1-3', day='5', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2011, 1, 5))
- assert trigger.next() == timezone.localize(datetime(2011, 2, 5))
- assert trigger.next() == timezone.localize(datetime(2011, 3, 5))
- assert trigger.next() == timezone.localize(datetime(2013, 1, 5))
+ assert trigger.next() == datetime(2011, 1, 5, tzinfo=timezone)
+ assert trigger.next() == datetime(2011, 2, 5, tzinfo=timezone)
+ assert trigger.next() == datetime(2011, 3, 5, tzinfo=timezone)
+ assert trigger.next() == datetime(2013, 1, 5, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2009/2', month='1-3', day='5', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
"start_time='2009-10-14T00:00:00+02:00', timezone='Europe/Berlin')")
def test_cron_trigger_3(timezone, serializer):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year='2009', month='feb-dec', hour='8-9', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 2, 1, 8))
- assert trigger.next() == timezone.localize(datetime(2009, 2, 1, 9))
- assert trigger.next() == timezone.localize(datetime(2009, 2, 2, 8))
+ assert trigger.next() == datetime(2009, 2, 1, 8, tzinfo=timezone)
+ assert trigger.next() == datetime(2009, 2, 1, 9, tzinfo=timezone)
+ assert trigger.next() == datetime(2009, 2, 2, 8, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2009', month='feb-dec', day='*', week='*', "
"day_of_week='*', hour='8-9', minute='0', second='0', "
"start_time='2009-01-01T00:00:00+01:00', timezone='Europe/Berlin')")
def test_cron_trigger_4(timezone, serializer):
- start_time = timezone.localize(datetime(2012, 2, 1))
+ start_time = datetime(2012, 2, 1, tzinfo=timezone)
trigger = CronTrigger(year='2012', month='2', day='last', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2012, 2, 29))
+ assert trigger.next() == datetime(2012, 2, 29, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2012', month='2', day='last', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
"start_time='2012-02-01T00:00:00+01:00', timezone='Europe/Berlin')")
@@ -114,28 +119,28 @@ def test_cron_trigger_4(timezone, serializer):
@pytest.mark.parametrize('expr', ['3-5', 'wed-fri'], ids=['numeric', 'text'])
def test_weekday_overlap(timezone, serializer, expr):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week=expr, start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 1, 7))
+ assert trigger.next() == datetime(2009, 1, 7, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2009', month='1', day='6-10', week='*', "
"day_of_week='wed-fri', hour='0', minute='0', second='0', "
"start_time='2009-01-01T00:00:00+01:00', timezone='Europe/Berlin')")
def test_weekday_range(timezone, serializer):
- start_time = timezone.localize(datetime(2020, 1, 1))
+ start_time = datetime(2020, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2020, month=1, week=1, day_of_week='fri-sun', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2020, 1, 3))
- assert trigger.next() == timezone.localize(datetime(2020, 1, 4))
- assert trigger.next() == timezone.localize(datetime(2020, 1, 5))
+ assert trigger.next() == datetime(2020, 1, 3, tzinfo=timezone)
+ assert trigger.next() == datetime(2020, 1, 4, tzinfo=timezone)
+ assert trigger.next() == datetime(2020, 1, 5, tzinfo=timezone)
assert trigger.next() is None
assert repr(trigger) == ("CronTrigger(year='2020', month='1', day='*', week='1', "
"day_of_week='fri-sun', hour='0', minute='0', second='0', "
@@ -143,14 +148,14 @@ def test_weekday_range(timezone, serializer):
def test_last_weekday(timezone, serializer):
- start_time = timezone.localize(datetime(2020, 1, 1))
+ start_time = datetime(2020, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2020, day='last sun', start_time=start_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2020, 1, 26))
- assert trigger.next() == timezone.localize(datetime(2020, 2, 23))
- assert trigger.next() == timezone.localize(datetime(2020, 3, 29))
+ assert trigger.next() == datetime(2020, 1, 26, tzinfo=timezone)
+ assert trigger.next() == datetime(2020, 2, 23, tzinfo=timezone)
+ assert trigger.next() == datetime(2020, 3, 29, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2020', month='*', day='last sun', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
"start_time='2020-01-01T00:00:00+01:00', timezone='Europe/Berlin')")
@@ -162,30 +167,30 @@ def test_increment_weekday(timezone, serializer):
date won't cause problems.
"""
- start_time = timezone.localize(datetime(2009, 9, 25, 7))
+ start_time = datetime(2009, 9, 25, 7, tzinfo=timezone)
trigger = CronTrigger(hour='5-6', start_time=start_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 9, 26, 5))
+ assert trigger.next() == datetime(2009, 9, 26, 5, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='*', month='*', day='*', week='*', "
"day_of_week='*', hour='5-6', minute='0', second='0', "
"start_time='2009-09-25T07:00:00+02:00', timezone='Europe/Berlin')")
def test_month_rollover(timezone, serializer):
- start_time = timezone.localize(datetime(2016, 2, 1))
+ start_time = datetime(2016, 2, 1, tzinfo=timezone)
trigger = CronTrigger(day=30, start_time=start_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2016, 3, 30))
- assert trigger.next() == timezone.localize(datetime(2016, 4, 30))
+ assert trigger.next() == datetime(2016, 3, 30, tzinfo=timezone)
+ assert trigger.next() == datetime(2016, 4, 30, tzinfo=timezone)
@pytest.mark.parametrize('weekday', ['1,0', 'mon,sun'], ids=['numeric', 'text'])
def test_weekday_nomatch(timezone, serializer, weekday):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week=weekday,
start_time=start_time, timezone=timezone)
if serializer:
@@ -198,13 +203,13 @@ def test_weekday_nomatch(timezone, serializer, weekday):
def test_weekday_positional(timezone, serializer):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, month=1, day='4th wed', start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 1, 28))
+ assert trigger.next() == datetime(2009, 1, 28, tzinfo=timezone)
assert repr(trigger) == ("CronTrigger(year='2009', month='1', day='4th wed', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
"start_time='2009-01-01T00:00:00+01:00', timezone='Europe/Berlin')")
@@ -212,13 +217,13 @@ def test_weekday_positional(timezone, serializer):
def test_end_time(timezone, serializer):
"""Test that next() won't produce"""
- start_time = timezone.localize(datetime(2014, 4, 13, 2))
- end_time = timezone.localize(datetime(2014, 4, 13, 4))
+ start_time = datetime(2014, 4, 13, 2, tzinfo=timezone)
+ end_time = datetime(2014, 4, 13, 4, tzinfo=timezone)
trigger = CronTrigger(hour=4, start_time=start_time, end_time=end_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2014, 4, 13, 4))
+ assert trigger.next() == datetime(2014, 4, 13, 4, tzinfo=timezone)
assert trigger.next() is None
assert repr(trigger) == ("CronTrigger(year='*', month='*', day='*', week='*', "
"day_of_week='*', hour='4', minute='0', second='0', "
@@ -227,13 +232,13 @@ def test_end_time(timezone, serializer):
def test_week_1(timezone, serializer):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, month=2, week=8, start_time=start_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
for day in range(16, 23):
- assert trigger.next() == timezone.localize(datetime(2009, 2, day))
+ assert trigger.next() == datetime(2009, 2, day, tzinfo=timezone)
assert trigger.next() is None
assert repr(trigger) == ("CronTrigger(year='2009', month='2', day='*', week='8', "
@@ -243,43 +248,48 @@ def test_week_1(timezone, serializer):
@pytest.mark.parametrize('weekday', [3, 'wed'], ids=['numeric', 'text'])
def test_week_2(timezone, serializer, weekday):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, week=15, day_of_week=weekday, start_time=start_time,
timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(datetime(2009, 4, 8))
+ assert trigger.next() == datetime(2009, 4, 8, tzinfo=timezone)
assert trigger.next() is None
assert repr(trigger) == ("CronTrigger(year='2009', month='*', day='*', week='15', "
"day_of_week='wed', hour='0', minute='0', second='0', "
"start_time='2009-01-01T00:00:00+01:00', timezone='Europe/Berlin')")
-@pytest.mark.parametrize('trigger_args, start_time, start_time_dst, correct_next_date', [
- ({'hour': 8}, datetime(2013, 3, 9, 12), False, datetime(2013, 3, 10, 8)),
- ({'hour': 8}, datetime(2013, 11, 2, 12), True, datetime(2013, 11, 3, 8)),
- ({'minute': '*/30'}, datetime(2013, 3, 10, 1, 35), False, datetime(2013, 3, 10, 3)),
- ({'minute': '*/30'}, datetime(2013, 11, 3, 1, 35), True, datetime(2013, 11, 3, 1))
-], ids=['absolute_spring', 'absolute_autumn', 'interval_spring', 'interval_autumn'])
-def test_dst_change(trigger_args, start_time, start_time_dst, correct_next_date, serializer):
+@pytest.mark.parametrize('trigger_args, start_time, start_time_fold, correct_next_date,'
+ 'correct_next_date_fold', [
+ ({'hour': 8}, datetime(2013, 3, 9, 12), 0, datetime(2013, 3, 10, 8), 0),
+ ({'hour': 8}, datetime(2013, 11, 2, 12), 0, datetime(2013, 11, 3, 8), 0),
+ ({'minute': '*/30'}, datetime(2013, 3, 10, 1, 35),
+ 0, datetime(2013, 3, 10, 3), 0),
+ ({'minute': '*/30'}, datetime(2013, 11, 3, 1, 35),
+ 0, datetime(2013, 11, 3, 1), 1)
+ ], ids=['absolute_spring', 'absolute_autumn', 'interval_spring', 'interval_autumn'])
+def test_dst_change(trigger_args, start_time, start_time_fold, correct_next_date,
+ correct_next_date_fold, serializer):
"""
Making sure that CronTrigger works correctly when crossing the DST switch threshold.
Note that you should explicitly compare datetimes as strings to avoid the internal datetime
comparison which would test for equality in the UTC timezone.
"""
- timezone = pytz.timezone('US/Eastern')
- start_time = timezone.localize(start_time, is_dst=start_time_dst)
+ timezone = ZoneInfo('US/Eastern')
+ start_time = start_time.replace(tzinfo=timezone, fold=start_time_fold)
trigger = CronTrigger(timezone=timezone, start_time=start_time, **trigger_args)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
- assert trigger.next() == timezone.localize(correct_next_date, is_dst=not start_time_dst)
+ assert trigger.next() == correct_next_date.replace(tzinfo=timezone,
+ fold=correct_next_date_fold)
def test_zero_value(timezone):
- start_time = timezone.localize(datetime(2020, 1, 1))
+ start_time = datetime(2020, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year=2009, month=2, hour=0, start_time=start_time, timezone=timezone)
assert repr(trigger) == ("CronTrigger(year='2009', month='2', day='*', week='*', "
"day_of_week='*', hour='0', minute='0', second='0', "
@@ -287,12 +297,12 @@ def test_zero_value(timezone):
def test_year_list(timezone, serializer):
- start_time = timezone.localize(datetime(2009, 1, 1))
+ start_time = datetime(2009, 1, 1, tzinfo=timezone)
trigger = CronTrigger(year='2009,2008', start_time=start_time, timezone=timezone)
assert repr(trigger) == "CronTrigger(year='2009,2008', month='1', day='1', week='*', " \
"day_of_week='*', hour='0', minute='0', second='0', " \
"start_time='2009-01-01T00:00:00+01:00', timezone='Europe/Berlin')"
- assert trigger.next() == timezone.localize(datetime(2009, 1, 1))
+ assert trigger.next() == datetime(2009, 1, 1, tzinfo=timezone)
assert trigger.next() is None
@@ -328,7 +338,7 @@ def test_year_list(timezone, serializer):
'saturday_first', 'weekend'])
def test_from_crontab(expr, expected_repr, timezone, serializer):
trigger = CronTrigger.from_crontab(expr, timezone)
- trigger.start_time = timezone.localize(datetime(2020, 5, 19, 19, 53, 22))
+ trigger.start_time = datetime(2020, 5, 19, 19, 53, 22, tzinfo=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
diff --git a/tests/triggers/test_date.py b/tests/triggers/test_date.py
index 5f4c607..6147c1c 100644
--- a/tests/triggers/test_date.py
+++ b/tests/triggers/test_date.py
@@ -4,7 +4,7 @@ from apscheduler.triggers.date import DateTrigger
def test_run_time(timezone, serializer):
- run_time = timezone.localize(datetime(2020, 5, 14, 11, 56, 12))
+ run_time = datetime(2020, 5, 14, 11, 56, 12, tzinfo=timezone)
trigger = DateTrigger(run_time)
if serializer:
payload = serializer.serialize(trigger)
@@ -12,4 +12,4 @@ def test_run_time(timezone, serializer):
assert trigger.next() == run_time
assert trigger.next() is None
- assert repr(trigger) == "DateTrigger('2020-05-14T11:56:12+02:00')"
+ assert repr(trigger) == "DateTrigger('2020-05-14 11:56:12+02:00')"
diff --git a/tests/triggers/test_interval.py b/tests/triggers/test_interval.py
index 6761777..292659f 100644
--- a/tests/triggers/test_interval.py
+++ b/tests/triggers/test_interval.py
@@ -10,16 +10,16 @@ def test_bad_interval():
def test_bad_end_time(timezone):
- start_time = timezone.localize(datetime(2020, 5, 16))
- end_time = timezone.localize(datetime(2020, 5, 15))
+ start_time = datetime(2020, 5, 16, tzinfo=timezone)
+ end_time = datetime(2020, 5, 15, tzinfo=timezone)
exc = pytest.raises(ValueError, IntervalTrigger, seconds=1, start_time=start_time,
end_time=end_time)
exc.match('end_time cannot be earlier than start_time')
def test_end_time(timezone, serializer):
- start_time = timezone.localize(datetime(2020, 5, 16, 19, 32, 44, 649521))
- end_time = timezone.localize(datetime(2020, 5, 16, 22, 33, 1))
+ start_time = datetime(2020, 5, 16, 19, 32, 44, 649521, tzinfo=timezone)
+ end_time = datetime(2020, 5, 16, 22, 33, 1, tzinfo=timezone)
interval = timedelta(hours=1, seconds=6)
trigger = IntervalTrigger(start_time=start_time, end_time=end_time, hours=1, seconds=6)
if serializer:
@@ -32,13 +32,13 @@ def test_end_time(timezone, serializer):
def test_repr(timezone, serializer):
- start_time = timezone.localize(datetime(2020, 5, 15, 12, 55, 32, 954032))
- end_time = timezone.localize(datetime(2020, 6, 4, 16, 18, 49, 306942))
+ start_time = datetime(2020, 5, 15, 12, 55, 32, 954032, tzinfo=timezone)
+ end_time = datetime(2020, 6, 4, 16, 18, 49, 306942, tzinfo=timezone)
trigger = IntervalTrigger(weeks=1, days=2, hours=3, minutes=4, seconds=5, microseconds=123525,
start_time=start_time, end_time=end_time, timezone=timezone)
if serializer:
trigger = serializer.deserialize(serializer.serialize(trigger))
assert repr(trigger) == ("IntervalTrigger(weeks=1, days=2, hours=3, minutes=4, seconds=5, "
- "microseconds=123525, start_time='2020-05-15T12:55:32.954032+02:00', "
- "end_time='2020-06-04T16:18:49.306942+02:00')")
+ "microseconds=123525, start_time='2020-05-15 12:55:32.954032+02:00', "
+ "end_time='2020-06-04 16:18:49.306942+02:00')")