Skip to content

Commit 6534a36

Browse files
authored
Merge branch 'main' into gh-119186-joining
2 parents e5f62ec + db00fee commit 6534a36

23 files changed

+262
-87
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Include/internal/pycore_freelist.h @ericsnowcurrently
7272
Include/internal/pycore_global_objects.h @ericsnowcurrently
7373
Include/internal/pycore_obmalloc.h @ericsnowcurrently
7474
Include/internal/pycore_pymem.h @ericsnowcurrently
75+
Include/internal/pycore_stackref.h @Fidget-Spinner
7576
Modules/main.c @ericsnowcurrently
7677
Programs/_bootstrap_python.c @ericsnowcurrently
7778
Programs/python.c @ericsnowcurrently

Doc/c-api/object.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Object Protocol
5252
5353
The reference is borrowed from the interpreter, and is valid until the
5454
interpreter finalization.
55+
5556
.. versionadded:: 3.13
5657
5758

Doc/library/os.path.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ the :mod:`glob` module.)
389389
that contains symbolic links. On Windows, it converts forward slashes to
390390
backward slashes. To normalize case, use :func:`normcase`.
391391

392-
.. note::
392+
.. note::
393393
On POSIX systems, in accordance with `IEEE Std 1003.1 2013 Edition; 4.13
394394
Pathname Resolution <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13>`_,
395395
if a pathname begins with exactly two slashes, the first component

Doc/library/pathlib.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,7 +1539,7 @@ Creating files and directories
15391539
Copying, renaming and deleting
15401540
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15411541

1542-
.. method:: Path.copy(target, *, follow_symlinks=True)
1542+
.. method:: Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)
15431543

15441544
Copy the contents of this file to the *target* file. If *target* specifies
15451545
a file that already exists, it will be replaced.
@@ -1548,11 +1548,11 @@ Copying, renaming and deleting
15481548
will be created as a symbolic link. If *follow_symlinks* is true and this
15491549
file is a symbolic link, *target* will be a copy of the symlink target.
15501550

1551-
.. note::
1552-
This method uses operating system functionality to copy file content
1553-
efficiently. The OS might also copy some metadata, such as file
1554-
permissions. After the copy is complete, users may wish to call
1555-
:meth:`Path.chmod` to set the permissions of the target file.
1551+
If *preserve_metadata* is false (the default), only the file data is
1552+
guaranteed to be copied. Set *preserve_metadata* to true to ensure that the
1553+
file mode (permissions), flags, last access and modification times, and
1554+
extended attributes are copied where supported. This argument has no effect
1555+
on Windows, where metadata is always preserved when copying.
15561556

15571557
.. versionadded:: 3.14
15581558

Doc/requirements-oldest-sphinx.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ python-docs-theme>=2022.1
1414

1515
alabaster==0.7.16
1616
Babel==2.15.0
17-
certifi==2024.6.2
17+
certifi==2024.7.4
1818
charset-normalizer==3.3.2
1919
docutils==0.19
2020
idna==3.7

Lib/asyncio/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def run(self):
116116
if err := check():
117117
raise RuntimeError(err)
118118
except Exception as e:
119-
console.interact(banner="", exitmsg=exit_message)
119+
console.interact(banner="", exitmsg="")
120120
else:
121121
try:
122122
run_multiline_interactive_console(console=console)

Lib/os.py

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -373,61 +373,45 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
373373
# minor reason when (say) a thousand readable directories are still
374374
# left to visit.
375375
try:
376-
scandir_it = scandir(top)
376+
with scandir(top) as entries:
377+
for entry in entries:
378+
try:
379+
if followlinks is _walk_symlinks_as_files:
380+
is_dir = entry.is_dir(follow_symlinks=False) and not entry.is_junction()
381+
else:
382+
is_dir = entry.is_dir()
383+
except OSError:
384+
# If is_dir() raises an OSError, consider the entry not to
385+
# be a directory, same behaviour as os.path.isdir().
386+
is_dir = False
387+
388+
if is_dir:
389+
dirs.append(entry.name)
390+
else:
391+
nondirs.append(entry.name)
392+
393+
if not topdown and is_dir:
394+
# Bottom-up: traverse into sub-directory, but exclude
395+
# symlinks to directories if followlinks is False
396+
if followlinks:
397+
walk_into = True
398+
else:
399+
try:
400+
is_symlink = entry.is_symlink()
401+
except OSError:
402+
# If is_symlink() raises an OSError, consider the
403+
# entry not to be a symbolic link, same behaviour
404+
# as os.path.islink().
405+
is_symlink = False
406+
walk_into = not is_symlink
407+
408+
if walk_into:
409+
walk_dirs.append(entry.path)
377410
except OSError as error:
378411
if onerror is not None:
379412
onerror(error)
380413
continue
381414

