Skip to content

Commit 5ea8746

Browse files
committed
v3/as_drivers/sched Improve schedule() arg pattern.
1 parent c6890ae commit 5ea8746

File tree

3 files changed

+139
-125
lines changed

3 files changed

+139
-125
lines changed

v3/as_drivers/sched/asynctest.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import uasyncio as asyncio
77
from sched.sched import schedule
8-
from sched.cron import cron
98
from time import localtime
109

1110
def foo(txt): # Demonstrate callback
@@ -21,17 +20,14 @@ async def bar(txt): # Demonstrate coro launch
2120

2221
async def main():
2322
print('Asynchronous test running...')
24-
cron4 = cron(hrs=None, mins=range(0, 60, 4))
25-
asyncio.create_task(schedule(cron4, foo, ('every 4 mins',)))
23+
asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
2624

27-
cron5 = cron(hrs=None, mins=range(0, 60, 5))
28-
asyncio.create_task(schedule(cron5, foo, ('every 5 mins',)))
25+
asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5)))
2926

30-
cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine
31-
asyncio.create_task(schedule(cron3, bar, ('every 3 mins',)))
27+
# Launch a coroutine
28+
asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3)))
3229

33-
cron2 = cron(hrs=None, mins=range(0, 60, 2))
34-
asyncio.create_task(schedule(cron2, foo, ('one shot',), True))
30+
asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1))
3531
await asyncio.sleep(900) # Quit after 15 minutes
3632

3733
try:

v3/as_drivers/sched/sched.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
import uasyncio as asyncio
77
from sched.primitives import launch
88
from time import time
9+
from sched.cron import cron
910

10-
async def schedule(fcron, routine, args=(), run_once=False):
11+
async def schedule(func, *args, times=None, **kwargs):
12+
fcron = cron(**kwargs)
1113
maxt = 1000 # uasyncio can't handle arbitrarily long delays
12-
done = False
13-
while not done:
14+
while times is None or times > 0:
1415
tw = fcron(int(time())) # Time to wait (s)
1516
while tw > 0: # While there is still time to wait
1617
tw = min(tw, maxt)
1718
await asyncio.sleep(tw)
1819
tw -= maxt
19-
launch(routine, args)
20-
done = run_once
20+
launch(func, args)
21+
if times is not None:
22+
times -= 1
2123
await asyncio.sleep_ms(1200) # ensure we're into next second

v3/docs/SCHEDULE.md

