Skip to content

gh-136413: Add os.linkat() function #136417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
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
63 changes: 63 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,39 @@ features:
If it's unavailable, using it will raise a :exc:`NotImplementedError`.


.. data:: AT_FDCWD

.. availability:: Linux.

.. data:: AT_EMPTY_PATH

If *src_path* is an empty string, create a link to the file referenced by
*src_dir_fd* (which may have been obtained using the open(2) :data:`O_PATH`
flag). In this case, *src_dir_fd* can refer to any type of file except a
directory. This will generally not work if the file has a link count of
zero (files created with :data:`O_TMPFILE` and without :data:`O_EXCL` are an
exception).

The caller must have the ``CAP_DAC_READ_SEARCH`` capability in order to use
this flag.

.. availability:: Linux >= 2.6.39.

.. data:: AT_SYMLINK_FOLLOW

By default, :func:`linkat`, does not dereference *src_path* if it is a
symbolic link (like :func:`link`). The flag :data:`!AT_SYMLINK_FOLLOW` can
be specified in flags to cause *src_path* to be dereferenced if it is a
symbolic link.

If procfs is mounted, this can be used as an alternative to
:data:`AT_EMPTY_PATH`, like this::

os.linkat(os.AT_FDCWD, "/proc/self/fd/<fd>",
dst_dir_fd, dst_name, os.AT_SYMLINK_FOLLOW)

.. availability:: Linux >= 2.6.18.


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

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


.. function:: linkat(src_dir_fd, src_path, dst_dir_fd, dst_path, flags, /)

The :func:`!linkat` function operates in exactly the same way as
:func:`link`, except for the differences described here.

If the pathname given in *src_path* is relative, then it is interpreted
relative to the directory referred to by the file descriptor *src_dir_fd*
(rather than relative to the current working directory of the calling
process, as is done by link() for a relative pathname).

If *src_path* is relative and *src_dir_fd* is the special value
:data:`AT_FDCWD`, then *src_path* is interpreted relative to the current
working directory of the calling process (like :func:`link`).

If *src_path* is absolute, then *src_dir_fd* is ignored.

The interpretation of *dst_path* is as for *src_path*, except that a
relative pathname is interpreted relative to the directory referred to
by the file descriptor *dst_dir_fd*.

The following values can be bitwise ORed in flags:

* :data:`AT_EMPTY_PATH`
* :data:`AT_SYMLINK_FOLLOW`

.. audit-event:: os.linkat src_dir_fd,src_path,dst_dir_fd,dst_path,flags os.linkat

.. availability:: Unix.


.. function:: listdir(path='.')

Return a list containing the names of the entries in the directory given by
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ math
(Contributed by Bénédikt Tran in :gh:`135853`.)


os
--

* Added :func:`os.linkat` to create a hard link with flags.
Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and
:data:`os.AT_SYMLINK_FOLLOW`.
(Contributed by Victor Stinner in :gh:`136413`.)

os.path
-------

Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(dont_inherit)
STRUCT_FOR_ID(dst)
STRUCT_FOR_ID(dst_dir_fd)
STRUCT_FOR_ID(dst_path)
STRUCT_FOR_ID(eager_start)
STRUCT_FOR_ID(effective_ids)
STRUCT_FOR_ID(element_factory)
Expand Down Expand Up @@ -729,6 +730,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(spam)
STRUCT_FOR_ID(src)
STRUCT_FOR_ID(src_dir_fd)
STRUCT_FOR_ID(src_path)
STRUCT_FOR_ID(stacklevel)
STRUCT_FOR_ID(start)
STRUCT_FOR_ID(statement)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -2655,6 +2655,74 @@ def test_unicode_name(self):
self.file2 = self.file1 + "2"
self._test_link(self.file1, self.file2)


@unittest.skipUnless(hasattr(os, 'linkat'), 'requires os.linkat')
class LinkAtTests(unittest.TestCase):
@staticmethod
def linkat(*args):
try:
os.linkat(*args)
except OSError as exc:
if exc.errno == errno.ENOSYS:
self.skipTest(str(exc))
raise

def test_no_flags(self):
src = "linkat_src"
self.addCleanup(os_helper.unlink, src)
with open(src, "w", encoding='utf8') as fp:
fp.write("hello")

dst = "linkat_dst"
self.addCleanup(os_helper.unlink, dst)
self.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0

with open(dst, encoding='utf8') as fp:
self.assertEqual(fp.read(), 'hello')

def test_destination_exists(self):
src = "linkat_src"
self.addCleanup(os_helper.unlink, src)
open(src, "w").close()

dst = "linkat_dst"
self.addCleanup(os_helper.unlink, dst)
open(dst, "w").close()

with self.assertRaises(FileExistsError):
self.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0

@unittest.skipUnless(hasattr(os, 'O_TMPFILE'), 'need os.O_TMPFILE')
def check_flag(self, flag):
filename = os_helper.TESTFN
self.addCleanup(os_helper.unlink, filename)

fd = os.open(os.path.curdir, os.O_WRONLY | os.O_TMPFILE, 0o600)
os.write(fd, b"hello")
if flag == os.AT_EMPTY_PATH:
os.linkat(fd, b"",
os.AT_FDCWD, filename,
os.AT_EMPTY_PATH)
else:
os.linkat(fd, f"/proc/self/fd/{fd}",
os.AT_FDCWD, filename,
os.AT_SYMLINK_FOLLOW)
os.close(fd)

with open(filename, encoding='utf8') as fp:
self.assertEqual(fp.read(), 'hello')

@unittest.skipUnless(hasattr(os, 'AT_EMPTY_PATH'),
'need os.AT_EMPTY_PATH')
def test_empty_path(self):
self.check_flag(os.AT_EMPTY_PATH)

@unittest.skipUnless(hasattr(os, 'AT_SYMLINK_FOLLOW'),
'need os.AT_SYMLINK_FOLLOW')
def test_symlink_follow(self):
self.check_flag(os.AT_SYMLINK_FOLLOW)


@unittest.skipIf(sys.platform == "win32", "Posix specific tests")
class PosixUidGidTests(unittest.TestCase):
# uid_t and gid_t are 32-bit unsigned integers on Linux
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added :func:`os.linkat` to create a hard link with flags. Patch by Victor
Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and
:data:`os.AT_SYMLINK_FOLLOW`.
Stinner.
98 changes: 97 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading