diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb79c55..5fc1566a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.25.1 + +* Fixed: `tasktiger.schedule.cron_expr` now raises a clear `ImportError` directing users to `pip install tasktiger[cron]` when `croniter` or `pytz` is missing, instead of a simple `ModuleNotFoundError`. Added `extras_require={"cron": ["croniter>=2.0", "pytz>=2024.1"]}` in `setup.py`. + ## Version 0.25.0 * Added `Task.scheduled_at` property signifying when the task is/was supposed to run. diff --git a/README.rst b/README.rst index 2d92bb3f..faa5b150 100644 --- a/README.rst +++ b/README.rst @@ -419,6 +419,11 @@ The following options can be only specified in the task decorator: use ``schedule=cron_expr("0 * * * *")``. To run a task every Sunday at 4am UTC, you could use ``schedule=cron_expr("0 4 * * 0")``. + ``cron_expr`` requires the ``croniter`` and ``pytz`` packages. Install + them with ``pip install tasktiger[cron]`` (or add them to your own + environment). They are imported lazily, so ``tasktiger`` adds no + runtime dependencies for users who do not schedule by cron expression. + Custom retrying --------------- diff --git a/setup.py b/setup.py index f17f52fa..a82f17ae 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,9 @@ test_suite="tests", tests_require=tests_require, install_requires=install_requires, + extras_require={ + "cron": ["croniter>=2.0", "pytz>=2024.1"], + }, classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", diff --git a/tasktiger/schedule.py b/tasktiger/schedule.py index 35aa85f6..0fe77fd8 100644 --- a/tasktiger/schedule.py +++ b/tasktiger/schedule.py @@ -64,8 +64,15 @@ def _cron_expr( start_date: datetime.datetime, end_date: Optional[datetime.datetime] = None, ) -> Optional[datetime.datetime]: - import croniter # type: ignore - import pytz # type: ignore + try: + import croniter # type: ignore + import pytz # type: ignore + except ModuleNotFoundError as exc: + raise ImportError( + "tasktiger.schedule.cron_expr requires the 'croniter' and 'pytz' " + "packages (missing: {name!r}). Install them with: " + "pip install tasktiger[cron]".format(name=exc.name) + ) from exc localize = pytz.utc.localize diff --git a/tests/test_schedule_cron.py b/tests/test_schedule_cron.py new file mode 100644 index 00000000..09352cd0 --- /dev/null +++ b/tests/test_schedule_cron.py @@ -0,0 +1,35 @@ +"""Regression tests for tasktiger.schedule.cron_expr. + +Covers both the happy path (croniter + pytz installed) and the diagnostic +ImportError path that directs users to `pip install tasktiger[cron]` +""" + +import datetime +import sys +from unittest.mock import patch + +import pytest + +from tasktiger.schedule import cron_expr + + +def test_cron_expr_happy_path() -> None: + """With croniter + pytz available, cron_expr returns the next execution datetime.""" + fn, args = cron_expr("0 * * * *") + result = fn(datetime.datetime(2026, 1, 1, 0, 30), *args) + assert isinstance(result, datetime.datetime) + assert result == datetime.datetime(2026, 1, 1, 1, 0) + + +@pytest.mark.parametrize("missing_module", ["croniter", "pytz"]) +def test_cron_expr_missing_dependency_error_message(missing_module: str) -> None: + """Missing croniter or pytz raises ImportError naming tasktiger[cron].""" + fn, args = cron_expr("0 * * * *") + # Setting the module to None in sys.modules makes `import ` raise + # ModuleNotFoundError even if the package is installed in the env. + with patch.dict(sys.modules, {missing_module: None}): + with pytest.raises(ImportError) as exc_info: + fn(datetime.datetime(2026, 1, 1, 0, 30), *args) + message = str(exc_info.value) + assert "pip install tasktiger[cron]" in message + assert missing_module in message