From 970db05d96937e0dd22ab9c751af63be61b78b94 Mon Sep 17 00:00:00 2001 From: "A.Shpak" Date: Sat, 7 Dec 2024 12:28:57 +0300 Subject: [PATCH] feat: add typing --- .github/workflows/tox.yml | 6 ++-- .gitignore | 4 ++- aiocron/__init__.py | 64 +++++++++++++++++++++++---------------- aiocron/__main__.py | 2 +- pyproject.toml | 2 +- uv.lock | 6 ---- 6 files changed, 46 insertions(+), 38 deletions(-) diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 0146a10..27cc0b7 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -1,6 +1,6 @@ name: tox -on: [push, pull_request] +on: [ push, pull_request ] jobs: tox: @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v3 with: - version: "0.4.18" + version: "0.5" enable-cache: true - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index d01cff3..a41c0c0 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,6 @@ MANIFEST .epg_data/* # uv -.python-version \ No newline at end of file +.python-version + +.venv/ \ No newline at end of file diff --git a/aiocron/__init__.py b/aiocron/__init__.py index 8f7568d..f1a06a9 100644 --- a/aiocron/__init__.py +++ b/aiocron/__init__.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- from croniter.croniter import croniter +import zoneinfo from datetime import datetime from functools import wraps, partial from tzlocal import get_localzone -from uuid import uuid4 +from uuid import uuid4, UUID import time import asyncio import sys import inspect +import typing as tp -async def null_callback(*args): +async def null_callback(*args: tp.Any) -> tuple[tp.Any, ...]: return args @@ -30,16 +32,16 @@ async def wrapper(*args, **kwargs): class Cron(object): def __init__( self, - spec, - func=None, - args=(), - kwargs=None, - start=False, - uuid=None, - loop=None, - tz=None, - croniter_kwargs=None, - ): + spec: str, + func: tp.Optional[tp.Callable[..., tp.Union[tp.Any, tp.Awaitable[tp.Any]]]] = None, + args: tuple[tp.Any, ...] = (), + kwargs: tp.Optional[tp.Mapping[str, tp.Any]] = None, + start: bool = False, + uuid: tp.Optional[UUID] = None, + loop: tp.Optional[asyncio.AbstractEventLoop] = None, + tz: tp.Optional[zoneinfo.ZoneInfo] = None, + croniter_kwargs: tp.Optional[tp.Mapping[str, tp.Any]] = None, + ) -> None: self.spec = spec if func is not None: kwargs = kwargs or {} @@ -47,35 +49,37 @@ def __init__( else: self.func = null_callback self.tz = get_localzone() if tz is None else tz - self.cron = wrap_func(self.func) + self.cron: tp.Callable[..., tp.Awaitable[tp.Any]] = wrap_func(self.func) self.auto_start = start self.uuid = uuid if uuid is not None else uuid4() - self.handle = self.future = self.croniter = None + self.handle = None + self.future: tp.Optional[asyncio.Future] = None + self.croniter: tp.Optional[croniter] = None self.loop = loop if loop is not None else asyncio.get_event_loop() if start and self.func is not null_callback: self.handle = self.loop.call_soon_threadsafe(self.start) self.croniter_kwargs = croniter_kwargs or {} - def start(self): + def start(self) -> None: """Start scheduling""" self.stop() self.initialize() self.handle = self.loop.call_at(self.get_next(), self.call_next) - def stop(self): + def stop(self) -> None: """Stop scheduling""" if self.handle is not None: self.handle.cancel() self.handle = self.future = self.croniter = None - async def next(self, *args): + async def next(self, *args: tp.Any) -> tp.Any: """yield from .next()""" self.initialize() self.future = asyncio.Future(loop=self.loop) self.handle = self.loop.call_at(self.get_next(), self.call_func, *args) return await self.future - def initialize(self): + def initialize(self) -> None: """Initialize croniter and related times""" if self.croniter is None: self.time = time.time() @@ -85,11 +89,11 @@ def initialize(self): self.spec, start_time=self.datetime, **self.croniter_kwargs ) - def get_next(self): + def get_next(self) -> float: """Return next iteration time related to loop time""" return self.loop_time + (self.croniter.get_next(float) - self.time) - def call_next(self): + def call_next(self) -> None: """Set next hop in the loop. Call task""" if self.handle is not None: self.handle.cancel() @@ -97,7 +101,7 @@ def call_next(self): self.handle = self.loop.call_at(next_time, self.call_next) self.call_func() - def call_func(self, *args, **kwargs): + def call_func(self, *args: tp.Any, **kwargs: tp.Any) -> None: """Called. Take care of exceptions using gather""" """Check the version of python installed""" if sys.version_info[0:2] >= (3, 10): @@ -109,7 +113,7 @@ def call_func(self, *args, **kwargs): self.cron(*args, **kwargs), loop=self.loop, return_exceptions=True ).add_done_callback(self.set_result) - def set_result(self, result): + def set_result(self, result: asyncio.Future) -> None: """Set future's result if needed (can be an exception). Else raise if needed.""" result = result.result()[0] @@ -122,7 +126,7 @@ def set_result(self, result): elif isinstance(result, Exception): raise result - def __call__(self, func): + def __call__(self, func: tp.Callable[..., tp.Awaitable[tp.Any]]) -> 'Cron': """Used as a decorator""" self.func = func self.cron = wrap_func(func) @@ -130,14 +134,22 @@ def __call__(self, func): self.loop.call_soon_threadsafe(self.start) return self - def __str__(self): + def __str__(self) -> str: return "{0.spec} {0.func}".format(self) - def __repr__(self): + def __repr__(self) -> str: return "".format(self) -def crontab(spec, func=None, args=(), kwargs=None, start=True, loop=None, tz=None): +def crontab( + spec: str, + func: tp.Optional[tp.Callable[..., tp.Union[tp.Any, tp.Awaitable[tp.Any]]]] = None, + args: tuple[tp.Any, ...] = (), + kwargs: tp.Optional[tp.Mapping[str, tp.Any]] = None, + start: bool = False, + loop: tp.Optional[asyncio.AbstractEventLoop] = None, + tz: tp.Optional[zoneinfo.ZoneInfo] = None, +) -> Cron: return Cron( spec, func=func, args=args, kwargs=kwargs, start=start, loop=loop, tz=tz ) diff --git a/aiocron/__main__.py b/aiocron/__main__.py index 5b812e4..65e6261 100644 --- a/aiocron/__main__.py +++ b/aiocron/__main__.py @@ -6,7 +6,7 @@ import argparse -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.prog = "python -m aiocron" parser.add_argument( diff --git a/pyproject.toml b/pyproject.toml index 0eeae99..7f567ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -46,7 +47,6 @@ build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["aiocron"] - [tool.pytest.ini_options] addopts = "--cov aiocron --cov-report term-missing" testpaths = ["tests"] diff --git a/uv.lock b/uv.lock index 9fde648..df2673a 100644 --- a/uv.lock +++ b/uv.lock @@ -18,10 +18,6 @@ dependencies = [ ] [package.optional-dependencies] -coverage = [ - { name = "pytest" }, - { name = "pytest-cov" }, -] test = [ { name = "coverage" }, { name = "pytest" }, @@ -32,9 +28,7 @@ test = [ requires-dist = [ { name = "coverage", marker = "extra == 'test'", specifier = ">=7.6.1" }, { name = "croniter", specifier = ">=3.0.3" }, - { name = "pytest", marker = "extra == 'coverage'", specifier = ">=8.3.3" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.3" }, - { name = "pytest-cov", marker = "extra == 'coverage'", specifier = ">=5.0.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" }, { name = "tox", specifier = ">=4.21.1" }, { name = "tox-uv", specifier = ">=1.13.0" },