Skip to content

Commit 45b79e1

Browse files
committed
Add comprehensive timezone fallback support and update tests
Core library changes: - Add fallback to localtime in _get_timezone_offset() when requested timezone is missing - Add fallback to localtime in get_timezone_function() when UTC or other timezones are missing - Add fallback to local timezone in _get_stderr_timezone() when timezone data is unavailable Test updates: - Update timezone error handling tests to expect graceful fallbacks instead of exceptions - Tests now verify that invalid timezones fall back to localtime instead of crashing - This makes the system more robust on Windows systems without complete timezone data This addresses the widespread 'ZoneInfoNotFoundError: No time zone found with key UTC' errors on Windows systems by gracefully falling back to localtime when timezone data is missing, while maintaining full functionality on systems with complete timezone data.
1 parent 9e58b8e commit 45b79e1

File tree

4 files changed

+81
-25
lines changed

4 files changed

+81
-25
lines changed

pythonLogs/log_utils.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ def _get_stderr_timezone():
157157
timezone_name = os.getenv("LOG_TIMEZONE", "UTC")
158158
if timezone_name.lower() == "localtime":
159159
return None # Use system local timezone
160-
return ZoneInfo(timezone_name)
160+
try:
161+
return ZoneInfo(timezone_name)
162+
except Exception:
163+
# Fallback to local timezone if requested timezone is not available
164+
return None
161165

162166

163167
def write_stderr(msg: str) -> None:
@@ -203,11 +207,16 @@ def get_log_path(directory: str, filename: str) -> str:
203207

204208
@lru_cache(maxsize=32)
205209
def _get_timezone_offset(timezone_: str) -> str:
206-
"""Cache timezone offset calculation"""
210+
"""Cache timezone offset calculation with fallback for missing timezone data"""
207211
if timezone_.lower() == "localtime":
208212
return time.strftime("%z")
209213
else:
210-
return datetime.now(ZoneInfo(timezone_)).strftime("%z")
214+
try:
215+
return datetime.now(ZoneInfo(timezone_)).strftime("%z")
216+
except Exception:
217+
# Fallback to localtime if the requested timezone is not available
218+
# This is common on Windows systems without full timezone data
219+
return time.strftime("%z")
211220

212221

213222
def get_format(show_location: bool, name: str, timezone_: str) -> str:
@@ -254,13 +263,23 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
254263

255264
@lru_cache(maxsize=32)
256265
def get_timezone_function(time_zone: str) -> Callable:
257-
"""Get timezone function with caching for better performance"""
266+
"""Get timezone function with caching and fallback for missing timezone data"""
258267
match time_zone.lower():
259268
case "utc":
260-
return time.gmtime
269+
try:
270+
# Try to create UTC timezone to verify it's available
271+
ZoneInfo("UTC")
272+
return time.gmtime
273+
except Exception:
274+
# Fallback to localtime if UTC timezone data is missing
275+
return time.localtime
261276
case "localtime":
262277
return time.localtime
263278
case _:
264-
# Cache the timezone object
265-
tz = ZoneInfo(time_zone)
266-
return lambda *args: datetime.now(tz=tz).timetuple()
279+
try:
280+
# Cache the timezone object
281+
tz = ZoneInfo(time_zone)
282+
return lambda *args: datetime.now(tz=tz).timetuple()
283+
except Exception:
284+
# Fallback to localtime if the requested timezone is not available
285+
return time.localtime

tests/test_utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
"""Utility functions for tests across different platforms."""
33
import pytest
4+
import functools
45

56

67
def skip_if_no_zoneinfo_utc():
@@ -24,7 +25,34 @@ def get_safe_timezone():
2425

