diff --git a/hermes_time.py b/hermes_time.py index 08f865333b..4bbe7d2e2e 100644 --- a/hermes_time.py +++ b/hermes_time.py @@ -27,11 +27,22 @@ except ImportError: # Python 3.8 fallback (shouldn't be needed — Hermes requires 3.9+) from backports.zoneinfo import ZoneInfo # type: ignore[no-redef] -# Cached state — resolved once, reused on every call. +# Cached state, keyed to the active timezone source. This process can multiplex +# profiles by switching HERMES_HOME, so a single unkeyed value would leak the +# first profile's timezone into later profile-scoped work. # Call reset_cache() to force re-resolution (e.g. after config changes). _cached_tz: Optional[ZoneInfo] = None _cached_tz_name: Optional[str] = None _cache_resolved: bool = False +_cache_identity: Optional[tuple[str, str]] = None + + +def _timezone_cache_identity() -> tuple[str, str]: + """Return the active source identity for the timezone cache.""" + tz_env = os.getenv("HERMES_TIMEZONE", "").strip() + if tz_env: + return ("environment", tz_env) + return ("config", str(get_config_path())) def _resolve_timezone_name() -> str: @@ -94,14 +105,17 @@ def _get_zoneinfo(name: str) -> Optional[ZoneInfo]: def get_timezone() -> Optional[ZoneInfo]: - """Return the user's configured ZoneInfo, or None (meaning server-local). + """Return the active profile's configured ZoneInfo, or None (server-local). - Resolved once and cached. Call ``reset_cache()`` after config changes. + The cache is isolated by the active environment override or profile config + path. Call ``reset_cache()`` after editing the active config in place. """ - global _cached_tz, _cached_tz_name, _cache_resolved - if not _cache_resolved: + global _cached_tz, _cached_tz_name, _cache_resolved, _cache_identity + cache_identity = _timezone_cache_identity() + if not _cache_resolved or _cache_identity != cache_identity: _cached_tz_name = _resolve_timezone_name() _cached_tz = _get_zoneinfo(_cached_tz_name) + _cache_identity = cache_identity _cache_resolved = True return _cached_tz @@ -113,10 +127,11 @@ def reset_cache() -> None: config edit or ``HERMES_TIMEZONE`` update) to force ``get_timezone()`` / ``now()`` to read the new value instead of the value cached at first use. """ - global _cached_tz, _cached_tz_name, _cache_resolved + global _cached_tz, _cached_tz_name, _cache_resolved, _cache_identity _cached_tz = None _cached_tz_name = None _cache_resolved = False + _cache_identity = None def now() -> datetime: diff --git a/tests/test_timezone.py b/tests/test_timezone.py index 524fbf2507..30ae6c628c 100644 --- a/tests/test_timezone.py +++ b/tests/test_timezone.py @@ -25,6 +25,7 @@ def _reset_hermes_time_cache(): hermes_time._cached_tz = None hermes_time._cached_tz_name = None hermes_time._cache_resolved = False + hermes_time._cache_identity = None # ========================================================================= @@ -86,11 +87,28 @@ class TestGetTimezone: assert isinstance(tz, ZoneInfo) assert str(tz) == "Europe/London" + def test_cache_isolated_by_active_profile_config(self, tmp_path, monkeypatch): + """Switching HERMES_HOME must not reuse another profile's timezone.""" + first_home = tmp_path / "first" + second_home = tmp_path / "second" + first_home.mkdir() + second_home.mkdir() + (first_home / "config.yaml").write_text("timezone: Asia/Tokyo\n", encoding="utf-8") + (second_home / "config.yaml").write_text( + "timezone: America/New_York\n", encoding="utf-8" + ) + monkeypatch.delenv("HERMES_TIMEZONE", raising=False) + monkeypatch.setenv("HERMES_HOME", str(first_home)) + assert str(hermes_time.get_timezone()) == "Asia/Tokyo" + # Multiplexed profile runtime scopes switch HERMES_HOME in one process. + monkeypatch.setenv("HERMES_HOME", str(second_home)) + assert str(hermes_time.get_timezone()) == "America/New_York" # ========================================================================= + # execute_code child env — TZ injection # =========================================================================