382-
cont = False
383-
with scandir_it:
384-
while True:
385-
try:
386-
try:
387-
entry = next(scandir_it)
388-
except StopIteration:
389-
break
390-
except OSError as error:
391-
if onerror is not None:
392-
onerror(error)
393-
cont = True
394-
break
395-
396-
try:
397-
if followlinks is _walk_symlinks_as_files:
398-
is_dir = entry.is_dir(follow_symlinks=False) and not entry.is_junction()
399-
else:
400-
is_dir = entry.is_dir()
401-
except OSError:
402-
# If is_dir() raises an OSError, consider the entry not to
403-
# be a directory, same behaviour as os.path.isdir().
404-
is_dir = False
405-
406-
if is_dir:
407-
dirs.append(entry.name)
408-
else:
409-
nondirs.append(entry.name)
410-
411-
if not topdown and is_dir:
412-
# Bottom-up: traverse into sub-directory, but exclude
413-
# symlinks to directories if followlinks is False
414-
if followlinks:
415-
walk_into = True
416-
else:
417-
try:
418-
is_symlink = entry.is_symlink()
419-
except OSError:
420-
# If is_symlink() raises an OSError, consider the
421-
# entry not to be a symbolic link, same behaviour
422-
# as os.path.islink().
423-
is_symlink = False
424-
walk_into = not is_symlink
425-
426-
if walk_into:
427-
walk_dirs.append(entry.path)
428-
if cont:
429-
continue
430-
431415
if topdown:
432416
# Yield before sub-directory traversal if going top down
433417
yield top, dirs, nondirs

Lib/pathlib/_abc.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,32 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
781781
"""
782782
raise UnsupportedOperation(self._unsupported_msg('mkdir()'))
783783

784-
def copy(self, target, follow_symlinks=True):
784+
# Metadata keys supported by this path type.
785+
_readable_metadata = _writable_metadata = frozenset()
786+
787+
def _read_metadata(self, keys=None, *, follow_symlinks=True):
788+
"""
789+
Returns path metadata as a dict with string keys.
790+
"""
791+
raise UnsupportedOperation(self._unsupported_msg('_read_metadata()'))
792+
793+
def _write_metadata(self, metadata, *, follow_symlinks=True):
794+
"""
795+
Sets path metadata from the given dict with string keys.
796+
"""
797+
raise UnsupportedOperation(self._unsupported_msg('_write_metadata()'))
798+
799+
def _copy_metadata(self, target, *, follow_symlinks=True):
800+
"""
801+
Copies metadata (permissions, timestamps, etc) from this path to target.
802+
"""
803+
# Metadata types supported by both source and target.
804+
keys = self._readable_metadata & target._writable_metadata
805+
if keys:
806+
metadata = self._read_metadata(keys, follow_symlinks=follow_symlinks)
807+
target._write_metadata(metadata, follow_symlinks=follow_symlinks)
808+
809+
def copy(self, target, *, follow_symlinks=True, preserve_metadata=False):
785810
"""
786811
Copy the contents of this file to the given target. If this file is a
787812
symlink and follow_symlinks is false, a symlink will be created at the
@@ -793,6 +818,8 @@ def copy(self, target, follow_symlinks=True):
793818
raise OSError(f"{self!r} and {target!r} are the same file")
794819
if not follow_symlinks and self.is_symlink():
795820
target.symlink_to(self.readlink())
821+
if preserve_metadata:
822+
self._copy_metadata(target, follow_symlinks=False)
796823
return
797824
with self.open('rb') as source_f:
798825
try:
@@ -805,6 +832,8 @@ def copy(self, target, follow_symlinks=True):
805832
f'Directory does not exist: {target}') from e
806833
else:
807834
raise
835+
if preserve_metadata:
836+
self._copy_metadata(target)
808837

809838
def copytree(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
810839
ignore=None, on_error=None):

Lib/pathlib/_local.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
except ImportError:
1818
grp = None
1919

20-
from ._os import UnsupportedOperation, copyfile
20+
from ._os import (UnsupportedOperation, copyfile, file_metadata_keys,
21+
read_file_metadata, write_file_metadata)
2122
from ._abc import PurePathBase, PathBase
2223

2324

@@ -781,8 +782,12 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
781782
if not exist_ok or not self.is_dir():
782783
raise
783784

