Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Doc/c-api/function.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 0 additions & 10 deletions Doc/tools/templates/download.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,6 @@ <h1>{% trans %}Download Python {{ dl_version }} documentation{% endtrans %}</h1>
See the <a href="https://docs.python.org/{{ version }}/archives/">directory listing</a>
for file sizes.{% endtrans %}</p>

<h2>{% trans %}Unpacking{% endtrans %}</h2>

<p>{% 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 <a href="https://infozip.sourceforge.net">Info-ZIP</a> 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 %}</p>

<p>{% 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 %}</p>

<h2>{% trans %}Problems{% endtrans %}</h2>
{% set bugs = pathto('bugs') %}
Expand Down
3 changes: 2 additions & 1 deletion Include/cpython/funcobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 6 additions & 8 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions Lib/test/test_pathlib/test_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Function watchers can now receive a PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME event when a watched functions qualname is changed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix race condition in :class:`pathlib.Path` on the internal ``_raw_paths``
field.
2 changes: 2 additions & 0 deletions Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
Expand Down
Loading