Skip to content

Commit 2da6de2

Browse files
authored
Merge pull request #143 from kiorky/r6
R6
2 parents dbeeb1e + 3f6221b commit 2da6de2

11 files changed

+326
-304
lines changed

CHANGELOG.rst

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
Changelog
22
==============
33

4-
5.0.2 (unreleased)
4+
6.0.0 (unreleased)
55
------------------
6-
6+
- Rework timestamp_to_datetime to use whatever timezone [kiorky]
7+
- Make datetime_to_timestamp & timestamp_to_datetime public [kiorky]
8+
- Fix EPOCH calculation in case of non UTC & 32 bits based systems [kiorky]
79
- Apply isort formatter [kiorky]
10+
- Reintegrate test_speed [kiorky]
811
- Apply black formatter [evanpurkhiser, kiorky]
9-
- Code quality changes by evanpurkhiser:
12+
- Code quality changes [evanpurkhiser, kiorky]
1013
- Remove unused _get_caller_globals_and_locals [evanpurkhiser]
1114
- Remove single-use bad_length [evanpurkhiser]
1215
- Remove unused `days` in `proc_month` [evanpurkhiser]

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def read(*rnames):
2424

2525
setup(
2626
name='croniter',
27-
version='5.0.2.dev0',
27+
version='6.0.0.dev0',
2828
py_modules=['croniter', ],
2929
description=(
3030
'croniter provides iteration for datetime '

src/croniter/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import absolute_import
33

4+
from . import croniter as cron_m
45
from .croniter import (
6+
DAY_FIELD,
7+
HOUR_FIELD,
8+
MINUTE_FIELD,
9+
MONTH_FIELD,
510
OVERFLOW32B_MODE,
11+
SECOND_FIELD,
12+
UTC_DT,
13+
YEAR_FIELD,
614
CroniterBadCronError,
715
CroniterBadDateError,
816
CroniterBadTypeRangeError,
17+
CroniterError,
918
CroniterNotAlphaError,
1019
CroniterUnsupportedSyntaxError,
1120
croniter,

src/croniter/croniter.py

+39-23
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ def is_32bit():
7373
OrderedDict = dict # py26 degraded mode, expanders order will not be immutable
7474

7575

76-
EPOCH = datetime.datetime.fromtimestamp(0)
76+
try:
77+
# py3 recent
78+
UTC_DT = datetime.timezone.utc
79+
except AttributeError:
80+
UTC_DT = pytz.utc
81+
EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT)
7782

7883
# fmt: off
7984
M_ALPHAS = {
@@ -126,12 +131,9 @@ def is_32bit():
126131
YEAR_CRON_LEN = len(YEAR_FIELDS)
127132
# retrocompat
128133
VALID_LEN_EXPRESSION = set(a for a in CRON_FIELDS if isinstance(a, int))
134+
TIMESTAMP_TO_DT_CACHE = {}
129135
EXPRESSIONS = {}
130-
try:
131-
# py3 recent
132-
UTC_DT = datetime.timezone.utc
133-
except AttributeError:
134-
UTC_DT = pytz.utc
136+
MARKER = object()
135137

136138

137139
def timedelta_to_seconds(td):
@@ -301,44 +303,57 @@ def get_prev(self, ret_type=None, start_time=None, update_current=True):
301303
def get_current(self, ret_type=None):
302304
ret_type = ret_type or self._ret_type
303305
if issubclass(ret_type, datetime.datetime):
304-
return self._timestamp_to_datetime(self.cur)
306+
return self.timestamp_to_datetime(self.cur)
305307
return self.cur
306308

307309
def set_current(self, start_time, force=True):
308310
if (force or (self.cur is None)) and start_time is not None:
309311
if isinstance(start_time, datetime.datetime):
310312
self.tzinfo = start_time.tzinfo
311-
start_time = self._datetime_to_timestamp(start_time)
313+
start_time = self.datetime_to_timestamp(start_time)
312314

313315
self.start_time = start_time
314316
self.dst_start_time = start_time
315317
self.cur = start_time
316318
return self.cur
317319

318320
@staticmethod
319-
def _datetime_to_timestamp(d):
321+
def datetime_to_timestamp(d):
320322
"""
321323
Converts a `datetime` object `d` into a UNIX timestamp.
322324
"""
323325
return datetime_to_timestamp(d)
324326

325-
def _timestamp_to_datetime(self, timestamp):
327+
_datetime_to_timestamp = datetime_to_timestamp # retrocompat
328+
329+
def timestamp_to_datetime(self, timestamp, tzinfo=MARKER):
326330
"""
327-
Converts a UNIX timestamp `timestamp` into a `datetime` object.
331+
Converts a UNIX `timestamp` into a `datetime` object.
328332
"""
333+
if tzinfo is MARKER: # allow to give tzinfo=None even if self.tzinfo is set
334+
tzinfo = self.tzinfo
335+
k = timestamp
336+
if tzinfo:
337+
k = (timestamp, repr(tzinfo))
338+
try:
339+
return TIMESTAMP_TO_DT_CACHE[k]
340+
except KeyError:
341+
pass
329342
if OVERFLOW32B_MODE:
330343
# degraded mode to workaround Y2038
331344
# see https://github.com/python/cpython/issues/101069
332-
result = EPOCH + datetime.timedelta(seconds=timestamp)
345+
result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
333346
else:
334347
result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
335-
if self.tzinfo:
336-
result = result.replace(tzinfo=tzutc()).astimezone(self.tzinfo)
337-
348+
if tzinfo:
349+
result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
350+
TIMESTAMP_TO_DT_CACHE[(result, repr(result.tzinfo))] = result
338351
return result
339352

353+
_timestamp_to_datetime = timestamp_to_datetime # retrocompat
354+
340355
@staticmethod
341-
def _timedelta_to_seconds(td):
356+
def timedelta_to_seconds(td):
342357
"""
343358
Converts a 'datetime.timedelta' object `td` into seconds contained in
344359
the duration.
@@ -347,6 +362,8 @@ def _timedelta_to_seconds(td):
347362
"""
348363
return timedelta_to_seconds(td)
349364

365+
_timedelta_to_seconds = timedelta_to_seconds # retrocompat
366+
350367
def _get_next(
351368
self,
352369
ret_type=None,
@@ -400,7 +417,7 @@ def _get_next(
400417
# DST Handling for cron job spanning across days
401418
dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
402419
dtstarttime_utcoffset = dtstarttime.utcoffset() or datetime.timedelta(0)
403-
dtresult = self._timestamp_to_datetime(result)
420+
dtresult = self.timestamp_to_datetime(result)
404421
lag = lag_hours = 0
405422
# do we trigger DST on next crontab (handle backward changes)
406423
dtresult_utcoffset = dtstarttime_utcoffset
@@ -490,7 +507,7 @@ def _calc(self, now, expanded, nth_weekday_of_month, is_prev):
490507
sign = 1
491508
offset = 1 if (len(expanded) > UNIX_CRON_LEN) else 60
492509

493-
dst = now = self._timestamp_to_datetime(now + sign * offset)
510+
dst = now = self.timestamp_to_datetime(now + sign * offset)
494511

495512
month, year = dst.month, dst.year
496513
current_year = now.year
@@ -693,7 +710,7 @@ def proc_second(d):
693710
break
694711
if next:
695712
continue
696-
return self._datetime_to_timestamp(dst.replace(microsecond=0))
713+
return self.datetime_to_timestamp(dst.replace(microsecond=0))
697714

698715
if is_prev:
699716
raise CroniterBadDateError("failed to find prev date")
@@ -766,10 +783,9 @@ def _get_prev_nearest_diff(x, to_check, range_val):
766783
if c <= range_val:
767784
candidate = c
768785
break
786+
# fix crontab "0 6 30 3 *" condidates only a element, then get_prev error return 2021-03-02 06:00:00
769787
if candidate > range_val:
770-
# fix crontab "0 6 30 3 *" condidates only a element,
771-
# then get_prev error return 2021-03-02 06:00:00
772-
return -x
788+
return -range_val
773789
return candidate - x - range_val
774790

775791
@staticmethod
@@ -824,7 +840,7 @@ def _expand(
824840
}
825841

826842
efl = expr_format.lower()
827-
hash_id_expr = hash_id is not None and 1 or 0
843+
hash_id_expr = 1 if hash_id is not None else 0
828844
try:
829845
efl = expr_aliases[efl][hash_id_expr]
830846
except KeyError:

src/croniter/tests/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ class TestCase(unittest.TestCase):
99
We use this base class for all the tests in this package.
1010
If necessary, we can put common utility or setup code in here.
1111
"""
12+
13+
maxDiff = 10 ** 10

src/croniter/tests/test_croniter.py

+46-52
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3+
try:
4+
import unittest2 as unittest
5+
except ImportError:
6+
import unittest
37

4-
import unittest
58
from datetime import datetime, timedelta
69
from functools import partial
710
from time import sleep
@@ -2113,22 +2116,9 @@ def test_issue_2038y(self):
21132116
raise Exception("overflow not fixed!")
21142117

21152118
def test_revert_issue_90_aka_support_DOW7(self):
2116-
base = datetime(2040, 1, 1, 0, 0)
2117-
itr = croniter("* * * * 1-7").get_next()
21182119
self.assertTrue(croniter.is_valid("* * * * 1-7"))
21192120
self.assertTrue(croniter.is_valid("* * * * 7"))
21202121

2121-
def test_sunday_ranges_to(self):
2122-
self._test_sunday_ranges(
2123-
"0 0 * * Sun-Sun",
2124-
# fmt: off
2125-
[
2126-
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
2127-
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2128-
],
2129-
# fmt: on
2130-
)
2131-
21322122
def test_sunday_ranges_to(self):
21332123
self._test_sunday_ranges(
21342124
"0 0 * * Sun-Sun",
@@ -2346,55 +2336,59 @@ def test_mth_ranges_from(self):
23462336
# fmt: on
23472337
)
23482338

2349-
def _test_cron_ranges(self, res_generator, expr, wanted, iterations=None, start_time=None):
2350-
rets = res_generator(expr, iterations=iterations, start_time=start_time)
2339+
def _test_cron_ranges(self, expr, wanted, generator=None, loops=None, start=None, is_prev=None):
2340+
rets = (generator or gen_x_results)(
2341+
expr, loops=loops or 10, start=start or datetime(2024, 1, 1), is_prev=is_prev
2342+
)
23512343
for ret in rets:
23522344
self.assertEqual(wanted, ret)
23532345

2354-
def _test_mth_cron_ranges(self, expr, wanted, iterations=None, res_generator=None, start_time=None):
2346+
def _test_mth_cron_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
23552347
return self._test_cron_ranges(
2356-
gen_x_mth_results,
23572348
expr,
23582349
wanted,
2359-
iterations=iterations,
2360-
start_time=start_time,
2350+
generator=gen_x_mth_results,
2351+
loops=loops or 16,
2352+
start=start,
2353+
is_prev=is_prev,
23612354
)
23622355

2363-
def _test_sunday_ranges(self, expr, wanted, iterations=None, start_time=None):
2356+
def _test_sunday_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
23642357
return self._test_cron_ranges(
2365-
gen_all_sunday_forms,
23662358
expr,
23672359
wanted,
2368-
iterations=iterations,
2369-
start_time=start_time,
2370-
)
2371-
2372-
2373-
def gen_x_mth_results(expr, iterations=None, start_time=None):
2374-
start_time = start_time or datetime(2024, 1, 1)
2375-
cron = croniter(expr, start_time=start_time)
2376-
return [
2377-
[
2378-
"{0} {1}".format(str(a.year)[-2:], a.month)
2379-
for a in [cron.get_next(datetime) for i in range(iterations or 16)]
2380-
]
2381-
]
2382-
2383-
2384-
def gen_x_results(expr, iterations=None, start_time=None):
2385-
start_time = start_time or datetime(2024, 1, 1)
2386-
cron = croniter(expr, start_time=start_time)
2387-
return [[a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]]
2388-
2389-
2390-
def gen_all_sunday_forms(expr, iterations=None, start_time=None):
2391-
start_time = start_time or datetime(2024, 1, 1)
2392-
cron = croniter(expr, start_time=start_time)
2393-
ret1 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
2394-
cron = croniter(expr.lower().replace("sun", "7"), start_time=start_time)
2395-
ret2 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
2396-
cron = croniter(expr.lower().replace("sun", "0"), start_time=start_time)
2397-
ret3 = [a.day for a in [cron.get_next(datetime) for i in range(iterations or 30)]]
2360+
generator=gen_all_sunday_forms,
2361+
loops=loops or 30,
2362+
start=start,
2363+
is_prev=is_prev,
2364+
)
2365+
2366+
2367+
def gen_x_mth_results(expr, loops=None, start=None, is_prev=None):
2368+
start = start or datetime(2024, 1, 1)
2369+
cron = croniter(expr, start_time=start)
2370+
n = cron.get_prev if is_prev else cron.get_next
2371+
return [["{0} {1}".format(str(a.year)[-2:], a.month) for a in [n(datetime) for i in range(loops or 16)]]]
2372+
2373+
2374+
def gen_x_results(expr, loops=None, start=None, is_prev=None):
2375+
start = start or datetime(2024, 1, 1)
2376+
cron = croniter(expr, start_time=start)
2377+
n = cron.get_prev if is_prev else cron.get_next
2378+
return [[a.isoformat() for a in [n(datetime) for i in range(loops or 30)]]]
2379+
2380+
2381+
def gen_all_sunday_forms(expr, loops=None, start=None, is_prev=None):
2382+
start = start or datetime(2024, 1, 1)
2383+
cron = croniter(expr, start_time=start)
2384+
n = cron.get_prev if is_prev else cron.get_next
2385+
ret1 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
2386+
cron = croniter(expr.lower().replace("sun", "7"), start_time=start)
2387+
n = cron.get_prev if is_prev else cron.get_next
2388+
ret2 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
2389+
cron = croniter(expr.lower().replace("sun", "0"), start_time=start)
2390+
n = cron.get_prev if is_prev else cron.get_next
2391+
ret3 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
23982392
return ret1, ret2, ret3
23992393

24002394

0 commit comments

Comments
 (0)