Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R6 #143

Merged
merged 12 commits into from
Dec 17, 2024
9 changes: 6 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
Changelog
==============

5.0.2 (unreleased)
6.0.0 (unreleased)
------------------

- Rework timestamp_to_datetime to use whatever timezone [kiorky]
- Make datetime_to_timestamp & timestamp_to_datetime public [kiorky]
- Fix EPOCH calculation in case of non UTC & 32 bits based systems [kiorky]
- Apply isort formatter [kiorky]
- Reintegrate test_speed [kiorky]
- Apply black formatter [evanpurkhiser, kiorky]
- Code quality changes by evanpurkhiser:
- Code quality changes [evanpurkhiser, kiorky]
- Remove unused _get_caller_globals_and_locals [evanpurkhiser]
- Remove single-use bad_length [evanpurkhiser]
- Remove unused `days` in `proc_month` [evanpurkhiser]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def read(*rnames):

setup(
name='croniter',
version='5.0.2.dev0',
version='6.0.0.dev0',
py_modules=['croniter', ],
description=(
'croniter provides iteration for datetime '
Expand Down
9 changes: 9 additions & 0 deletions src/croniter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from . import croniter as cron_m
from .croniter import (
DAY_FIELD,
HOUR_FIELD,
MINUTE_FIELD,
MONTH_FIELD,
OVERFLOW32B_MODE,
SECOND_FIELD,
UTC_DT,
YEAR_FIELD,
CroniterBadCronError,
CroniterBadDateError,
CroniterBadTypeRangeError,
CroniterError,
CroniterNotAlphaError,
CroniterUnsupportedSyntaxError,
croniter,
Expand Down
62 changes: 39 additions & 23 deletions src/croniter/croniter.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ def is_32bit():
OrderedDict = dict # py26 degraded mode, expanders order will not be immutable


EPOCH = datetime.datetime.fromtimestamp(0)
try:
# py3 recent
UTC_DT = datetime.timezone.utc
except AttributeError:
UTC_DT = pytz.utc
EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT)

# fmt: off
M_ALPHAS = {
Expand Down Expand Up @@ -126,12 +131,9 @@ def is_32bit():
YEAR_CRON_LEN = len(YEAR_FIELDS)
# retrocompat
VALID_LEN_EXPRESSION = set(a for a in CRON_FIELDS if isinstance(a, int))
TIMESTAMP_TO_DT_CACHE = {}
EXPRESSIONS = {}
try:
# py3 recent
UTC_DT = datetime.timezone.utc
except AttributeError:
UTC_DT = pytz.utc
MARKER = object()


def timedelta_to_seconds(td):
Expand Down Expand Up @@ -301,44 +303,57 @@ def get_prev(self, ret_type=None, start_time=None, update_current=True):
def get_current(self, ret_type=None):
ret_type = ret_type or self._ret_type
if issubclass(ret_type, datetime.datetime):
return self._timestamp_to_datetime(self.cur)
return self.timestamp_to_datetime(self.cur)
return self.cur

def set_current(self, start_time, force=True):
if (force or (self.cur is None)) and start_time is not None:
if isinstance(start_time, datetime.datetime):
self.tzinfo = start_time.tzinfo
start_time = self._datetime_to_timestamp(start_time)
start_time = self.datetime_to_timestamp(start_time)

self.start_time = start_time
self.dst_start_time = start_time
self.cur = start_time
return self.cur

@staticmethod
def _datetime_to_timestamp(d):
def datetime_to_timestamp(d):
"""
Converts a `datetime` object `d` into a UNIX timestamp.
"""
return datetime_to_timestamp(d)

def _timestamp_to_datetime(self, timestamp):
_datetime_to_timestamp = datetime_to_timestamp # retrocompat

def timestamp_to_datetime(self, timestamp, tzinfo=MARKER):
"""
Converts a UNIX timestamp `timestamp` into a `datetime` object.
Converts a UNIX `timestamp` into a `datetime` object.
"""
if tzinfo is MARKER: # allow to give tzinfo=None even if self.tzinfo is set
tzinfo = self.tzinfo
k = timestamp
if tzinfo:
k = (timestamp, repr(tzinfo))
try:
return TIMESTAMP_TO_DT_CACHE[k]
except KeyError:
pass
if OVERFLOW32B_MODE:
# degraded mode to workaround Y2038
# see https://github.com/python/cpython/issues/101069
result = EPOCH + datetime.timedelta(seconds=timestamp)
result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
else:
result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
if self.tzinfo:
result = result.replace(tzinfo=tzutc()).astimezone(self.tzinfo)

if tzinfo:
result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
TIMESTAMP_TO_DT_CACHE[(result, repr(result.tzinfo))] = result
return result

_timestamp_to_datetime = timestamp_to_datetime # retrocompat

@staticmethod
def _timedelta_to_seconds(td):
def timedelta_to_seconds(td):
"""
Converts a 'datetime.timedelta' object `td` into seconds contained in
the duration.
Expand All @@ -347,6 +362,8 @@ def _timedelta_to_seconds(td):
"""
return timedelta_to_seconds(td)

_timedelta_to_seconds = timedelta_to_seconds # retrocompat

def _get_next(
self,
ret_type=None,
Expand Down Expand Up @@ -400,7 +417,7 @@ def _get_next(
# DST Handling for cron job spanning across days
dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
dtstarttime_utcoffset = dtstarttime.utcoffset() or datetime.timedelta(0)
dtresult = self._timestamp_to_datetime(result)
dtresult = self.timestamp_to_datetime(result)
lag = lag_hours = 0
# do we trigger DST on next crontab (handle backward changes)
dtresult_utcoffset = dtstarttime_utcoffset
Expand Down Expand Up @@ -490,7 +507,7 @@ def _calc(self, now, expanded, nth_weekday_of_month, is_prev):
sign = 1
offset = 1 if (len(expanded) > UNIX_CRON_LEN) else 60

dst = now = self._timestamp_to_datetime(now + sign * offset)
dst = now = self.timestamp_to_datetime(now + sign * offset)

month, year = dst.month, dst.year
current_year = now.year
Expand Down Expand Up @@ -693,7 +710,7 @@ def proc_second(d):
break
if next:
continue
return self._datetime_to_timestamp(dst.replace(microsecond=0))
return self.datetime_to_timestamp(dst.replace(microsecond=0))

if is_prev:
raise CroniterBadDateError("failed to find prev date")
Expand Down Expand Up @@ -766,10 +783,9 @@ def _get_prev_nearest_diff(x, to_check, range_val):
if c <= range_val:
candidate = c
break
# fix crontab "0 6 30 3 *" condidates only a element, then get_prev error return 2021-03-02 06:00:00
if candidate > range_val:
# fix crontab "0 6 30 3 *" condidates only a element,
# then get_prev error return 2021-03-02 06:00:00
return -x
return -range_val
return candidate - x - range_val

@staticmethod
Expand Down Expand Up @@ -824,7 +840,7 @@ def _expand(
}

efl = expr_format.lower()
hash_id_expr = hash_id is not None and 1 or 0
hash_id_expr = 1 if hash_id is not None else 0
try:
efl = expr_aliases[efl][hash_id_expr]
except KeyError:
Expand Down
2 changes: 2 additions & 0 deletions src/croniter/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ class TestCase(unittest.TestCase):
We use this base class for all the tests in this package.
If necessary, we can put common utility or setup code in here.
"""

maxDiff = 10 ** 10
98 changes: 46 additions & 52 deletions src/croniter/tests/test_croniter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
try:
import unittest2 as unittest
except ImportError:
import unittest

import unittest
from datetime import datetime, timedelta
from functools import partial
from time import sleep
Expand Down Expand Up @@ -2113,22 +2116,9 @@ def test_issue_2038y(self):
raise Exception("overflow not fixed!")

def test_revert_issue_90_aka_support_DOW7(self):
base = datetime(2040, 1, 1, 0, 0)
itr = croniter("* * * * 1-7").get_next()
self.assertTrue(croniter.is_valid("* * * * 1-7"))
self.assertTrue(croniter.is_valid("* * * * 7"))

def test_sunday_ranges_to(self):
self._test_sunday_ranges(
"0 0 * * Sun-Sun",
# fmt: off
[
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
],
# fmt: on
)

def test_sunday_ranges_to(self):
self._test_sunday_ranges(
"0 0 * * Sun-Sun",
Expand Down Expand Up @@ -2346,55 +2336,59 @@ def test_mth_ranges_from(self):
# fmt: on
)

def _test_cron_ranges(self, res_generator, expr, wanted, iterations=None, start_time=None):
rets = res_generator(expr, iterations=iterations, start_time=start_time)
def _test_cron_ranges(self, expr, wanted, generator=None, loops=None, start=None, is_prev=None):
rets = (generator or gen_x_results)(
expr, loops=loops or 10, start=start or datetime(2024, 1, 1), is_prev=is_prev
)
for ret in rets:
self.assertEqual(wanted, ret)

def _test_mth_cron_ranges(self, expr, wanted, iterations=None, res_generator=None, start_time=None):
def _test_mth_cron_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
return self._test_cron_ranges(
gen_x_mth_results,
expr,
wanted,
iterations=iterations,
start_time=start_time,
generator=gen_x_mth_results,
loops=loops or 16,
start=start,
is_prev=is_prev,
)

def _test_sunday_ranges(self, expr, wanted, iterations=None, start_time=None):
def _test_sunday_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
return self._test_cron_ranges(
gen_all_sunday_forms,
expr,
wanted,
iterations=iterations,
start_time=start_time,
)


def gen_x_mth_results(expr, iterations=None, start_time=None):
start_time = start_time or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start_time)
return [
[
"{0} {1}".format(str(a.year)[-2:], a.month)
for a in [cron.get_next(datetime) for i in range(iterations or 16)]
]
]


def gen_x_results(expr, iterations=None, start_time=None):
start_time = start_time or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start_time)
return [[a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]]


def gen_all_sunday_forms(expr, iterations=None, start_time=None):
start_time = start_time or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start_time)
ret1 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
cron = croniter(expr.lower().replace("sun", "7"), start_time=start_time)
ret2 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
cron = croniter(expr.lower().replace("sun", "0"), start_time=start_time)
ret3 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
generator=gen_all_sunday_forms,
loops=loops or 30,
start=start,
is_prev=is_prev,
)


def gen_x_mth_results(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
return [["{0} {1}".format(str(a.year)[-2:], a.month) for a in [n(datetime) for i in range(loops or 16)]]]


def gen_x_results(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
return [[a.isoformat() for a in [n(datetime) for i in range(loops or 30)]]]


def gen_all_sunday_forms(expr, loops=None, start=None, is_prev=None):
start = start or datetime(2024, 1, 1)
cron = croniter(expr, start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret1 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
cron = croniter(expr.lower().replace("sun", "7"), start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret2 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
cron = croniter(expr.lower().replace("sun", "0"), start_time=start)
n = cron.get_prev if is_prev else cron.get_next
ret3 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
return ret1, ret2, ret3


Expand Down
Loading
Loading