Skip to content

Commit 255ff13

Browse files
committed
gh-136413: Add os.linkat() function
1 parent db699db commit 255ff13

10 files changed

Lines changed: 295 additions & 1 deletion

File tree

Doc/library/os.rst

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,39 @@ features:
20462046
If it's unavailable, using it will raise a :exc:`NotImplementedError`.
20472047

20482048

2049+
.. data:: AT_FDCWD
2050+
2051+
.. availability:: Linux.
2052+
2053+
.. data:: AT_EMPTY_PATH
2054+
2055+
If *src_path* is an empty string, create a link to the file referenced by
2056+
*src_dir_fd* (which may have been obtained using the open(2) :data:`O_PATH`
2057+
flag). In this case, *src_dir_fd* can refer to any type of file except a
2058+
directory. This will generally not work if the file has a link count of
2059+
zero (files created with :data:`O_TMPFILE` and without :data:`O_EXCL` are an
2060+
exception).
2061+
2062+
The caller must have the ``CAP_DAC_READ_SEARCH`` capability in order to use
2063+
this flag.
2064+
2065+
.. availability:: Linux >= 2.6.39.
2066+
2067+
.. data:: AT_SYMLINK_FOLLOW
2068+
2069+
By default, :func:`linkat`, does not dereference *src_path* if it is a
2070+
symbolic link (like :func:`link`). The flag :data:`!AT_SYMLINK_FOLLOW` can
2071+
be specified in flags to cause *src_path* to be dereferenced if it is a
2072+
symbolic link.
2073+
2074+
If procfs is mounted, this can be used as an alternative to
2075+
:data:`AT_EMPTY_PATH`, like this::
2076+
2077+
os.linkat(os.AT_FDCWD, "/proc/self/fd/<fd>",
2078+
dst_dir_fd, dst_name, os.AT_SYMLINK_FOLLOW)
2079+
2080+
.. availability:: Linux >= 2.6.18.
2081+
20492082

20502083
.. function:: access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True)
20512084

@@ -2354,6 +2387,36 @@ features:
23542387
Accepts a :term:`path-like object` for *src* and *dst*.
23552388

23562389

2390+
.. function:: linkat(src_dir_fd, src_path, dst_dir_fd, dst_path, flags, /)
2391+
2392+
The :func:`!linkat` function operates in exactly the same way as
2393+
:func:`link`, except for the differences described here.
2394+
2395+
If the pathname given in *src_path* is relative, then it is interpreted
2396+
relative to the directory referred to by the file descriptor *src_dir_fd*
2397+
(rather than relative to the current working directory of the calling
2398+
process, as is done by link() for a relative pathname).
2399+
2400+
If *src_path* is relative and *src_dir_fd* is the special value
2401+
:data:`AT_FDCWD`, then *src_path* is interpreted relative to the current
2402+
working directory of the calling process (like :func:`link`).
2403+
2404+
If *src_path* is absolute, then *src_dir_fd* is ignored.
2405+
2406+
The interpretation of *dst_path* is as for *src_path*, except that a
2407+
relative pathname is interpreted relative to the directory referred to
2408+
by the file descriptor *dst_dir_fd*.
2409+
2410+
The following values can be bitwise ORed in flags:
2411+
2412+
* :data:`AT_EMPTY_PATH`
2413+
* :data:`AT_SYMLINK_FOLLOW`
2414+
2415+
.. audit-event:: os.linkat src_dir_fd,src_path,dst_dir_fd,dst_path,flags os.linkat
2416+
2417+
.. availability:: Unix.
2418+
2419+
23572420
.. function:: listdir(path='.')
23582421

23592422
Return a list containing the names of the entries in the directory given by

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ math
119119
(Contributed by Bénédikt Tran in :gh:`135853`.)
120120

121121

