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/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') %} 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/pathlib/__init__.py b/Lib/pathlib/__init__.py index 8a892102cc00ea..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 @@ -490,16 +485,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_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/Lib/test/test_pathlib/test_join.py b/Lib/test/test_pathlib/test_join.py index f1a24204b4c30a..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 @@ -354,6 +376,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 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/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. 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; }