Skip to content

Commit

Permalink
Merge e5e00c6 into c57abf2
Browse files Browse the repository at this point in the history
  • Loading branch information
connorskees committed Jan 15, 2019
2 parents c57abf2 + e5e00c6 commit c327143
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 48 deletions.
91 changes: 61 additions & 30 deletions schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,22 @@
logger = logging.getLogger('schedule')


class ScheduleError(Exception):
"""Base schedule exception"""


class ScheduleValueError(ScheduleError):
"""Base schedule value error"""


class IntervalError(ScheduleValueError):
"""An improper interval was used"""


class CancelJob(object):
"""
Can be returned from a job to unschedule itself.
"""
pass


class Scheduler(object):
Expand Down Expand Up @@ -228,7 +239,8 @@ def format_time(t):

@property
def second(self):
assert self.interval == 1, 'Use seconds instead of second'
if self.interval != 1:
raise IntervalError('Use seconds instead of second')
return self.seconds

@property
Expand All @@ -238,7 +250,8 @@ def seconds(self):

@property
def minute(self):
assert self.interval == 1, 'Use minutes instead of minute'
if self.interval != 1:
raise IntervalError('Use minutes instead of minute')
return self.minutes

@property
Expand All @@ -248,7 +261,8 @@ def minutes(self):

@property
def hour(self):
assert self.interval == 1, 'Use hours instead of hour'
if self.interval != 1:
raise IntervalError('Use hours instead of hour')
return self.hours

@property
Expand All @@ -258,7 +272,8 @@ def hours(self):

@property
def day(self):
assert self.interval == 1, 'Use days instead of day'
if self.interval != 1:
raise IntervalError('Use days instead of day')
return self.days

@property
Expand All @@ -268,7 +283,8 @@ def days(self):

@property
def week(self):
assert self.interval == 1, 'Use weeks instead of week'
if self.interval != 1:
raise IntervalError('Use weeks instead of week')
return self.weeks

@property
Expand All @@ -278,43 +294,50 @@ def weeks(self):

@property
def monday(self):
assert self.interval == 1, 'Use mondays instead of monday'
if self.interval != 1:
raise IntervalError('Use mondays instead of monday')
self.start_day = 'monday'
return self.weeks

@property
def tuesday(self):
assert self.interval == 1, 'Use tuesdays instead of tuesday'
if self.interval != 1:
raise IntervalError('Use tuesdays instead of tuesday')
self.start_day = 'tuesday'
return self.weeks

@property
def wednesday(self):
assert self.interval == 1, 'Use wednesdays instead of wednesday'
if self.interval != 1:
raise IntervalError('Use wednesdays instead of wednesday')
self.start_day = 'wednesday'
return self.weeks

@property
def thursday(self):
assert self.interval == 1, 'Use thursdays instead of thursday'
if self.interval != 1:
raise IntervalError('Use thursdays instead of thursday')
self.start_day = 'thursday'
return self.weeks

@property
def friday(self):
assert self.interval == 1, 'Use fridays instead of friday'
if self.interval != 1:
raise IntervalError('Use fridays instead of friday')
self.start_day = 'friday'
return self.weeks

@property
def saturday(self):
assert self.interval == 1, 'Use saturdays instead of saturday'
if self.interval != 1:
raise IntervalError('Use saturdays instead of saturday')
self.start_day = 'saturday'
return self.weeks

@property
def sunday(self):
assert self.interval == 1, 'Use sundays instead of sunday'
if self.interval != 1:
raise IntervalError('Use sundays instead of sunday')
self.start_day = 'sunday'
return self.weeks

