Skip to content

Commit 62c54da

Browse files
sobolevnAlexWaygoodJelleZijlstra
authored
Complete python-crontab (#9306)
Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 32c575d commit 62c54da

File tree

4 files changed

+123
-100
lines changed

4 files changed

+123
-100
lines changed

pyrightconfig.stricter.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"stubs/Pillow",
5050
"stubs/prettytable",
5151
"stubs/protobuf",
52-
"stubs/python-crontab",
5352
"stubs/google-cloud-ndb",
5453
"stubs/influxdb-client",
5554
"stubs/passlib",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
# Runtime only-hack that doesn't affect typing:
2+
crontabs.CronTabs.__new__
13
# stub does not have *args argument "args", but function doesn't actually accept positional args
24
crontab.CronTab.remove_all

stubs/python-crontab/crontab.pyi

Lines changed: 120 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import re
22
import subprocess
33
from _typeshed import Incomplete, Self
4+
from builtins import range as _range
45
from collections import OrderedDict
5-
from collections.abc import Callable, Generator, Iterator
6+
from collections.abc import Callable, Generator, Iterable, Iterator
7+
from datetime import datetime
68
from logging import Logger
79
from types import TracebackType
810
from typing import Any
9-
from typing_extensions import SupportsIndex
11+
from typing_extensions import SupportsIndex, TypeAlias
1012

1113
from cronlog import CronLog
1214

15+
_User: TypeAlias = str | bool | None
16+
1317
__pkgname__: str
1418
ITEMREX: re.Pattern[str]
1519
SPECREX: re.Pattern[str]
@@ -28,18 +32,19 @@ CRON_COMMAND: str
2832
SHELL: str
2933
current_user: Callable[[], str | None]
3034

31-
def open_pipe(cmd: str, *args: str, **flags) -> subprocess.Popen[Any]: ...
35+
def open_pipe(cmd: str, *args: str, **flags: str) -> subprocess.Popen[Any]: ...
3236

3337
class CronTab:
34-
lines: Incomplete
35-
crons: list[CronItem]
38+
lines: list[str | CronItem] | None
39+
crons: list[CronItem] | None
3640
filen: str | None
37-
cron_command: Incomplete
38-
env: OrderedVariableList
41+
cron_command: str
42+
env: OrderedVariableList | None
3943
root: bool
4044
intab: str | None
45+
tabfile: str | None
4146
def __init__(
42-
self, user: bool | str | None = ..., tab: str | None = ..., tabfile: str | None = ..., log: str | None = ...
47+
self, user: _User = ..., tab: str | None = ..., tabfile: str | None = ..., log: CronLog | str | None = ...
4348
) -> None: ...
4449
def __enter__(self: Self) -> Self: ...
4550
def __exit__(
@@ -48,7 +53,7 @@ class CronTab:
4853
@property
4954
def log(self) -> CronLog: ...
5055
@property
51-
def user(self) -> str | None: ...
56+
def user(self) -> _User: ...
5257
@property
5358
def user_opt(self) -> dict[str, str]: ...
5459
def read(self, filename: str | None = ...) -> None: ...
@@ -59,11 +64,14 @@ class CronTab:
5964
read: bool = ...,
6065
before: str | re.Pattern[str] | list[CronItem] | tuple[CronItem, ...] | Generator[CronItem, Any, Any] | None = ...,
6166
) -> None: ...
62-
def write(self, filename: str | None = ..., user: bool | str | None = ..., errors: bool = ...) -> None: ...
67+
def write(self, filename: str | None = ..., user: _User = ..., errors: bool = ...) -> None: ...
6368
def write_to_user(self, user: bool | str = ...) -> None: ...
64-
def run_pending(self, **kwargs) -> Generator[Incomplete, None, None]: ...
65-
def run_scheduler(self, timeout: int = ..., **kwargs) -> Generator[Incomplete, None, None]: ...
66-
def render(self, errors: bool = ..., specials: bool = ...) -> str: ...
69+
# Usually `kwargs` are just `now: datetime | None`, but technically this can
70+
# work for `CronItem` subclasses, which might define other kwargs.
71+
def run_pending(self, **kwargs: Any) -> Iterator[str]: ...
72+
# There are two known kwargs and others are unused:
73+
def run_scheduler(self, timeout: int = ..., *, warp: object = ..., cadence: int = ..., **kwargs: object) -> Iterator[str]: ...
74+
def render(self, errors: bool = ..., specials: bool | None = ...) -> str: ...
6775
def new(
6876
self,
6977
command: str = ...,
@@ -72,97 +80,104 @@ class CronTab:
7280
pre_comment: bool = ...,
7381
before: str | re.Pattern[str] | list[CronItem] | tuple[CronItem, ...] | Generator[CronItem, Any, Any] | None = ...,
7482
) -> CronItem: ...
75-
def find_command(self, command: str | re.Pattern[str]) -> Generator[CronItem, None, None]: ...
76-
def find_comment(self, comment: str | re.Pattern[str]) -> Generator[CronItem, None, None]: ...
77-
def find_time(self, *args) -> Generator[CronItem, None, None]: ...
83+
def find_command(self, command: str | re.Pattern[str]) -> Iterator[CronItem]: ...
84+
def find_comment(self, comment: str | re.Pattern[str]) -> Iterator[CronItem]: ...
85+
def find_time(self, *args: Any) -> Iterator[CronItem]: ...
7886
@property
79-
def commands(self) -> Generator[Incomplete, None, None]: ...
87+
def commands(self) -> Iterator[str]: ...
8088
@property
81-
def comments(self) -> Generator[Incomplete, None, None]: ...
82-
def remove_all(self, *, command: str | re.Pattern[str] = ..., comment: str | re.Pattern[str] = ..., time=...) -> int: ...
83-
def remove(self, *items: CronItem | list[CronItem] | tuple[CronItem, ...] | Generator[CronItem, Any, Any]) -> int: ...
89+
def comments(self) -> Iterator[str]: ...
90+
# You cannot actually pass `*args`, it will raise an exception,
91+
# also known kwargs are added:
92+
def remove_all(
93+
self, *, command: str | re.Pattern[str] = ..., comment: str | re.Pattern[str] = ..., time: Any = ..., **kwargs: object
94+
) -> int: ...
95+
def remove(self, *items: CronItem | Iterable[CronItem]) -> int: ...
8496
def __iter__(self) -> Iterator[CronItem]: ...
8597
def __getitem__(self, i: SupportsIndex) -> CronItem: ...
8698
def __len__(self) -> int: ...
8799

88100
class CronItem:
89-
cron: Incomplete
90-
user: str | None
101+
cron: CronTab | None
102+
user: _User
91103
valid: bool
92104
enabled: bool
93105
special: bool
94-
comment: Incomplete
95-
command: Incomplete
96-
last_run: Incomplete
97-
env: Incomplete
106+
comment: str
107+
command: str | None
108+
last_run: datetime | None
109+
env: OrderedVariableList
98110
pre_comment: bool
99-
marker: Incomplete
100-
stdin: Incomplete
101-
slices: Incomplete
102-
def __init__(self, command: str = ..., comment: str = ..., user: str | None = ..., pre_comment: bool = ...) -> None: ...
111+
marker: str | None
112+
stdin: str | None
113+
slices: CronSlices
114+
def __init__(self, command: str = ..., comment: str = ..., user: _User = ..., pre_comment: bool = ...) -> None: ...
103115
def __hash__(self) -> int: ...
104116
def __eq__(self, other: object) -> bool: ...
105117
@classmethod
106118
def from_line(cls: type[Self], line: str, user: str | None = ..., cron: Incomplete | None = ...) -> Self: ...
107119
def delete(self) -> None: ...
108120
def set_command(self, cmd: str, parse_stdin: bool = ...) -> None: ...
109121
def set_comment(self, cmt: str, pre_comment: bool = ...) -> None: ...
110-
def parse(self, line) -> None: ...
122+
def parse(self, line: str) -> None: ...
111123
def enable(self, enabled: bool = ...) -> bool: ...
112124
def is_enabled(self) -> bool: ...
113125
def is_valid(self) -> bool: ...
114126
def render(self, specials: bool = ...) -> str: ...
115-
def every_reboot(self): ...
116-
def every(self, unit: int = ...): ...
117-
def setall(self, *args: Any): ...
118-
def clear(self): ...
127+
def every_reboot(self) -> None: ...
128+
def every(self, unit: int = ...) -> Every: ...
129+
def setall(self, *args: Any) -> None: ...
130+
def clear(self) -> None: ...
119131
def frequency(self, year: int | None = ...) -> int: ...
120132
def frequency_per_year(self, year: int | None = ...) -> int: ...
121133
def frequency_per_day(self) -> int: ...
122134
def frequency_per_hour(self) -> int: ...
123-
def run_pending(self, now: Incomplete | None = ...): ...
124-
def run(self): ...
125-
def schedule(self, date_from: Incomplete | None = ...): ...
126-
def description(self, **kw: Any): ...
135+
def run_pending(self, now: datetime | None = ...) -> int | str: ...
136+
def run(self) -> str: ...
137+
# TODO: use types from `croniter` module here:
138+
def schedule(self, date_from: datetime | None = ...) -> Incomplete: ...
139+
# TODO: use types from `cron_descriptor` here:
140+
def description(self, **kw: Incomplete) -> Incomplete: ...
127141
@property
128-
def log(self): ...
142+
def log(self) -> CronLog: ...
129143
@property
130-
def minute(self) -> CronSlice: ...
144+
def minute(self) -> int | str: ...
131145
@property
132-
def minutes(self) -> CronSlice: ...
146+
def minutes(self) -> int | str: ...
133147
@property
134-
def hour(self) -> CronSlice: ...
148+
def hour(self) -> int | str: ...
135149
@property
136-
def hours(self) -> CronSlice: ...
150+
def hours(self) -> int | str: ...
137151
@property
138-
def day(self) -> CronSlice: ...
152+
def day(self) -> int | str: ...
139153
@property
140-
def dom(self) -> CronSlice: ...
154+
def dom(self) -> int | str: ...
141155
@property
142-
def month(self) -> CronSlice: ...
156+
def month(self) -> int | str: ...
143157
@property
144-
def months(self) -> CronSlice: ...
158+
def months(self) -> int | str: ...
145159
@property
146-
def dow(self) -> CronSlice: ...
160+
def dow(self) -> int | str: ...
147161
def __len__(self) -> int: ...
148-
def __getitem__(self, key: SupportsIndex) -> CronSlice: ...
149-
def __lt__(self, value) -> bool: ...
150-
def __gt__(self, value) -> bool: ...
162+
def __getitem__(self, key: int | str) -> int | str: ...
163+
def __lt__(self, value: object) -> bool: ...
164+
def __gt__(self, value: object) -> bool: ...
151165

152166
class Every:
153-
slices: Incomplete
154-
unit: Incomplete
155-
def __init__(self, item, units) -> None: ...
167+
slices: CronSlices
168+
unit: int
169+
# TODO: add generated attributes
170+
def __init__(self, item: CronSlices, units: int) -> None: ...
156171
def set_attr(self, target: int) -> Callable[[], None]: ...
157172
def year(self) -> None: ...
158173

159174
class CronSlices(list[CronSlice]):
160-
special: Incomplete
175+
special: bool | None
161176
def __init__(self, *args: Any) -> None: ...
162177
def is_self_valid(self, *args: Any) -> bool: ...
163178
@classmethod
164179
def is_valid(cls, *args: Any) -> bool: ...
165-
def setall(self, *slices) -> None: ...
180+
def setall(self, *slices: str) -> None: ...
166181
def clean_render(self) -> str: ...
167182
def render(self, specials: bool = ...) -> str: ...
168183
def clear(self) -> None: ...
@@ -175,64 +190,72 @@ class CronSlices(list[CronSlice]):
175190
class SundayError(KeyError): ...
176191

177192
class Also:
178-
obj: Incomplete
179-
def __init__(self, obj) -> None: ...
180-
def every(self, *a): ...
181-
def on(self, *a): ...
182-
def during(self, *a): ...
193+
obj: CronSlice
194+
def __init__(self, obj: CronSlice) -> None: ...
195+
# These method actually use `*args`, but pass them to `CronSlice` methods,
196+
# this is why they are typed as `Any`.
197+
def every(self, *a: Any) -> _Part: ...
198+
def on(self, *a: Any) -> list[_Part]: ...
199+
def during(self, *a: Any) -> _Part: ...
200+
201+
_Part: TypeAlias = int | CronValue | CronRange
183202

184203
class CronSlice:
185-
min: Incomplete
186-
max: Incomplete
187-
name: Incomplete
188-
enum: Incomplete
189-
parts: Incomplete
190-
def __init__(self, info, value: Incomplete | None = ...) -> None: ...
204+
min: int | None
205+
max: int | None
206+
name: str | None
207+
enum: list[str | None] | None
208+
parts: list[_Part]
209+
def __init__(self, info: int | dict[str, Any], value: str | None = ...) -> None: ...
191210
def __hash__(self) -> int: ...
192-
def parse(self, value) -> None: ...
193-
def render(self, resolve: bool = ..., specials: bool = ...): ...
211+
def parse(self, value: str | None) -> None: ...
212+
def render(self, resolve: bool = ..., specials: bool = ...) -> str: ...
194213
def __eq__(self, arg: object) -> bool: ...
195-
def every(self, n_value, also: bool = ...): ...
196-
def on(self, *n_value, **opts): ...
197-
def during(self, vfrom, vto, also: bool = ...): ...
214+
def every(self, n_value: int, also: bool = ...) -> _Part: ...
215+
# The only known kwarg, others are unused,
216+
# `*args`` are passed to `parse_value`, so they are `Any`
217+
def on(self, *n_value: Any, also: bool = ...) -> list[_Part]: ...
218+
def during(self, vfrom: int | str, vto: int | str, also: bool = ...) -> _Part: ...
198219
@property
199-
def also(self): ...
220+
def also(self) -> Also: ...
200221
def clear(self) -> None: ...
201-
def get_range(self, *vrange): ...
202-
def __iter__(self): ...
222+
def get_range(self, *vrange: int | str | CronValue) -> list[int | CronRange]: ...
223+
def __iter__(self) -> Iterator[int]: ...
203224
def __len__(self) -> int: ...
204-
def parse_value(self, val, sunday: Incomplete | None = ...): ...
225+
def parse_value(self, val: str, sunday: int | None = ...) -> int | CronValue: ...
205226

206-
def get_cronvalue(value, enums): ...
227+
def get_cronvalue(value: int, enums: list[str]) -> int | CronValue: ...
207228

208229
class CronValue:
209-
text: Incomplete
210-
value: Incomplete
211-
def __init__(self, value, enums) -> None: ...
212-
def __lt__(self, value): ...
230+
text: str
231+
value: int
232+
def __init__(self, value: str, enums: list[str]) -> None: ...
233+
def __lt__(self, value: object) -> bool: ...
213234
def __int__(self) -> int: ...
214235

215236
class CronRange:
216-
dangling: Incomplete
217-
slice: Incomplete
218-
cron: Incomplete
237+
dangling: int | None
238+
slice: str
239+
cron: CronTab | None
219240
seq: int
220-
def __init__(self, vslice, *vrange) -> None: ...
221-
vfrom: Incomplete
222-
vto: Incomplete
223-
def parse(self, value) -> None: ...
241+
def __init__(self, vslice: str, *vrange: int | str | CronValue) -> None: ...
242+
# Are not set in `__init__`:
243+
vfrom: int | CronValue
244+
vto: int | CronValue
245+
def parse(self, value: str) -> None: ...
224246
def all(self) -> None: ...
225-
def render(self, resolve: bool = ...): ...
226-
def range(self): ...
227-
def every(self, value) -> None: ...
228-
def __lt__(self, value): ...
229-
def __gt__(self, value): ...
247+
def render(self, resolve: bool = ...) -> str: ...
248+
def range(self) -> _range: ...
249+
def every(self, value: int | str) -> None: ...
250+
def __lt__(self, value: object) -> bool: ...
251+
def __gt__(self, value: object) -> bool: ...
230252
def __int__(self) -> int: ...
231253

254+
# TODO: make generic
232255
class OrderedVariableList(OrderedDict[Incomplete, Incomplete]):
233256
job: Incomplete
234257
def __init__(self, *args: Any, **kw: Any) -> None: ...
235258
@property
236-
def previous(self): ...
237-
def all(self): ...
238-
def __getitem__(self, key): ...
259+
def previous(self) -> Incomplete: ...
260+
def all(self: Self) -> Self: ...
261+
def __getitem__(self, key: Incomplete) -> Incomplete: ...

stubs/python-crontab/crontabs.pyi

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ class SystemTab(list[CronTab]):
1313

1414
class AnaCronTab(list[CronTab]):
1515
def __init__(self, loc: str, tabs: CronTabs | None = ...) -> None: ...
16-
def add(self, loc: str, item: str, anajob) -> CronTab: ...
16+
def add(self, loc: str, item: str, anajob: CronTab) -> CronTab: ...
1717

1818
KNOWN_LOCATIONS: list[tuple[UserSpool | SystemTab | AnaCronTab, str]]
1919

2020
class CronTabs(list[UserSpool | SystemTab | AnaCronTab]):
21-
def __new__(cls, *args: Any, **kw: Any): ...
2221
def __init__(self) -> None: ...
2322
def add(self, cls: type[UserSpool | SystemTab | AnaCronTab], *args: Any) -> None: ...
2423
@property

0 commit comments

Comments
 (0)