From 2edda92d2d25a32a51c555dcba35ed85d2858e13 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Wed, 3 Sep 2025 03:55:43 -0700 Subject: [PATCH 1/8] zoneinfo: improve error message for PathLike relative paths --- Lib/test/test_zoneinfo/test_zoneinfo.py | 1 + Lib/zoneinfo/_tzpath.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index addc905af5ede9..19faa307a31c7d 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1784,6 +1784,7 @@ def test_reset_tzpath_relative_paths(self): ("/usr/share/zoneinfo", "../relative/path",), ("path/to/somewhere", "../relative/path",), ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",), + (pathlib.Path("path/to/somewhere"),) ] for input_paths in bad_values: with self.subTest(input_paths=input_paths): diff --git a/Lib/zoneinfo/_tzpath.py b/Lib/zoneinfo/_tzpath.py index 78fa6f00a8590a..558d0377fbd55f 100644 --- a/Lib/zoneinfo/_tzpath.py +++ b/Lib/zoneinfo/_tzpath.py @@ -57,7 +57,7 @@ def _parse_python_tzpath(env_var, stacklevel): def _get_invalid_paths_message(tzpaths): - invalid_paths = (path for path in tzpaths if not os.path.isabs(path)) + invalid_paths = (os.fspath(path) for path in tzpaths if not os.path.isabs(path)) prefix = "\n " indented_str = prefix + prefix.join(invalid_paths) From d52c8591f0e7a63b64bfb2f6d37c3a1bad38972b Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Wed, 3 Sep 2025 15:20:20 -0700 Subject: [PATCH 2/8] add NEWS entry --- .../Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst diff --git a/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst b/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst new file mode 100644 index 00000000000000..b13425baf2505d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst @@ -0,0 +1,4 @@ +:meth:`zoneinfo.reset_tzpath` will now raise ``ValueError`` if it encounters +an :class:`os.PathLike` object representing a relative path, along with a +more informative error message. It previously raised ``TypeError`` in this +case. From bb1addc0a39e761b0ce91753f1b69bc132beac56 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 5 Sep 2025 05:41:16 -0700 Subject: [PATCH 3/8] add a test for an absolute os.Pathlike in TZPATH --- Lib/test/test_zoneinfo/test_zoneinfo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 19faa307a31c7d..c9b0f11896c06c 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1806,6 +1806,7 @@ def test_tzpath_type_error(self): def test_tzpath_attribute(self): tzpath_0 = [f"{DRIVE}/one", f"{DRIVE}/two"] tzpath_1 = [f"{DRIVE}/three"] + tzpath_pathlike = (pathlib.Path("/usr/share/zoneinfo"),) with self.tzpath_context(tzpath_0): query_0 = self.module.TZPATH @@ -1813,8 +1814,12 @@ def test_tzpath_attribute(self): with self.tzpath_context(tzpath_1): query_1 = self.module.TZPATH + with self.tzpath_context(tzpath_pathlike): + query_pathlike = self.module.TZPATH + self.assertSequenceEqual(tzpath_0, query_0) self.assertSequenceEqual(tzpath_1, query_1) + self.assertSequenceEqual(tzpath_pathlike, query_pathlike) class CTzPathTest(TzPathTest): From 49bdcacca4b4243e2d6214d15d0feaf35ddd65d1 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Tue, 9 Sep 2025 19:40:17 -0700 Subject: [PATCH 4/8] check that all paths are strings --- Lib/zoneinfo/_tzpath.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/zoneinfo/_tzpath.py b/Lib/zoneinfo/_tzpath.py index 558d0377fbd55f..4746d52b0a1a48 100644 --- a/Lib/zoneinfo/_tzpath.py +++ b/Lib/zoneinfo/_tzpath.py @@ -13,6 +13,14 @@ def _reset_tzpath(to=None, stacklevel=4): + f"not {type(tzpaths)}: {tzpaths!r}" ) + tzpaths = [os.fspath(p) for p in tzpaths] + nonstr_paths = [p for p in tzpaths if not isinstance(p, str)] + if nonstr_paths: + raise TypeError( + "All elements of a tzpath sequence must be strings or " + "os.PathLike objects which convert to strings." + ) + if not all(map(os.path.isabs, tzpaths)): raise ValueError(_get_invalid_paths_message(tzpaths)) base_tzpath = tzpaths @@ -57,7 +65,7 @@ def _parse_python_tzpath(env_var, stacklevel): def _get_invalid_paths_message(tzpaths): - invalid_paths = (os.fspath(path) for path in tzpaths if not os.path.isabs(path)) + invalid_paths = (path for path in tzpaths if not os.path.isabs(path)) prefix = "\n " indented_str = prefix + prefix.join(invalid_paths) From 220f4c2509dd62297dc4afa2182495356ec4b530 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Tue, 9 Sep 2025 19:52:13 -0700 Subject: [PATCH 5/8] update NEWS entry --- Lib/test/test_zoneinfo/test_zoneinfo.py | 2 ++ .../2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index c9b0f11896c06c..284e1f06434517 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1796,6 +1796,8 @@ def test_tzpath_type_error(self): "/etc/zoneinfo:/usr/share/zoneinfo", b"/etc/zoneinfo:/usr/share/zoneinfo", 0, + (b"/bytes/path", "/valid/path"), + (pathlib.Path(b"/bytes/path"),) ] for bad_value in bad_values: diff --git a/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst b/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst index b13425baf2505d..b48b978a6d5c75 100644 --- a/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst +++ b/Misc/NEWS.d/next/Library/2025-09-03-15-20-10.gh-issue-138432.RMc7UX.rst @@ -1,4 +1,6 @@ -:meth:`zoneinfo.reset_tzpath` will now raise ``ValueError`` if it encounters -an :class:`os.PathLike` object representing a relative path, along with a -more informative error message. It previously raised ``TypeError`` in this -case. +:meth:`zoneinfo.reset_tzpath` will now convert any :class:`os.PathLike` objects +it receives into strings before adding them to ``TZPATH``. It will raise +``TypeError`` if anything other than a string is found after this conversion. +If given an :class:`os.PathLike` object that represents a relative path, it +will now raise ``ValueError`` instead of ``TypeError``, and present a more +informative error message. From 4499e961f095d10238d28a897be918f67447d818 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Tue, 9 Sep 2025 19:58:32 -0700 Subject: [PATCH 6/8] fix tests --- Lib/test/test_zoneinfo/test_zoneinfo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 284e1f06434517..05d95646f17f04 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1797,7 +1797,6 @@ def test_tzpath_type_error(self): b"/etc/zoneinfo:/usr/share/zoneinfo", 0, (b"/bytes/path", "/valid/path"), - (pathlib.Path(b"/bytes/path"),) ] for bad_value in bad_values: @@ -1821,7 +1820,7 @@ def test_tzpath_attribute(self): self.assertSequenceEqual(tzpath_0, query_0) self.assertSequenceEqual(tzpath_1, query_1) - self.assertSequenceEqual(tzpath_pathlike, query_pathlike) + self.assertSequenceEqual(tuple([os.fspath(p) for p in tzpath_pathlike]), query_pathlike) class CTzPathTest(TzPathTest): From 519625b4c9bfdd43a2b5281bb5b9790c903c7625 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Tue, 9 Sep 2025 23:04:21 -0700 Subject: [PATCH 7/8] fix tests on windows --- Lib/test/test_zoneinfo/test_zoneinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 05d95646f17f04..294fc1b5ec27e9 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1807,7 +1807,7 @@ def test_tzpath_type_error(self): def test_tzpath_attribute(self): tzpath_0 = [f"{DRIVE}/one", f"{DRIVE}/two"] tzpath_1 = [f"{DRIVE}/three"] - tzpath_pathlike = (pathlib.Path("/usr/share/zoneinfo"),) + tzpath_pathlike = (pathlib.Path("{DRIVE}/usr/share/zoneinfo"),) with self.tzpath_context(tzpath_0): query_0 = self.module.TZPATH From 7e029b288dfc9f3c819c085250b7cec7f764ef14 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Wed, 10 Sep 2025 20:27:57 -0700 Subject: [PATCH 8/8] code review --- Lib/test/test_zoneinfo/test_zoneinfo.py | 8 +++++--- Lib/zoneinfo/_tzpath.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 294fc1b5ec27e9..2f9a5dfc1a89a0 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -18,7 +18,7 @@ from functools import cached_property from test.support import MISSING_C_DOCSTRINGS -from test.support.os_helper import EnvironmentVarGuard +from test.support.os_helper import EnvironmentVarGuard, FakePath from test.test_zoneinfo import _support as test_support from test.test_zoneinfo._support import TZPATH_TEST_LOCK, ZoneInfoTestBase from test.support.import_helper import import_module, CleanImport @@ -1784,7 +1784,7 @@ def test_reset_tzpath_relative_paths(self): ("/usr/share/zoneinfo", "../relative/path",), ("path/to/somewhere", "../relative/path",), ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",), - (pathlib.Path("path/to/somewhere"),) + (FakePath("path/to/somewhere"),) ] for input_paths in bad_values: with self.subTest(input_paths=input_paths): @@ -1797,6 +1797,8 @@ def test_tzpath_type_error(self): b"/etc/zoneinfo:/usr/share/zoneinfo", 0, (b"/bytes/path", "/valid/path"), + (FakePath(b"/bytes/path"),), + (0,), ] for bad_value in bad_values: @@ -1807,7 +1809,7 @@ def test_tzpath_type_error(self): def test_tzpath_attribute(self): tzpath_0 = [f"{DRIVE}/one", f"{DRIVE}/two"] tzpath_1 = [f"{DRIVE}/three"] - tzpath_pathlike = (pathlib.Path("{DRIVE}/usr/share/zoneinfo"),) + tzpath_pathlike = (FakePath(f"{DRIVE}/usr/share/zoneinfo"),) with self.tzpath_context(tzpath_0): query_0 = self.module.TZPATH diff --git a/Lib/zoneinfo/_tzpath.py b/Lib/zoneinfo/_tzpath.py index 4746d52b0a1a48..177d32c35eff29 100644 --- a/Lib/zoneinfo/_tzpath.py +++ b/Lib/zoneinfo/_tzpath.py @@ -14,8 +14,7 @@ def _reset_tzpath(to=None, stacklevel=4): ) tzpaths = [os.fspath(p) for p in tzpaths] - nonstr_paths = [p for p in tzpaths if not isinstance(p, str)] - if nonstr_paths: + if not all(isinstance(p, str) for p in tzpaths): raise TypeError( "All elements of a tzpath sequence must be strings or " "os.PathLike objects which convert to strings."