785+
_readable_metadata = _writable_metadata = file_metadata_keys
786+
_read_metadata = read_file_metadata
787+
_write_metadata = write_file_metadata
788+
784789
if copyfile:
785-
def copy(self, target, follow_symlinks=True):
790+
def copy(self, target, *, follow_symlinks=True, preserve_metadata=False):
786791
"""
787792
Copy the contents of this file to the given target. If this file is a
788793
symlink and follow_symlinks is false, a symlink will be created at the
@@ -799,7 +804,8 @@ def copy(self, target, follow_symlinks=True):
799804
return
800805
except UnsupportedOperation:
801806
pass # Fall through to generic code.
802-
PathBase.copy(self, target, follow_symlinks=follow_symlinks)
807+
PathBase.copy(self, target, follow_symlinks=follow_symlinks,
808+
preserve_metadata=preserve_metadata)
803809

804810
def chmod(self, mode, *, follow_symlinks=True):
805811
"""

Lib/pathlib/_os.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Low-level OS functionality wrappers used by pathlib.
33
"""
44

5-
from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
5+
from errno import *
66
import os
77
import stat
88
import sys
@@ -178,3 +178,100 @@ def copyfileobj(source_f, target_f):
178178
write_target = target_f.write
179179
while buf := read_source(1024 * 1024):
180180
write_target(buf)
181+
182+
183+
# Kinds of metadata supported by the operating system.
184+
file_metadata_keys = {'mode', 'times_ns'}
185+
if hasattr(os.stat_result, 'st_flags'):
186+
file_metadata_keys.add('flags')
187+
if hasattr(os, 'listxattr'):
188+
file_metadata_keys.add('xattrs')
189+
file_metadata_keys = frozenset(file_metadata_keys)
190+
191+
192+
def read_file_metadata(path, keys=None, *, follow_symlinks=True):
193+
"""
194+
Returns local path metadata as a dict with string keys.
195+
"""
196+
if keys is None:
197+
keys = file_metadata_keys
198+
assert keys.issubset(file_metadata_keys)
199+
result = {}
200+
for key in keys:
201+
if key == 'xattrs':
202+
try:
203+
result['xattrs'] = [
204+
(attr, os.getxattr(path, attr, follow_symlinks=follow_symlinks))
205+
for attr in os.listxattr(path, follow_symlinks=follow_symlinks)]
206+
except OSError as err:
207+
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
208+
raise
209+
continue
210+
st = os.stat(path, follow_symlinks=follow_symlinks)
211+
if key == 'mode':
212+
result['mode'] = stat.S_IMODE(st.st_mode)
213+
elif key == 'times_ns':
214+
result['times_ns'] = st.st_atime_ns, st.st_mtime_ns
215+
elif key == 'flags':
216+
result['flags'] = st.st_flags
217+
return result
218+
219+
220+
def write_file_metadata(path, metadata, *, follow_symlinks=True):
221+
"""
222+
Sets local path metadata from the given dict with string keys.
223+
"""
224+
assert frozenset(metadata.keys()).issubset(file_metadata_keys)
225+
226+
def _nop(*args, ns=None, follow_symlinks=None):
227+
pass
228+
229+
if follow_symlinks:
230+
# use the real function if it exists
231+
def lookup(name):
232+
return getattr(os, name, _nop)
233+
else:
234+
# use the real function only if it exists
235+
# *and* it supports follow_symlinks
236+
def lookup(name):
237+
fn = getattr(os, name, _nop)
238+
if fn in os.supports_follow_symlinks:
239+
return fn
240+
return _nop
241+
242+
times_ns = metadata.get('times_ns')
243+
if times_ns is not None:
244+
lookup("utime")(path, ns=times_ns, follow_symlinks=follow_symlinks)
245+
# We must copy extended attributes before the file is (potentially)
246+
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
247+
xattrs = metadata.get('xattrs')
248+
if xattrs is not None:
249+
for attr, value in xattrs:
250+
try:
251+
os.setxattr(path, attr, value, follow_symlinks=follow_symlinks)
252+
except OSError as e:
253+
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
254+
raise
255+
mode = metadata.get('mode')
256+
if mode is not None:
257+
try:
258+
lookup("chmod")(path, mode, follow_symlinks=follow_symlinks)
259+
except NotImplementedError:
260+
# if we got a NotImplementedError, it's because
261+
# * follow_symlinks=False,
262+
# * lchown() is unavailable, and
263+
# * either
264+
# * fchownat() is unavailable or
265+
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
266+
# (it returned ENOSUP.)
267+
# therefore we're out of options--we simply cannot chown the
268+
# symlink. give up, suppress the error.
269+
# (which is what shutil always did in this circumstance.)
270+
pass
271+
flags = metadata.get('flags')
272+
if flags is not None:
273+
try:
274+
lookup("chflags")(path, flags, follow_symlinks=follow_symlinks)
275+
except OSError as why:
276+
if why.errno not in (EOPNOTSUPP, ENOTSUP):
277+
raise

0 commit comments

Comments
 (0)