From 49aaee7978c54211967392678072accc403d15f2 Mon Sep 17 00:00:00 2001
From: Barney Gale
Date: Fri, 10 Oct 2025 19:08:55 +0100
Subject: [PATCH 1/4] pathlib ABCs: restore `relative_to()` and
`is_relative_to()` (#138853)
Restore `JoinablePath.[is_]relative_to()`, which were deleted in
ef63cca494571f50906baae1d176469a3dcf8838. These methods are too useful to
forgo. Restore old tests, and add new tests covering path classes with
non-overridden `__eq__()` and `__hash__()`.
Slightly simplify `PurePath.relative_to()` while we're in the area.
No change to public APIs, because the pathlib ABCs are still private.
---
Lib/pathlib/__init__.py | 7 ++--
Lib/pathlib/types.py | 27 +++++++++++++++
Lib/test/test_pathlib/test_join.py | 55 ++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+), 2 deletions(-)
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py
index 8a892102cc00ea..6c07cd9ab010ad 100644
--- a/Lib/pathlib/__init__.py
+++ b/Lib/pathlib/__init__.py
@@ -490,16 +490,19 @@ def relative_to(self, other, *, walk_up=False):
"""
if not hasattr(other, 'with_segments'):
other = self.with_segments(other)
- for step, path in enumerate(chain([other], other.parents)):
+ parts = []
+ for path in chain([other], other.parents):
if path == self or path in self.parents:
break
elif not walk_up:
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
+ else:
+ parts.append('..')
else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
- parts = ['..'] * step + self._tail[len(path._tail):]
+ parts.extend(self._tail[len(path._tail):])
return self._from_parsed_parts('', '', parts)
def is_relative_to(self, other):
diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py
index fea0dd305fe2a3..f21ce0774548f8 100644
--- a/Lib/pathlib/types.py
+++ b/Lib/pathlib/types.py
@@ -234,6 +234,33 @@ def parents(self):
parent = split(path)[0]
return tuple(parents)
+ def relative_to(self, other, *, walk_up=False):
+ """Return the relative path to another path identified by the passed
+ arguments. If the operation is not possible (because this is not
+ related to the other path), raise ValueError.
+
+ The *walk_up* parameter controls whether `..` may be used to resolve
+ the path.
+ """
+ parts = []
+ for path in (other,) + other.parents:
+ if self.is_relative_to(path):
+ break
+ elif not walk_up:
+ raise ValueError(f"{self!r} is not in the subpath of {other!r}")
+ elif path.name == '..':
+ raise ValueError(f"'..' segment in {other!r} cannot be walked")
+ else:
+ parts.append('..')
+ else:
+ raise ValueError(f"{self!r} and {other!r} have different anchors")
+ return self.with_segments(*parts, *self.parts[len(path.parts):])
+
+ def is_relative_to(self, other):
+ """Return True if the path is relative to another path or False.
+ """
+ return other == self or other in self.parents
+
def full_match(self, pattern):
"""
Return True if this path matches the given glob-style pattern. The
diff --git a/Lib/test/test_pathlib/test_join.py b/Lib/test/test_pathlib/test_join.py
index f1a24204b4c30a..2f4e79345f3652 100644
--- a/Lib/test/test_pathlib/test_join.py
+++ b/Lib/test/test_pathlib/test_join.py
@@ -354,6 +354,61 @@ def test_with_suffix(self):
self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
self.assertRaises(TypeError, P('a/b').with_suffix, None)
+ def test_relative_to(self):
+ P = self.cls
+ p = P('a/b')
+ self.assertEqual(p.relative_to(P('')), P('a', 'b'))
+ self.assertEqual(p.relative_to(P('a')), P('b'))
+ self.assertEqual(p.relative_to(P('a/b')), P(''))
+ self.assertEqual(p.relative_to(P(''), walk_up=True), P('a', 'b'))
+ self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b'))
+ self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P(''))
+ self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('..', 'b'))
+ self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..'))
+ self.assertEqual(p.relative_to(P('c'), walk_up=True), P('..', 'a', 'b'))
+ self.assertRaises(ValueError, p.relative_to, P('c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/b/c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/c'))
+ self.assertRaises(ValueError, p.relative_to, P('/a'))
+ self.assertRaises(ValueError, p.relative_to, P('../a'))
+ self.assertRaises(ValueError, p.relative_to, P('a/..'))
+ self.assertRaises(ValueError, p.relative_to, P('/a/..'))
+ self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True)
+ class Q(self.cls):
+ __eq__ = object.__eq__
+ __hash__ = object.__hash__
+ q = Q('a/b')
+ self.assertTrue(q.relative_to(q))
+ self.assertRaises(ValueError, q.relative_to, Q(''))
+ self.assertRaises(ValueError, q.relative_to, Q('a'))
+ self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True)
+ self.assertRaises(ValueError, q.relative_to, Q('a/b'))
+ self.assertRaises(ValueError, q.relative_to, Q('c'))
+
+ def test_is_relative_to(self):
+ P = self.cls
+ p = P('a/b')
+ self.assertTrue(p.is_relative_to(P('')))
+ self.assertTrue(p.is_relative_to(P('a')))
+ self.assertTrue(p.is_relative_to(P('a/b')))
+ self.assertFalse(p.is_relative_to(P('c')))
+ self.assertFalse(p.is_relative_to(P('a/b/c')))
+ self.assertFalse(p.is_relative_to(P('a/c')))
+ self.assertFalse(p.is_relative_to(P('/a')))
+ class Q(self.cls):
+ __eq__ = object.__eq__
+ __hash__ = object.__hash__
+ q = Q('a/b')
+ self.assertTrue(q.is_relative_to(q))
+ self.assertFalse(q.is_relative_to(Q('')))
+ self.assertFalse(q.is_relative_to(Q('a')))
+ self.assertFalse(q.is_relative_to(Q('a/b')))
+ self.assertFalse(q.is_relative_to(Q('c')))
+
class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPath
From aa840f500ca901deea29669df68992193a273a62 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Fri, 10 Oct 2025 20:22:34 +0100
Subject: [PATCH 2/4] gh-138843: Removing "Unpacking" section from Download
page (GH-139918)
---
Doc/tools/templates/download.html | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/Doc/tools/templates/download.html b/Doc/tools/templates/download.html
index 523b505f59673e..f914ad862116d7 100644
--- a/Doc/tools/templates/download.html
+++ b/Doc/tools/templates/download.html
@@ -75,16 +75,6 @@ {% trans %}Download Python {{ dl_version }} documentation{% endtrans %}
See the directory listing
for file sizes.{% endtrans %}
-{% trans %}Unpacking{% endtrans %}
-
-{% trans %}Unix users should download the .tar.bz2 archives; these are bzipped tar
-archives and can be handled in the usual way using tar and the bzip2
-program. The Info-ZIP unzip program can be
-used to handle the ZIP archives if desired. The .tar.bz2 archives provide the
-best compression and fastest download times.{% endtrans %}
-
-{% trans %}Windows users can use the ZIP archives since those are customary on that
-platform. These are created on Unix using the Info-ZIP zip program.{% endtrans %}
{% trans %}Problems{% endtrans %}
{% set bugs = pathto('bugs') %}
From d9b4eef71e7904fbe3a3786a908e493be7debbff Mon Sep 17 00:00:00 2001
From: Sam Gross
Date: Fri, 10 Oct 2025 17:20:18 -0400
Subject: [PATCH 3/4] gh-139001: Fix thread-safety issue in `pathlib.Path`
(gh-139066)
Don't cache the joined path in `_raw_path` because the caching isn't thread safe.
---
Lib/pathlib/__init__.py | 7 +-----
Lib/test/test_pathlib/test_join.py | 22 +++++++++++++++++++
...-09-17-12-07-21.gh-issue-139001.O6tseN.rst | 2 ++
3 files changed, 25 insertions(+), 6 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-09-17-12-07-21.gh-issue-139001.O6tseN.rst
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py
index 6c07cd9ab010ad..51359cec8b0f9e 100644
--- a/Lib/pathlib/__init__.py
+++ b/Lib/pathlib/__init__.py
@@ -336,13 +336,8 @@ def _raw_path(self):
return paths[0]
elif paths:
# Join path segments from the initializer.
- path = self.parser.join(*paths)
- # Cache the joined path.
- paths.clear()
- paths.append(path)
- return path
+ return self.parser.join(*paths)
else:
- paths.append('')
return ''
@property
diff --git a/Lib/test/test_pathlib/test_join.py b/Lib/test/test_pathlib/test_join.py
index 2f4e79345f3652..4630210e492291 100644
--- a/Lib/test/test_pathlib/test_join.py
+++ b/Lib/test/test_pathlib/test_join.py
@@ -3,6 +3,8 @@
"""
import unittest
+import threading
+from test.support import threading_helper
from .support import is_pypi
from .support.lexical_path import LexicalPath
@@ -158,6 +160,26 @@ def test_parts(self):
parts = p.parts
self.assertEqual(parts, (sep, 'a', 'b'))
+ @threading_helper.requires_working_threading()
+ def test_parts_multithreaded(self):
+ P = self.cls
+
+ NUM_THREADS = 10
+ NUM_ITERS = 10
+
+ for _ in range(NUM_ITERS):
+ b = threading.Barrier(NUM_THREADS)
+ path = P('a') / 'b' / 'c' / 'd' / 'e'
+ expected = ('a', 'b', 'c', 'd', 'e')
+
+ def check_parts():
+ b.wait()
+ self.assertEqual(path.parts, expected)
+
+ threads = [threading.Thread(target=check_parts) for _ in range(NUM_THREADS)]
+ with threading_helper.start_threads(threads):
+ pass
+
def test_parent(self):
# Relative
P = self.cls
diff --git a/Misc/NEWS.d/next/Library/2025-09-17-12-07-21.gh-issue-139001.O6tseN.rst b/Misc/NEWS.d/next/Library/2025-09-17-12-07-21.gh-issue-139001.O6tseN.rst
new file mode 100644
index 00000000000000..3ad5a1272df4af
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-17-12-07-21.gh-issue-139001.O6tseN.rst
@@ -0,0 +1,2 @@
+Fix race condition in :class:`pathlib.Path` on the internal ``_raw_paths``
+field.
From ff7bb565d836162eed0851c36afa325a107a5a56 Mon Sep 17 00:00:00 2001
From: Dino Viehland
Date: Fri, 10 Oct 2025 15:25:38 -0700
Subject: [PATCH 4/4] gh-139924: Add PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME
event for function watchers (#139925)
Add PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME event for function watchers
---
Doc/c-api/function.rst | 3 +++
Include/cpython/funcobject.h | 3 ++-
Lib/test/test_capi/test_watchers.py | 4 ++++
.../next/C_API/2025-10-10-20-59-07.gh-issue-139924.ALByCb.rst | 1 +
Objects/funcobject.c | 2 ++
5 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-10-10-20-59-07.gh-issue-139924.ALByCb.rst
diff --git a/Doc/c-api/function.rst b/Doc/c-api/function.rst
index 5fb8567ef8c95f..764b2ac610be4d 100644
--- a/Doc/c-api/function.rst
+++ b/Doc/c-api/function.rst
@@ -175,6 +175,9 @@ There are a few functions specific to Python functions.
.. versionadded:: 3.12
+ - ``PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME``
+
+ .. versionadded:: 3.15
.. c:type:: int (*PyFunction_WatchCallback)(PyFunction_WatchEvent event, PyFunctionObject *func, PyObject *new_value)
diff --git a/Include/cpython/funcobject.h b/Include/cpython/funcobject.h
index 598cd330bc9ca9..9e1599a7648564 100644
--- a/Include/cpython/funcobject.h
+++ b/Include/cpython/funcobject.h
@@ -134,7 +134,8 @@ PyAPI_FUNC(PyObject *) PyStaticMethod_New(PyObject *);
V(DESTROY) \
V(MODIFY_CODE) \
V(MODIFY_DEFAULTS) \
- V(MODIFY_KWDEFAULTS)
+ V(MODIFY_KWDEFAULTS) \
+ V(MODIFY_QUALNAME)
typedef enum {
#define PY_DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT,
diff --git a/Lib/test/test_capi/test_watchers.py b/Lib/test/test_capi/test_watchers.py
index 8644479d83d5ed..bef72032513da5 100644
--- a/Lib/test/test_capi/test_watchers.py
+++ b/Lib/test/test_capi/test_watchers.py
@@ -514,6 +514,10 @@ def myfunc():
_testcapi.set_func_kwdefaults_via_capi(myfunc, new_kwdefaults)
self.assertIn((_testcapi.PYFUNC_EVENT_MODIFY_KWDEFAULTS, myfunc, new_kwdefaults), events)
+ new_qualname = "foo.bar"
+ myfunc.__qualname__ = new_qualname
+ self.assertIn((_testcapi.PYFUNC_EVENT_MODIFY_QUALNAME, myfunc, new_qualname), events)
+
# Clear events reference to func
events = []
del myfunc
diff --git a/Misc/NEWS.d/next/C_API/2025-10-10-20-59-07.gh-issue-139924.ALByCb.rst b/Misc/NEWS.d/next/C_API/2025-10-10-20-59-07.gh-issue-139924.ALByCb.rst
new file mode 100644
index 00000000000000..a53d5d068d7d86
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-10-10-20-59-07.gh-issue-139924.ALByCb.rst
@@ -0,0 +1 @@
+Function watchers can now receive a PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME event when a watched functions qualname is changed.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index d8a10075578087..43198aaf8a7048 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -62,6 +62,7 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func,
case PyFunction_EVENT_MODIFY_CODE:
case PyFunction_EVENT_MODIFY_DEFAULTS:
case PyFunction_EVENT_MODIFY_KWDEFAULTS:
+ case PyFunction_EVENT_MODIFY_QUALNAME:
RARE_EVENT_INTERP_INC(interp, func_modification);
break;
default:
@@ -747,6 +748,7 @@ func_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))
"__qualname__ must be set to a string object");
return -1;
}
+ handle_func_event(PyFunction_EVENT_MODIFY_QUALNAME, (PyFunctionObject *) op, value);
Py_XSETREF(op->func_qualname, Py_NewRef(value));
return 0;
}