Skip to content

Commit 00e87d1

Browse files
committed
fix(cron): avoid third-party utils import collision (#82069)
1 parent 05330e8 commit 00e87d1

3 files changed

Lines changed: 36 additions & 1 deletion

File tree

cron/_hermes_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Load Hermes' top-level utilities without colliding with third-party ``utils``."""
2+
3+
from importlib.util import module_from_spec, spec_from_file_location
4+
from pathlib import Path
5+
6+
7+
_UTILS_PATH = Path(__file__).resolve().parent.parent / "utils.py"
8+
_SPEC = spec_from_file_location("_hermes_utils_impl", _UTILS_PATH)
9+
if _SPEC is None or _SPEC.loader is None: # pragma: no cover - broken install
10+
raise ImportError(f"Unable to load Hermes utilities from {_UTILS_PATH}")
11+
_MODULE = module_from_spec(_SPEC)
12+
_SPEC.loader.exec_module(_MODULE)
13+
14+
atomic_replace = _MODULE.atomic_replace
15+
atomic_write_text = _MODULE.atomic_write_text
16+
17+
__all__ = ["atomic_replace", "atomic_write_text"]

cron/jobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
logger = logging.getLogger(__name__)
4040

4141
from hermes_time import now as _hermes_now
42-
from utils import atomic_replace, atomic_write_text
42+
from cron._hermes_utils import atomic_replace, atomic_write_text
4343

4444
# ``croniter`` compiles ~15 ms of regexes at import and only matters for
4545
# 5-field cron expressions. Resolve lazily; ``HAS_CRONITER`` stays a module
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Regression tests for cron imports in environments with a third-party utils module."""
2+
3+
import importlib
4+
import sys
5+
import types
6+
7+
8+
def test_cron_jobs_does_not_use_unrelated_utils_module(monkeypatch):
9+
unrelated_utils = types.ModuleType("utils")
10+
unrelated_utils.atomic_replace = None
11+
monkeypatch.setitem(sys.modules, "utils", unrelated_utils)
12+
for name in ("cron.jobs", "cron._hermes_utils"):
13+
sys.modules.pop(name, None)
14+
15+
jobs = importlib.import_module("cron.jobs")
16+
17+
assert callable(jobs.atomic_replace)
18+
assert callable(jobs.atomic_write_text)

0 commit comments

Comments
 (0)