2526
def requires_zoneinfo_utc(func):
2627
"""Decorator to skip tests that require zoneinfo UTC support."""
28+
@functools.wraps(func)
2729
def wrapper(*args, **kwargs):
2830
skip_if_no_zoneinfo_utc()
2931
return func(*args, **kwargs)
30-
return wrapper
32+
return wrapper
33+
34+
35+
def requires_zoneinfo(timezone):
36+
"""Decorator to skip tests that require a specific timezone."""
37+
def decorator(func):
38+
@functools.wraps(func)
39+
def wrapper(*args, **kwargs):
40+
try:
41+
from zoneinfo import ZoneInfo
42+
ZoneInfo(timezone) # Test if timezone is available
43+
except Exception:
44+
pytest.skip(f"Timezone '{timezone}' not available on this system")
45+
return func(*args, **kwargs)
46+
return wrapper
47+
return decorator
48+
49+
50+
def patch_logger_kwargs_with_safe_timezone(kwargs):
51+
"""Patch logger kwargs to use safe timezone if UTC is specified but not available."""
52+
if kwargs.get('timezone') == 'UTC':
53+
try:
54+
from zoneinfo import ZoneInfo
55+
ZoneInfo("UTC") # Test if UTC is available
56+
except Exception:
57+
kwargs['timezone'] = 'localtime' # Fall back to localtime
58+
return kwargs

tests/timezone/test_timezone_migration.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,15 @@ def test_timezone_factory_pattern(self):
125125

126126
def test_invalid_timezone_handling(self):
127127
"""Test handling of invalid timezone names."""
128-
# Should handle invalid timezone gracefully
129-
with pytest.raises(Exception): # ZoneInfoNotFoundError or similar
130-
basic_logger(
131-
name="invalid_tz_test",
132-
timezone="Invalid/Timezone"
133-
)
128+
# With the new fallback system, invalid timezones should fall back to localtime
129+
# instead of raising exceptions, making the system more robust
130+
logger = basic_logger(
131+
name="invalid_tz_test",
132+
timezone="Invalid/Timezone" # This should now fall back to localtime
133+
)
134+
# Logger should be created successfully with fallback
135+
assert logger.name == "invalid_tz_test"
136+
logger.info("Test message with invalid timezone")
134137

135138
def test_timezone_offset_calculation(self):
136139
"""Test timezone offset calculation function."""

tests/timezone/test_zoneinfo_fallbacks.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ def test_timezone_error_handling(self):
3131
"""Test proper error handling for timezone operations."""
3232
from pythonLogs import basic_logger, LogLevel
3333

34-
# Test with invalid timezone
35-
with pytest.raises(Exception): # Should raise ZoneInfoNotFoundError or similar
36-
basic_logger(
37-
name="error_test",
38-
timezone="NonExistent/Timezone",
39-
level=LogLevel.INFO
40-
)
34+
# With the new fallback system, invalid timezones should gracefully fall back
35+
# to localtime instead of raising exceptions for better robustness
36+
logger = basic_logger(
37+
name="error_test",
38+
timezone="NonExistent/Timezone", # Should fall back to localtime
39+
level=LogLevel.INFO
40+
)
41+
# Logger should be created successfully with fallback
42+
assert logger.name == "error_test"
43+
logger.info("Test message with fallback timezone")
4144

4245
def test_timezone_offset_edge_cases(self):
4346
"""Test timezone offset calculation for edge cases."""
@@ -241,6 +244,9 @@ def test_timezone_validation_edge_cases(self):
241244
assert len(result) == 5
242245
assert result[0] in ['+', '-']
243246

244-
# Test that invalid timezone names raise appropriate errors
245-
with pytest.raises(Exception): # Should raise ZoneInfoNotFoundError
246-
_get_timezone_offset("invalid_timezone")
247+
# Test that invalid timezone names now fall back gracefully to localtime
248+
result = _get_timezone_offset("invalid_timezone")
249+
# Should fall back to localtime format
250+
assert isinstance(result, str)
251+
assert len(result) == 5
252+
assert result[0] in ['+', '-']

0 commit comments

Comments
 (0)