Skip to content

Commit

Permalink
Merge pull request #143 from kiorky/r6
Browse files Browse the repository at this point in the history
R6
  • Loading branch information
kiorky authored Dec 17, 2024
2 parents dbeeb1e + 3f6221b commit 2da6de2
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 304 deletions.
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

0 comments on commit 2da6de2

Please sign in to comment.