This function can support specifying *src_dir_fd* and/or *dst_dir_fd* to
supply :ref:`paths relative to directory descriptors <dir_fd>`, and :ref:`not
following symlinks <follow_symlinks>`.
+ The default value of *follow_symlinks* is ``False`` on Windows.
.. audit-event:: os.link src,dst,src_dir_fd,dst_dir_fd os.link
self._test_module_has_signatures(operator)
def test_os_module_has_signatures(self):
- unsupported_signature = {'chmod', 'utime'}
+ unsupported_signature = {'chmod', 'link', 'utime'}
unsupported_signature |= {name for name in
['get_terminal_size', 'posix_spawn', 'posix_spawnp',
'register_at_fork', 'startfile']
self.assertEqual(cm.exception.errno, errno.EINVAL)
os.close(os.pidfd_open(os.getpid(), 0))
+ @unittest.skipUnless(hasattr(os, "link"), "test needs os.link()")
+ def test_link_follow_symlinks(self):
+ default_follow = sys.platform.startswith(
+ ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5'))
+ default_no_follow = sys.platform.startswith(('win32', 'linux'))
+ orig = os_helper.TESTFN
+ symlink = orig + 'symlink'
+ posix.symlink(orig, symlink)
+ self.addCleanup(os_helper.unlink, symlink)
+
+ with self.subTest('no follow_symlinks'):
+ # no follow_symlinks -> platform depending
+ link = orig + 'link'
+ posix.link(symlink, link)
+ self.addCleanup(os_helper.unlink, link)
+ if os.link in os.supports_follow_symlinks or default_follow:
+ self.assertEqual(posix.lstat(link), posix.lstat(orig))
+ elif default_no_follow:
+ self.assertEqual(posix.lstat(link), posix.lstat(symlink))
+
+ with self.subTest('follow_symlinks=False'):
+ # follow_symlinks=False -> duplicate the symlink itself
+ link = orig + 'link_nofollow'
+ try:
+ posix.link(symlink, link, follow_symlinks=False)
+ except NotImplementedError:
+ if os.link in os.supports_follow_symlinks or default_no_follow:
+ raise
+ else:
+ self.addCleanup(os_helper.unlink, link)
+ self.assertEqual(posix.lstat(link), posix.lstat(symlink))
+
+ with self.subTest('follow_symlinks=True'):
+ # follow_symlinks=True -> duplicate the target file
+ link = orig + 'link_following'
+ try:
+ posix.link(symlink, link, follow_symlinks=True)
+ except NotImplementedError:
+ if os.link in os.supports_follow_symlinks or default_follow:
+ raise
+ else:
+ self.addCleanup(os_helper.unlink, link)
+ self.assertEqual(posix.lstat(link), posix.lstat(orig))
+
# tests for the posix *at functions follow
class TestPosixDirFd(unittest.TestCase):
--- /dev/null
+Fix :func:`os.link` on platforms (like Linux) where the
+system :c:func:`!link` function does not follow symlinks. On Linux,
+it now follows symlinks by default or if
+``follow_symlinks=True`` is specified. On Windows, it now raises an error if
+``follow_symlinks=True`` is passed. On macOS, it now raises an error if
+``follow_symlinks=False`` is passed and the system :c:func:`!linkat`
+function is not available at runtime.
PyDoc_STRVAR(os_link__doc__,
"link($module, /, src, dst, *, src_dir_fd=None, dst_dir_fd=None,\n"
-" follow_symlinks=True)\n"
+" follow_symlinks=(os.name != \'nt\'))\n"
"--\n"
"\n"
"Create a hard link to a file.\n"
path_t dst = PATH_T_INITIALIZE_P("link", "dst", 0, 0, 0, 0);
int src_dir_fd = DEFAULT_DIR_FD;
int dst_dir_fd = DEFAULT_DIR_FD;
- int follow_symlinks = 1;
+ int follow_symlinks = -1;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
#ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
#define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
#endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */
-/*[clinic end generated code: output=a5ca2541f2af5462 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=f7b5635e0b948be4 input=a9049054013a1b77]*/
# define HAVE_FACCESSAT_RUNTIME 1
# define HAVE_FCHMODAT_RUNTIME 1
# define HAVE_FCHOWNAT_RUNTIME 1
+#ifdef __wasi__
+# define HAVE_LINKAT_RUNTIME 0
+# else
# define HAVE_LINKAT_RUNTIME 1
+# endif
# define HAVE_FDOPENDIR_RUNTIME 1
# define HAVE_MKDIRAT_RUNTIME 1
# define HAVE_RENAMEAT_RUNTIME 1
*
src_dir_fd : dir_fd = None
dst_dir_fd : dir_fd = None
- follow_symlinks: bool = True
+ follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = PLACEHOLDER
Create a hard link to a file.
static PyObject *
os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
int dst_dir_fd, int follow_symlinks)
-/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/
+/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/
{
#ifdef MS_WINDOWS
BOOL result = FALSE;
#else
int result;
#endif
-#if defined(HAVE_LINKAT)
- int linkat_unavailable = 0;
-#endif
-#ifndef HAVE_LINKAT
- if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
- argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
- return NULL;
+#ifdef HAVE_LINKAT
+ if (HAVE_LINKAT_RUNTIME) {
+ if (follow_symlinks < 0) {
+ follow_symlinks = 1;
+ }
}
+ else
#endif
-
-#ifndef MS_WINDOWS
- if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
- PyErr_SetString(PyExc_NotImplementedError,
- "link: src and dst must be the same type");
- return NULL;
- }
+ {
+ if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
+ argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
+ return NULL;
+ }
+/* See issue 85527: link() on Linux works like linkat without AT_SYMLINK_FOLLOW,
+ but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
+#if defined(MS_WINDOWS) || defined(__linux__)
+ if (follow_symlinks == 1) {
+ argument_unavailable_error("link", "follow_symlinks=True");
+ return NULL;
+ }
+#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) || (defined(__sun) && defined(__SVR4))
+ if (follow_symlinks == 0) {
+ argument_unavailable_error("link", "follow_symlinks=False");
+ return NULL;
+ }
+#else
+ if (follow_symlinks >= 0) {
+ argument_unavailable_error("link", "follow_symlinks");
+ return NULL;
+ }
#endif
+ }
if (PySys_Audit("os.link", "OOii", src->object, dst->object,
src_dir_fd == DEFAULT_DIR_FD ? -1 : src_dir_fd,
#else
Py_BEGIN_ALLOW_THREADS
#ifdef HAVE_LINKAT
- if ((src_dir_fd != DEFAULT_DIR_FD) ||
- (dst_dir_fd != DEFAULT_DIR_FD) ||
- (!follow_symlinks)) {
-
- if (HAVE_LINKAT_RUNTIME) {
-
- result = linkat(src_dir_fd, src->narrow,
- dst_dir_fd, dst->narrow,
- follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
-
- }
-#ifdef __APPLE__
- else {
- if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) {
- /* See issue 41355: This matches the behaviour of !HAVE_LINKAT */
- result = link(src->narrow, dst->narrow);
- } else {
- linkat_unavailable = 1;
- }
- }
-#endif
+ if (HAVE_LINKAT_RUNTIME) {
+ result = linkat(src_dir_fd, src->narrow,
+ dst_dir_fd, dst->narrow,
+ follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
}
else
-#endif /* HAVE_LINKAT */
+#endif
+ {
+ /* linkat not available */
result = link(src->narrow, dst->narrow);
- Py_END_ALLOW_THREADS
-
-#ifdef HAVE_LINKAT
- if (linkat_unavailable) {
- /* Either or both dir_fd arguments were specified */
- if (src_dir_fd != DEFAULT_DIR_FD) {
- argument_unavailable_error("link", "src_dir_fd");
- } else {
- argument_unavailable_error("link", "dst_dir_fd");
- }
- return NULL;
}
-#endif
+ Py_END_ALLOW_THREADS
if (result)
return path_error2(src, dst);
return path_error2(src, dst);
#else
- if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
- PyErr_Format(PyExc_ValueError,
- "%s: src and dst must be the same type", function_name);
- return NULL;
- }
-
Py_BEGIN_ALLOW_THREADS
#ifdef HAVE_RENAMEAT
if (dir_fd_specified) {
#else
- if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
- PyErr_SetString(PyExc_ValueError,
- "symlink: src and dst must be the same type");
- return NULL;
- }
-
Py_BEGIN_ALLOW_THREADS
#ifdef HAVE_SYMLINKAT
if (dir_fd != DEFAULT_DIR_FD) {