Expand Down Expand Up @@ -344,18 +367,22 @@ def at(self, time_str):
(e.g. `every().hour.at(':30')` vs. `every().minute.at(':30')`).
:return: The invoked job instance
"""
assert self.unit in ('days', 'hours', 'minutes') or self.start_day
if (self.unit not in ('days', 'hours', 'minutes')
and not self.start_day):
raise ScheduleValueError('Invalid unit.')
if not isinstance(time_str, str):
raise TypeError("at() should be passed a string.")
if self.unit == 'days' or self.start_day:
assert re.match(r'^([0-2]\d:)?[0-5]\d:[0-5]\d$', time_str), \
ValueError("Invalid time format.")
if not re.match(r'^([0-2]\d:)?[0-5]\d:[0-5]\d$', time_str):
raise ScheduleValueError("Invalid time format.")
if self.unit == 'hours':
assert re.match(r'^([0-5]\d)?:[0-5]\d$', time_str), \
ValueError("Invalid time format for an hourly job.")
if not re.match(r'^([0-5]\d)?:[0-5]\d$', time_str):
raise ScheduleValueError(("Invalid time format for"
" an hourly job."))
if self.unit == 'minutes':
assert re.match(r'^:[0-5]\d$', time_str), \
ValueError("Invalid time format for a minutely job.")
if not re.match(r'^:[0-5]\d$', time_str):
raise ScheduleValueError(("Invalid time format for"
" a minutely job."))
time_values = time_str.split(':')
if len(time_values) == 3:
hour, minute, second = time_values
Expand All @@ -368,16 +395,15 @@ def at(self, time_str):
second = 0
if self.unit == 'days' or self.start_day:
hour = int(hour)
assert 0 <= hour <= 23
if 0 > hour or hour > 24:
raise ScheduleValueError("Invalid number of hours.")
elif self.unit == 'hours':
hour = 0
elif self.unit == 'minutes':
hour = 0
minute = 0
minute = int(minute)
assert 0 <= minute <= 59
second = int(second)
assert 0 <= second <= 59
self.at_time = datetime.time(hour, minute, second)
return self

Expand Down Expand Up @@ -442,19 +468,21 @@ def _schedule_next_run(self):
"""
Compute the instant when this job should run next.
"""
assert self.unit in ('seconds', 'minutes', 'hours', 'days',
'weeks')
if self.unit not in ('seconds', 'minutes', 'hours', 'days', 'weeks'):
raise ScheduleValueError("Invalid unit.")

if self.latest is not None:
assert self.latest >= self.interval
if not (self.latest >= self.interval):
raise ScheduleError
interval = random.randint(self.interval, self.latest)
else:
interval = self.interval

self.period = datetime.timedelta(**{self.unit: interval})
self.next_run = datetime.datetime.now() + self.period
if self.start_day is not None:
assert self.unit == 'weeks'
if self.unit != 'weeks':
raise ScheduleValueError("`unit` should be 'weeks'")
weekdays = (
'monday',
'tuesday',
Expand All @@ -464,15 +492,18 @@ def _schedule_next_run(self):
'saturday',
'sunday'
)
assert self.start_day in weekdays
if self.start_day not in weekdays:
raise ScheduleValueError("Invalid start day.")
weekday = weekdays.index(self.start_day)
days_ahead = weekday - self.next_run.weekday()
if days_ahead <= 0: # Target day already happened this week
days_ahead += 7
self.next_run += datetime.timedelta(days_ahead) - self.period
if self.at_time is not None:
assert self.unit in ('days', 'hours', 'minutes') \
or self.start_day is not None
if (self.unit not in ('days', 'hours', 'minutes')
and self.start_day is None):
raise ScheduleValueError(("Invalid unit without"
" specifying start day."))
kwargs = {
'second': self.at_time.second,
'microsecond': 0
Expand Down
129 changes: 111 additions & 18 deletions test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# pylint: disable-msg=R0201,C0111,E0102,R0904,R0901

import schedule
from schedule import every
from schedule import every, ScheduleValueError, IntervalError


def make_mock_job(name=None):
Expand Down Expand Up @@ -58,6 +58,68 @@ def test_time_units(self):
assert every().days.unit == 'days'
assert every().weeks.unit == 'weeks'

job_instance = schedule.Job(interval=2)
# without a context manager, it incorrectly raises an error because
# it is not callable
with self.assertRaises(IntervalError):
job_instance.minute
with self.assertRaises(IntervalError):
job_instance.hour
with self.assertRaises(IntervalError):
job_instance.day
with self.assertRaises(IntervalError):
job_instance.week
with self.assertRaises(IntervalError):
job_instance.monday
with self.assertRaises(IntervalError):
job_instance.tuesday
with self.assertRaises(IntervalError):
job_instance.wednesday
with self.assertRaises(IntervalError):
job_instance.thursday
with self.assertRaises(IntervalError):
job_instance.friday
with self.assertRaises(IntervalError):
job_instance.saturday
with self.assertRaises(IntervalError):
job_instance.sunday

# test an invalid unit
job_instance.unit = "foo"
self.assertRaises(ScheduleValueError, job_instance.at, "1:0:0")
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)

# test start day exists but unit is not 'weeks'
job_instance.unit = "days"
job_instance.start_day = 1
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)

# test weeks with an invalid start day
job_instance.unit = "weeks"
job_instance.start_day = "bar"
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)

# test a valid unit with invalid hours/minutes/seconds
job_instance.unit = "days"
self.assertRaises(ScheduleValueError, job_instance.at, "25:00:00")
self.assertRaises(ScheduleValueError, job_instance.at, "00:61:00")
self.assertRaises(ScheduleValueError, job_instance.at, "00:00:61")

# test invalid time format
self.assertRaises(ScheduleValueError, job_instance.at, "25:0:0")
self.assertRaises(ScheduleValueError, job_instance.at, "0:61:0")
self.assertRaises(ScheduleValueError, job_instance.at, "0:0:61")

# test (very specific) seconds with unspecified start_day
job_instance.unit = "seconds"
job_instance.at_time = datetime.datetime.now()
job_instance.start_day = None
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)

# test self.latest >= self.interval
job_instance.latest = 3
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)

def test_singular_time_units_match_plural_units(self):
assert every().second.unit == every().seconds.unit
assert every().minute.unit == every().minutes.unit
Expand Down Expand Up @@ -94,14 +156,42 @@ def test_at_time(self):
assert every().day.at('10:30').do(mock_job).next_run.minute == 30
assert every().day.at('10:30:50').do(mock_job).next_run.second == 50

self.assertRaises(AssertionError, every().day.at, '2:30:000001')
self.assertRaises(AssertionError, every().day.at, '::2')
self.assertRaises(AssertionError, every().day.at, '.2')
self.assertRaises(AssertionError, every().day.at, '2')
self.assertRaises(AssertionError, every().day.at, ':2')
self.assertRaises(AssertionError, every().day.at, ' 2:30:00')
self.assertRaises(ScheduleValueError, every().day.at, '2:30:000001')
self.assertRaises(ScheduleValueError, every().day.at, '::2')
self.assertRaises(ScheduleValueError, every().day.at, '.2')
self.assertRaises(ScheduleValueError, every().day.at, '2')
self.assertRaises(ScheduleValueError, every().day.at, ':2')
self.assertRaises(ScheduleValueError, every().day.at, ' 2:30:00')
self.assertRaises(ScheduleValueError, every().do, lambda: 0)
self.assertRaises(TypeError, every().day.at, 2)

# without a context manager, it incorrectly raises an error because
# it is not callable
with self.assertRaises(IntervalError):
every(interval=2).second
with self.assertRaises(IntervalError):
every(interval=2).minute
with self.assertRaises(IntervalError):
every(interval=2).hour
with self.assertRaises(IntervalError):
every(interval=2).day
with self.assertRaises(IntervalError):
every(interval=2).week
with self.assertRaises(IntervalError):
every(interval=2).monday
with self.assertRaises(IntervalError):
every(interval=2).tuesday
with self.assertRaises(IntervalError):
every(interval=2).wednesday
with self.assertRaises(IntervalError):
every(interval=2).thursday
with self.assertRaises(IntervalError):
every(interval=2).friday
with self.assertRaises(IntervalError):
every(interval=2).saturday
with self.assertRaises(IntervalError):
every(interval=2).sunday

def test_at_time_hour(self):
with mock_datetime(2010, 1, 6, 12, 20):
mock_job = make_mock_job()
Expand All @@ -115,11 +205,14 @@ def test_at_time_hour(self):
assert every().hour.at(':00').do(mock_job).next_run.minute == 0
assert every().hour.at(':00').do(mock_job).next_run.second == 0

self.assertRaises(AssertionError, every().hour.at, '2:30:00')
self.assertRaises(AssertionError, every().hour.at, '::2')
self.assertRaises(AssertionError, every().hour.at, '.2')
self.assertRaises(AssertionError, every().hour.at, '2')
self.assertRaises(AssertionError, every().hour.at, ' 2:30')
self.assertRaises(ScheduleValueError, every().hour.at, '2:30:00')
self.assertRaises(ScheduleValueError, every().hour.at, '::2')
self.assertRaises(ScheduleValueError, every().hour.at, '.2')
self.assertRaises(ScheduleValueError, every().hour.at, '2')
self.assertRaises(ScheduleValueError, every().hour.at, ' 2:30')
self.assertRaises(ScheduleValueError, every().hour.at, "61:00")
self.assertRaises(ScheduleValueError, every().hour.at, "00:61")
self.assertRaises(ScheduleValueError, every().hour.at, "01:61")
self.assertRaises(TypeError, every().hour.at, 2)

def test_at_time_minute(self):
Expand All @@ -132,12 +225,12 @@ def test_at_time_minute(self):
assert every().minute.at(':10').do(mock_job).next_run.minute == 21
assert every().minute.at(':10').do(mock_job).next_run.second == 10

self.assertRaises(AssertionError, every().minute.at, '::2')
self.assertRaises(AssertionError, every().minute.at, '.2')
self.assertRaises(AssertionError, every().minute.at, '2')
self.assertRaises(AssertionError, every().minute.at, '2:30:00')
self.assertRaises(AssertionError, every().minute.at, '2:30')
self.assertRaises(AssertionError, every().minute.at, ' :30')
self.assertRaises(ScheduleValueError, every().minute.at, '::2')
self.assertRaises(ScheduleValueError, every().minute.at, '.2')
self.assertRaises(ScheduleValueError, every().minute.at, '2')
self.assertRaises(ScheduleValueError, every().minute.at, '2:30:00')
self.assertRaises(ScheduleValueError, every().minute.at, '2:30')
self.assertRaises(ScheduleValueError, every().minute.at, ' :30')
self.assertRaises(TypeError, every().minute.at, 2)

def test_next_run_time(self):
Expand Down

0 comments on commit c327143

Please sign in to comment.