Lines changed: 127 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks)
44
2. [Overview](./SCHEDULE.md#2-overview)
55
3. [Installation](./SCHEDULE.md#3-installation)
6-
4. [The cron object](./SCHEDULE.md#4-the-cron-object) How to specify times and dates
6+
4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for uasyncio
77
4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers)
8-
4.2 [The time to an event](./SCHEDULE.md#42-the-time-to-an-event)
9-
4.3 [How it works](./SCHEDULE.md#43-how-it-works)
10-
4.4 [Calendar behaviour](./SCHEDULE.md#44-calendar-behaviour)
11-
4.5 [Limitations](./SCHEDULE.md#45-limitations)
12-
4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build)
13-
5. [The schedule function](./SCHEDULE.md#5-the-schedule-function) The primary interface for uasyncio
8+
4.2 [Calendar behaviour](./SCHEDULE.md#42-calendar-behaviour) Calendars can be tricky...
9+
     4.2.1 [Behaviour of mday and wday values](./SCHEDULE.md#421-behaviour-of-mday-and-wday-values)
10+
     4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover)
11+
4.3 [Limitations](./SCHEDULE.md#43-limitations)
12+
4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build)
13+
5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders
14+
5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event)
15+
5.2 [How it works](./SCHEDULE.md#52-how-it-works)
1416
6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations)
1517
7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must
1618

@@ -39,15 +41,18 @@ and the Unix build (the latter is subject to a minor local time issue).
3941

4042
# 2. Overview
4143

42-
There are two components, the `cron` object (in `sched/cron.py`) and the
43-
`schedule` function (in `sched/sched.py`). The user creates `cron` instances,
44-
passing arguments specifying time intervals. The `cron` instance may be run at
45-
any time and will return the time in seconds to the next scheduled event.
44+
The `schedule` function (`sched/sched.py`) is the interface for use with
45+
`uasyncio`. The function takes a callback and causes that callback to run at
46+
specified times. A coroutine may be substituted for the callback - at the
47+
specified times it will be promoted to a `Task` and run.
4648

47-
The `schedule` function is an optional component for use with `uasyncio`. The
48-
function takes a `cron` instance and a callback and causes that callback to run
49-
at the times specified by the `cron`. A coroutine may be substituted for the
50-
callback - at the specified times it will be promoted to a `Task` and run.
49+
The `schedule` function instantiates a `cron` object (in `sched/cron.py`). This
50+
is the core of the scheduler: it is a closure created with a time specifier and
51+
returning the time to the next scheduled event. Users of `uasyncio` do not need
52+
to deal with `cron` instances.
53+
54+
This library can also be used in synchronous code, in which case `cron`
55+
instances must explicitly be created.
5156

5257
##### [Top](./SCHEDULE.md#0-contents)
5358

@@ -77,21 +82,67 @@ The `crontest` script is only of interest to those wishing to adapt `cron.py`.
7782
To run error-free a bare metal target should be used for the reason discussed
7883
[here](./SCHEDULE.md#46-the-unix-build).
7984

80-
# 4. The cron object
85+
# 4. The schedule function
8186

82-
This is a closure. It accepts a time specification for future events. Each call
83-
when passed the current time returns the number of seconds to wait for the next
84-
event to occur.
87+
This enables a callback or coroutine to be run at intervals. The callable can
88+
be specified to run once only. `schedule` is an asynchronous function.
8589

86-
It takes the following keyword-only args. A flexible set of data types are
87-
accepted. These are known as `Time specifiers` and described below. Valid
88-
numbers are shown as inclusive ranges.
90+
Positional args:
91+
1. `func` The callable (callback or coroutine) to run.
92+
2. Any further positional args are passed to the callable.
93+
94+
Keyword-only args. Args 1..6 are
95+
[Time specifiers](./SCHEDULE.md#41-time-specifiers): a variety of data types
96+
may be passed, but all ultimately produce integers (or `None`). Valid numbers
97+
are shown as inclusive ranges.
8998
1. `secs=0` Seconds (0..59).
9099
2. `mins=0` Minutes (0..59).
91100
3. `hrs=3` Hours (0..23).
92101
4. `mday=None` Day of month (1..31).
93102
5. `month=None` Months (1..12).
94103
6. `wday=None` Weekday (0..6 Mon..Sun).
104+
7. `times=None` If an integer `n` is passed the callable will be run at the
105+
next `n` scheduled times. Hence a value of 1 specifies a one-shot event.
106+
107+
The `schedule` function only terminates if `times` is not `None`, and then
108+
typically after a long time. Consequently `schedule` is usually started with
109+
`asyncio.create_task`, as in the following example where a callback is
110+
scheduled at various times. The code below may be run by issuing
111+
```python
112+
import sched.asynctest
113+
```
114+
This is the demo code.
115+
```python
116+
import uasyncio as asyncio
117+
from sched.sched import schedule
118+
from time import localtime
119+
120+
def foo(txt): # Demonstrate callback
121+
yr, mo, md, h, m, s, wd = localtime()[:7]
122+
fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
123+
print(fst.format(txt, h, m, s, md, mo, yr))
124+
125+
async def bar(txt): # Demonstrate coro launch
126+
yr, mo, md, h, m, s, wd = localtime()[:7]
127+
fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
128+
print(fst.format(txt, h, m, s, md, mo, yr))
129+
await asyncio.sleep(0)
130+
131+
async def main():
132+
print('Asynchronous test running...')
133+
asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
134+
asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5)))
135+
# Launch a coroutine
136+
asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3)))
137+
# Launch a one-shot task
138+
asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1))
139+
await asyncio.sleep(900) # Quit after 15 minutes
140+
141+
try:
142+
asyncio.run(main())
143+
finally:
144+
_ = asyncio.new_event_loop()
145+
```
95146

96147
##### [Top](./SCHEDULE.md#0-contents)
97148

@@ -105,8 +156,8 @@ The args may be of the following types.
105156
`wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be
106157
passed.
107158

108-
Legal ranges are listed above. Basic validation is done when a `cron` is
109-
instantiated.
159+
Legal integer values are listed above. Basic validation is done as soon as
160+
`schedule` is run.
110161

111162
Note the implications of the `None` wildcard. Setting `mins=None` will schedule
112163
the event to occur on every minute (equivalent to `*` in a Unix cron table).
@@ -115,38 +166,13 @@ events must be at least two seconds apart.
115166

116167
Default values schedule an event every day at 03.00.00.
117168

118-
## 4.2 The time to an event
119-
120-
When the `cron` instance is run, it must be passed a time value (normally the
121-
time now as returned by `time.time()`). The instance returns the number of
122-
seconds to the first event matching the specifier.
123-
124-
```python
125-
from sched.cron import cron
126-
cron1 = cron(hrs=None, mins=range(0, 60, 15)) # Every 15 minutes of every day
127-
cron2 = cron(mday=25, month=12, hrs=9) # 9am every Christmas day
128-
cron3 = cron(wday=(0, 4)) # 3am every Monday and Friday
129-
now = int(time.time()) # Unix build returns a float here
130-
tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event
131-
```
132-
133-
##### [Top](./SCHEDULE.md#0-contents)
134-
135-
## 4.3 How it works
136-
137-
When a cron instance is run it seeks a future time and date relative to the
138-
passed time value. This will be the soonest matching the specifier. A `cron`
139-
instance is a conventional function and does not store state. Repeated calls
140-
will return the same value if passed the same time value (`now` in the above
141-
example).
142-
143-
## 4.4 Calendar behaviour
169+
## 4.2 Calendar behaviour
144170

145171
Specifying a day in the month which exceeds the length of a specified month
146172
(e.g. `month=(2, 6, 7), mday=30`) will produce a `ValueError`. February is
147173
assumed to have 28 days.
148174

149-
### 4.4.1 Behaviour of mday and wday values
175+
### 4.2.1 Behaviour of mday and wday values
150176

151177
The following describes how to schedule something for (say) the second Sunday
152178
in a month, plus limitations of doing this.
@@ -164,30 +190,30 @@ Specifying `wday=d` and `mday=n` where n > 22 could result in a day beyond the
164190
end of the month. It's not obvious what constitutes rational behaviour in this
165191
pathological corner case. Validation will throw a `ValueError` in this case.
166192

167-
### 4.4.2 Time causing month rollover
193+
### 4.2.2 Time causing month rollover
168194

169195
The following describes behaviour which I consider correct.
170196

171197
On the last day of the month there are circumstances where a time specifier can
172-
cause a day rollover. Consider application start. If a `cron` is run whose time
173-
specifier provides only times prior to the current time, its month increments
174-
and the day changes to the 1st. This is the soonest that the event can occur at
175-
the specified time.
198+
cause a day rollover. Consider application start. If a callback is scheduled
199+
with a time specifier offering only times prior to the current time, its month
200+
increments and the day changes to the 1st. This is the soonest that the event
201+
can occur at the specified time.
176202

177203
Consider the case where the next month is disallowed. In this case the month
178204
will change to the next valid month. This code, run at 9am on 31st July, would
179-
aim to run the event at 1.59 on 1st October.
205+
aim to run `foo` at 1.59 on 1st October.
180206
```python
181-
my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day
182-
t_wait = my_cron(time.time()) # Next month is disallowed so jumps to October
207+
asyncio.create_task(schedule(foo, month=(2, 7, 10), hrs=1, mins=59))
183208
```
184209

185210
##### [Top](./SCHEDULE.md#0-contents)
186211

187-
## 4.5 Limitations
212+
## 4.3 Limitations
188213

189-
The `cron` code has a resolution of 1 second. It is intended for scheduling
190-
infrequent events (`uasyncio` is recommended for doing fast scheduling).
214+
The underlying `cron` code has a resolution of 1 second. The library is
215+
intended for scheduling infrequent events (`uasyncio` has its own approach to
216+
fast scheduling).
191217

192218
Specifying `secs=None` will cause a `ValueError`. The minimum interval between
193219
scheduled events is 2 seconds. Attempts to schedule events with a shorter gap
@@ -200,7 +226,7 @@ On hardware platforms the MicroPython `time` module does not handle daylight
200226
saving time. Scheduled times are relative to system time. This does not apply
201227
to the Unix build where daylight saving needs to be considered.
202228

203-
## 4.6 The Unix build
229+
## 4.4 The Unix build
204230

205231
Asynchronous use requires `uasyncio` V3, so ensure this is installed on the
206232
Linux target.
@@ -221,61 +247,50 @@ metal targets.
221247

222248
##### [Top](./SCHEDULE.md#0-contents)
223249

224-
# 5. The schedule function
250+
# 5. The cron object
225251

226-
This enables a callback or coroutine to be run at intervals specified by a
227-
`cron` instance. An option for one-shot use is available. It is an asynchronous
228-
function. Positional args:
229-
1. `fcron` A `cron` instance.
230-
2. `routine` The callable (callback or coroutine) to run.
231-
3. `args=()` A tuple of args for the callable.
232-
4. `run_once=False` If `True` the callable will be run once only.
252+
This is the core of the scheduler. Users of `uasyncio` do not need to concern
253+
themseleves with it. It is documented for those wishing to modify the code and
254+
for those wanting to perform scheduling in synchronous code.
233255

234-
The `schedule` function only terminates if `run_once=True`, and then typically
235-
after a long time. Usually `schedule` is started with `asyncio.create_task`, as
236-
in the following example where a callback is scheduled at various times. The
237-
code below may be run by issuing
238-
```python
239-
import sched.asynctest
240-
```
241-
This is the demo code.
242-
```python
243-
import uasyncio as asyncio
244-
from sched.sched import schedule
245-
from sched.cron import cron
246-
from time import localtime
256+
It is a closure whose creation accepts a time specification for future events.
257+
Each subsequent call is passed the current time and returns the number of
258+
seconds to wait for the next event to occur.
247259

248-
def foo(txt): # Demonstrate callback
249-
yr, mo, md, h, m, s, wd = localtime()[:7]
250-
fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
251-
print(fst.format(txt, h, m, s, md, mo, yr))
260+
It takes the following keyword-only args. A flexible set of data types are
261+
accepted namely [time specifiers](./SCHEDULE.md#41-time-specifiers). Valid
262+
numbers are shown as inclusive ranges.
263+
1. `secs=0` Seconds (0..59).
264+
2. `mins=0` Minutes (0..59).
265+
3. `hrs=3` Hours (0..23).
266+
4. `mday=None` Day of month (1..31).
267+
5. `month=None` Months (1..12).
268+
6. `wday=None` Weekday (0..6 Mon..Sun).
252269

253-
async def bar(txt): # Demonstrate coro launch
254-
yr, mo, md, h, m, s, wd = localtime()[:7]
255-
fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
256-
print(fst.format(txt, h, m, s, md, mo, yr))
257-
await asyncio.sleep(0)
270+
## 5.1 The time to an event
258271

259-
async def main():
260-
print('Asynchronous test running...')
261-
cron4 = cron(hrs=None, mins=range(0, 60, 4))
262-
asyncio.create_task(schedule(cron4, foo, ('every 4 mins',)))
272+
When the `cron` instance is run, it must be passed a time value (normally the
273+
time now as returned by `time.time()`). The instance returns the number of
274+
seconds to the first event matching the specifier.
263275

264-
cron5 = cron(hrs=None, mins=range(0, 60, 5))
265-
asyncio.create_task(schedule(cron5, foo, ('every 5 mins',)))
276+
```python
277+
from sched.cron import cron
278+
cron1 = cron(hrs=None, mins=range(0, 60, 15)) # Every 15 minutes of every day
279+
cron2 = cron(mday=25, month=12, hrs=9) # 9am every Christmas day
280+
cron3 = cron(wday=(0, 4)) # 3am every Monday and Friday
281+
now = int(time.time()) # Unix build returns a float here
282+
tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event
283+
```
266284

267-
cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine
268-
asyncio.create_task(schedule(cron3, bar, ('every 3 mins',)))
285+
##### [Top](./SCHEDULE.md#0-contents)
269286

270-
cron2 = cron(hrs=None, mins=range(0, 60, 2))
271-
asyncio.create_task(schedule(cron2, foo, ('one shot',), True))
272-
await asyncio.sleep(900) # Quit after 15 minutes
287+
## 5.2 How it works
273288

274-
try:
275-
asyncio.run(main())
276-
finally:
277-
_ = asyncio.new_event_loop()
278-
```
289+
When a cron instance is run it seeks a future time and date relative to the
290+
passed time value. This will be the soonest matching the specifier. A `cron`
291+
instance is a conventional function and does not store state. Repeated calls
292+
will return the same value if passed the same time value (`now` in the above
293+
example).
279294

280295
##### [Top](./SCHEDULE.md#0-contents)
281296

@@ -354,7 +369,8 @@ main()
354369

355370
In my opinion the asynchronous version is cleaner and easier to understand. It
356371
is also more versatile because the advanced features of `uasyncio` are
357-
available to the application. The above code is incompatible with `uasyncio`
358-
because of the blocking calls to `time.sleep()`.
372+
available to the application including cancellation of scheduled tasks. The
373+
above code is incompatible with `uasyncio` because of the blocking calls to
374+
`time.sleep()`.
359375

360376
##### [Top](./SCHEDULE.md#0-contents)

0 commit comments

Comments
 (0)