122+
os
123+
--
124+
125+
* Added :func:`os.linkat` to create a hard link with flags.
126+
Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and
127+
:data:`os.AT_SYMLINK_FOLLOW`.
128+
(Contributed by Victor Stinner in :gh:`136413`.)
129+
122130
os.path
123131
-------
124132

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ struct _Py_global_strings {
410410
STRUCT_FOR_ID(dont_inherit)
411411
STRUCT_FOR_ID(dst)
412412
STRUCT_FOR_ID(dst_dir_fd)
413+
STRUCT_FOR_ID(dst_path)
413414
STRUCT_FOR_ID(eager_start)
414415
STRUCT_FOR_ID(effective_ids)
415416
STRUCT_FOR_ID(element_factory)
@@ -729,6 +730,7 @@ struct _Py_global_strings {
729730
STRUCT_FOR_ID(spam)
730731
STRUCT_FOR_ID(src)
731732
STRUCT_FOR_ID(src_dir_fd)
733+
STRUCT_FOR_ID(src_path)
732734
STRUCT_FOR_ID(stacklevel)
733735
STRUCT_FOR_ID(start)
734736
STRUCT_FOR_ID(statement)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_os.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,58 @@ def test_unicode_name(self):
26552655
self.file2 = self.file1 + "2"
26562656
self._test_link(self.file1, self.file2)
26572657

2658+
2659+
@unittest.skipUnless(hasattr(os, 'linkat'), 'requires os.linkat')
2660+
class LinkAtTests(unittest.TestCase):
2661+
def test_no_flags(self):
2662+
# create hard link with no flags
2663+
src = "linkat_src"
2664+
self.addCleanup(os_helper.unlink, src)
2665+
with open(src, "w", encoding='utf8') as fp:
2666+
fp.write("hello")
2667+
2668+
dst = "linkat_dst"
2669+
self.addCleanup(os_helper.unlink, dst)
2670+
os.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0
2671+
2672+
with open(dst, encoding='utf8') as fp:
2673+
self.assertEqual(fp.read(), 'hello')
2674+
2675+
# destination already exists
2676+
src2 = "linkat_src2"
2677+
self.addCleanup(os_helper.unlink, src2)
2678+
with open(src2, "w", encoding='utf8') as fp:
2679+
fp.write("PYTHON")
2680+
2681+
with self.assertRaises(FileExistsError):
2682+
os.linkat(os.AT_FDCWD, src2, os.AT_FDCWD, dst) # flags=0
2683+
2684+
def check_flag(self, flag):
2685+
filename = os_helper.TESTFN
2686+
self.addCleanup(os_helper.unlink, filename)
2687+
2688+
fd = os.open(os.path.curdir, os.O_WRONLY | os.O_TMPFILE, 0o600)
2689+
os.write(fd, b"hello")
2690+
if flag == os.AT_EMPTY_PATH:
2691+
os.linkat(fd, b"",
2692+
os.AT_FDCWD, filename,
2693+
os.AT_EMPTY_PATH)
2694+
else:
2695+
os.linkat(fd, f"/proc/self/fd/{fd}",
2696+
os.AT_FDCWD, filename,
2697+
os.AT_SYMLINK_FOLLOW)
2698+
os.close(fd)
2699+
2700+
with open(filename, encoding='utf8') as fp:
2701+
self.assertEqual(fp.read(), 'hello')
2702+
2703+
def test_empty_path(self):
2704+
self.check_flag(os.AT_EMPTY_PATH)
2705+
2706+
def test_symlink_follow(self):
2707+
self.check_flag(os.AT_SYMLINK_FOLLOW)
2708+
2709+
26582710
@unittest.skipIf(sys.platform == "win32", "Posix specific tests")
26592711
class PosixUidGidTests(unittest.TestCase):
26602712
# uid_t and gid_t are 32-bit unsigned integers on Linux
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added :func:`os.linkat` to create a hard link with flags. Patch by Victor
2+
Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and
3+
:data:`os.AT_SYMLINK_FOLLOW`.
4+
Stinner.

Modules/clinic/posixmodule.c.h

Lines changed: 97 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)