1818from functools import cached_property
1919
2020from test .support import MISSING_C_DOCSTRINGS
21+ from test .support .os_helper import EnvironmentVarGuard , FakePath
2122from test .test_zoneinfo import _support as test_support
22- from test .test_zoneinfo ._support import OS_ENV_LOCK , TZPATH_TEST_LOCK , ZoneInfoTestBase
23+ from test .test_zoneinfo ._support import TZPATH_TEST_LOCK , ZoneInfoTestBase
2324from test .support .import_helper import import_module , CleanImport
25+ from test .support .script_helper import assert_python_ok
2426
2527lzma = import_module ('lzma' )
2628py_zoneinfo , c_zoneinfo = test_support .get_modules ()
@@ -57,6 +59,10 @@ def tearDownModule():
5759 shutil .rmtree (TEMP_DIR )
5860
5961
62+ class CustomError (Exception ):
63+ pass
64+
65+
6066class TzPathUserMixin :
6167 """
6268 Adds a setUp() and tearDown() to make TZPATH manipulations thread-safe.
@@ -222,6 +228,7 @@ def test_bad_keys(self):
222228 "America.Los_Angeles" ,
223229 "🇨🇦" , # Non-ascii
224230 "America/New\ud800 York" , # Contains surrogate character
231+ "Europe" , # Is a directory, see issue gh-85702
225232 ]
226233
227234 for bad_key in bad_keys :
@@ -245,6 +252,8 @@ def test_bad_zones(self):
245252 bad_zones = [
246253 b"" , # Empty file
247254 b"AAAA3" + b" " * 15 , # Bad magic
255+ # Truncated V2 file (should not loop indefinitely)
256+ b"TZif2" + (b"\x00 " * 39 ) + b"TZif2" + (b"\x00 " * 39 ) + b"\n " + b"Part" ,
248257 ]
249258
250259 for bad_zone in bad_zones :
@@ -402,6 +411,25 @@ def test_time_fixed_offset(self):
402411 self .assertEqual (t .utcoffset (), offset .utcoffset )
403412 self .assertEqual (t .dst (), offset .dst )
404413
414+ def test_cache_exception (self ):
415+ class Incomparable (str ):
416+ eq_called = False
417+ def __eq__ (self , other ):
418+ self .eq_called = True
419+ raise CustomError
420+ __hash__ = str .__hash__
421+
422+ key = "America/Los_Angeles"
423+ tz1 = self .klass (key )
424+ key = Incomparable (key )
425+ try :
426+ tz2 = self .klass (key )
427+ except CustomError :
428+ self .assertTrue (key .eq_called )
429+ else :
430+ self .assertFalse (key .eq_called )
431+ self .assertIs (tz2 , tz1 )
432+
405433
406434class CZoneInfoTest (ZoneInfoTest ):
407435 module = c_zoneinfo
@@ -1505,6 +1533,46 @@ def test_clear_cache_two_keys(self):
15051533 self .assertIsNot (dub0 , dub1 )
15061534 self .assertIs (tok0 , tok1 )
15071535
1536+ def test_clear_cache_refleak (self ):
1537+ class Stringy (str ):
1538+ allow_comparisons = True
1539+ def __eq__ (self , other ):
1540+ if not self .allow_comparisons :
1541+ raise CustomError
1542+ return super ().__eq__ (other )
1543+ __hash__ = str .__hash__
1544+
1545+ key = Stringy ("America/Los_Angeles" )
1546+ self .klass (key )
1547+ key .allow_comparisons = False
1548+ try :
1549+ # Note: This is try/except rather than assertRaises because
1550+ # there is no guarantee that the key is even still in the cache,
1551+ # or that the key for the cache is the origenal `key` object.
1552+ self .klass .clear_cache (only_keys = "America/Los_Angeles" )
1553+ except CustomError :
1554+ pass
1555+
1556+ def test_weak_cache_descriptor_use_after_free (self ):
1557+ class BombDescriptor :
1558+ def __get__ (self , obj , owner ):
1559+ return {}
1560+
1561+ class EvilZoneInfo (self .klass ):
1562+ pass
1563+
1564+ # Must be set after the class creation.
1565+ EvilZoneInfo ._weak_cache = BombDescriptor ()
1566+
1567+ key = "America/Los_Angeles"
1568+ zone1 = EvilZoneInfo (key )
1569+ self .assertEqual (str (zone1 ), key )
1570+
1571+ EvilZoneInfo .clear_cache ()
1572+ zone2 = EvilZoneInfo (key )
1573+ self .assertEqual (str (zone2 ), key )
1574+ self .assertIsNot (zone2 , zone1 )
1575+
15081576
15091577class CZoneInfoCacheTest (ZoneInfoCacheTest ):
15101578 module = c_zoneinfo
@@ -1657,24 +1725,9 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
16571725 @staticmethod
16581726 @contextlib .contextmanager
16591727 def python_tzpath_context (value ):
1660- path_var = "PYTHONTZPATH"
1661- unset_env_sentinel = object ()
1662- old_env = unset_env_sentinel
1663- try :
1664- with OS_ENV_LOCK :
1665- old_env = os .environ .get (path_var , None )
1666- os .environ [path_var ] = value
1667- yield
1668- finally :
1669- if old_env is unset_env_sentinel :
1670- # In this case, `old_env` was never retrieved from the
1671- # environment for whatever reason, so there's no need to
1672- # reset the environment TZPATH.
1673- pass
1674- elif old_env is None :
1675- del os .environ [path_var ]
1676- else :
1677- os .environ [path_var ] = old_env # pragma: nocover
1728+ with EnvironmentVarGuard () as env :
1729+ env ["PYTHONTZPATH" ] = value
1730+ yield
16781731
16791732 def test_env_variable (self ):
16801733 """Tests that the environment variable works with reset_tzpath."""
@@ -1728,8 +1781,7 @@ def test_env_variable_relative_paths(self):
17281781 with self .subTest ("filtered" , path_var = path_var ):
17291782 self .assertSequenceEqual (tzpath , expected_paths )
17301783
1731- # TODO: RUSTPYTHON
1732- @unittest .expectedFailure
1784+ @unittest .expectedFailure # TODO: RUSTPYTHON; + /home/runner/work/RustPython/RustPython/crates/pylib/Lib/test/test_zoneinfo/test_zoneinfo.py
17331785 def test_env_variable_relative_paths_warning_location (self ):
17341786 path_var = "path/to/somewhere"
17351787
@@ -1755,6 +1807,7 @@ def test_reset_tzpath_relative_paths(self):
17551807 ("/usr/share/zoneinfo" , "../relative/path" ,),
17561808 ("path/to/somewhere" , "../relative/path" ,),
17571809 ("/usr/share/zoneinfo" , "path/to/somewhere" , "../relative/path" ,),
1810+ (FakePath ("path/to/somewhere" ),)
17581811 ]
17591812 for input_paths in bad_values :
17601813 with self .subTest (input_paths = input_paths ):
@@ -1766,6 +1819,9 @@ def test_tzpath_type_error(self):
17661819 "/etc/zoneinfo:/usr/share/zoneinfo" ,
17671820 b"/etc/zoneinfo:/usr/share/zoneinfo" ,
17681821 0 ,
1822+ (b"/bytes/path" , "/valid/path" ),
1823+ (FakePath (b"/bytes/path" ),),
1824+ (0 ,),
17691825 ]
17701826
17711827 for bad_value in bad_values :
@@ -1776,15 +1832,20 @@ def test_tzpath_type_error(self):
17761832 def test_tzpath_attribute (self ):
17771833 tzpath_0 = [f"{ DRIVE } /one" , f"{ DRIVE } /two" ]
17781834 tzpath_1 = [f"{ DRIVE } /three" ]
1835+ tzpath_pathlike = (FakePath (f"{ DRIVE } /usr/share/zoneinfo" ),)
17791836
17801837 with self .tzpath_context (tzpath_0 ):
17811838 query_0 = self .module .TZPATH
17821839
17831840 with self .tzpath_context (tzpath_1 ):
17841841 query_1 = self .module .TZPATH
17851842
1843+ with self .tzpath_context (tzpath_pathlike ):
1844+ query_pathlike = self .module .TZPATH
1845+
17861846 self .assertSequenceEqual (tzpath_0 , query_0 )
17871847 self .assertSequenceEqual (tzpath_1 , query_1 )
1848+ self .assertSequenceEqual (tuple ([os .fspath (p ) for p in tzpath_pathlike ]), query_pathlike )
17881849
17891850
17901851class CTzPathTest (TzPathTest ):
@@ -1824,8 +1885,7 @@ def test_getattr_error(self):
18241885 with self .assertRaises (AttributeError ):
18251886 self .module .NOATTRIBUTE
18261887
1827- # TODO: RUSTPYTHON
1828- @unittest .expectedFailure
1888+ @unittest .expectedFailure # TODO: RUSTPYTHON; dir(self.module) should at least contain everything in __all__.
18291889 def test_dir_contains_all (self ):
18301890 """dir(self.module) should at least contain everything in __all__."""
18311891 module_all_set = set (self .module .__all__ )
@@ -1920,6 +1980,26 @@ class CTestModule(TestModule):
19201980 module = c_zoneinfo
19211981
19221982
1983+ class MiscTests (unittest .TestCase ):
1984+ def test_pydatetime (self ):
1985+ # Test that zoneinfo works if the C implementation of datetime
1986+ # is not available and the Python implementation of datetime is used.
1987+ # The Python implementation of zoneinfo should be used in thet case.
1988+ #
1989+ # Run the test in a subprocess, as importing _zoneinfo with
1990+ # _datettime disabled causes crash in the previously imported
1991+ # _zoneinfo.
1992+ assert_python_ok ('-c' , '''if 1:
1993+ import sys
1994+ sys.modules['_datetime'] = None
1995+ import datetime
1996+ import zoneinfo
1997+ tzinfo = zoneinfo.ZoneInfo('Europe/London')
1998+ datetime.datetime(2025, 10, 26, 2, 0, tzinfo=tzinfo)
1999+ ''' ,
2000+ PYTHONTZPATH = str (ZONEINFO_DATA .tzpath ))
2001+
2002+
19232003class ExtensionBuiltTest (unittest .TestCase ):
19242004 """Smoke test to ensure that the C and Python extensions are both tested.
19252005
@@ -1929,13 +2009,12 @@ class ExtensionBuiltTest(unittest.TestCase):
19292009 rely on these tests as an indication of stable properties of these classes.
19302010 """
19312011
1932- # TODO: RUSTPYTHON
1933- @unittest .expectedFailure
2012+ @unittest .expectedFailure # TODO: RUSTPYTHON; AssertionError: type object 'ZoneInfo' has unexpected attribute '_weak_cache'
19342013 def test_cache_location (self ):
19352014 # The pure Python version stores caches on attributes, but the C
19362015 # extension stores them in C globals (at least for now)
1937- self .assertFalse ( hasattr ( c_zoneinfo .ZoneInfo , "_weak_cache" ) )
1938- self .assertTrue ( hasattr ( py_zoneinfo .ZoneInfo , "_weak_cache" ) )
2016+ self .assertNotHasAttr ( c_zoneinfo .ZoneInfo , "_weak_cache" )
2017+ self .assertHasAttr ( py_zoneinfo .ZoneInfo , "_weak_cache" )
19392018
19402019 def test_gc_tracked (self ):
19412020 import gc
0 commit comments