diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 9eba3bafae0cb6e..56359163012f93e 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2792,7 +2792,7 @@ features: .. availability:: Unix. -.. function:: readlink(path, *, dir_fd=None) +.. function:: readlink(path, *, dir_fd=None, printname=False) Return a string representing the path to which the symbolic link points. The result may be either an absolute or relative pathname; if it is relative, it @@ -2807,6 +2807,13 @@ features: This function can also support :ref:`paths relative to directory descriptors `. + On Windows, if *printname* is true, return the *print name* of the link -- + the target path as it was specified when the link was created -- + instead of the *substitute name* used by the system to resolve the link, + which typically includes the ``\\?\`` prefix. + The substitute name is returned if the link has no print name. + *printname* is ignored on non-Windows platforms. + When trying to resolve a path that may contain links, use :func:`~os.path.realpath` to properly handle recursion and platform differences. @@ -2829,6 +2836,9 @@ features: substitution path (which typically includes ``\\?\`` prefix) rather than the optional "print name" field that was previously returned. + .. versionchanged:: next + Added the *printname* parameter. + .. function:: remove(path, *, dir_fd=None) Remove (delete) the file *path*. If *path* is a directory, an diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c61190822234ead..1bddb8241d1e1d2 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -509,6 +509,11 @@ os now raises :exc:`PermissionError` instead of the functions being unavailable. (Contributed by Md Arif in :gh:`152936`.) +* :func:`os.readlink` has a new *printname* parameter + to return the print name of a link on Windows -- + the target path as it was specified when the link was created. + (Contributed by Serhiy Storchaka in :gh:`85004`.) + pydoc ----- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index bcbfcdd9eaaec5e..62fcd457b5733b3 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -727,6 +727,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(prec), "prec", 4); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(preserve_exc), "preserve_exc", 12); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(print_file_and_line), "print_file_and_line", 19); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(printname), "printname", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(priority), "priority", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(progress), "progress", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(progress_callback), "progress_callback", 17); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 2e5020578ad69b8..360a2a51af366d7 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -739,6 +739,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(prec) STRUCT_FOR_ID(preserve_exc) STRUCT_FOR_ID(print_file_and_line) + STRUCT_FOR_ID(printname) STRUCT_FOR_ID(priority) STRUCT_FOR_ID(progress) STRUCT_FOR_ID(progress_callback) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index c95b2d4f95a50da..f2b40c2f4a416f1 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -2014,6 +2014,7 @@ extern "C" { INIT_ID(prec), \ INIT_ID(preserve_exc), \ INIT_ID(print_file_and_line), \ + INIT_ID(printname), \ INIT_ID(priority), \ INIT_ID(progress), \ INIT_ID(progress_callback), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 795515260daecb7..3af336eae7fcc08 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -694,6 +694,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_prec._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_preserve_exc._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_print_file_and_line._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_printname._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_priority._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_progress._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_progress_callback._ascii.ob_base), diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 81b3043eb7e75bc..0d8ae28334cf8c7 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -3284,6 +3284,33 @@ def test_bytes(self): self.assertPathEqual(path, self.filelinkb_target) self.assertIsInstance(path, bytes) + @os_helper.skip_unless_symlink + def test_printname(self): + # The print name is the target as it was specified, without the + # "\\?\" prefix which Windows adds to the substitute name. + os.symlink(self.filelink_target, self.filelink) + self.addCleanup(os_helper.unlink, self.filelink) + self.assertEqual(os.readlink(self.filelink, printname=True), + self.filelink_target) + self.assertPathEqual(os.readlink(self.filelink), self.filelink_target) + + @os_helper.skip_unless_symlink + def test_printname_bytes(self): + os.symlink(self.filelinkb_target, self.filelinkb) + self.addCleanup(os_helper.unlink, self.filelinkb) + path = os.readlink(self.filelinkb, printname=True) + self.assertEqual(path, self.filelinkb_target) + self.assertIsInstance(path, bytes) + + @os_helper.skip_unless_symlink + def test_printname_relative(self): + # A relative target is not converted to the substitute name. + os.symlink('relative-target', self.filelink) + self.addCleanup(os_helper.unlink, self.filelink) + self.assertEqual(os.readlink(self.filelink, printname=True), + 'relative-target') + self.assertEqual(os.readlink(self.filelink), 'relative-target') + @os_helper.skip_unless_symlink class NonLocalSymlinkTests(unittest.TestCase): diff --git a/Lib/test/test_os/test_windows.py b/Lib/test/test_os/test_windows.py index e79d2c2c94d25b2..3b58dcb58c7ca98 100644 --- a/Lib/test/test_os/test_windows.py +++ b/Lib/test/test_os/test_windows.py @@ -445,6 +445,12 @@ def test_create_junction(self): self.assertEqual(os.path.normcase("\\\\?\\" + self.junction_target), os.path.normcase(os.readlink(self.junction))) + def test_readlink_printname(self): + _winapi.CreateJunction(self.junction_target, self.junction) + self.assertEqual(os.path.normcase(self.junction_target), + os.path.normcase(os.readlink(self.junction, + printname=True))) + def test_unlink_removes_junction(self): _winapi.CreateJunction(self.junction_target, self.junction) self.assertTrue(os.path.exists(self.junction)) diff --git a/Misc/NEWS.d/next/Library/2026-08-09-12-00-00.gh-issue-85004.Kp3rZq.rst b/Misc/NEWS.d/next/Library/2026-08-09-12-00-00.gh-issue-85004.Kp3rZq.rst new file mode 100644 index 000000000000000..4f1cf8eada1bb71 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-09-12-00-00.gh-issue-85004.Kp3rZq.rst @@ -0,0 +1,3 @@ +Add the *printname* parameter in :func:`os.readlink`. On Windows it makes the +function return the print name of the link -- the target path as it was +specified when the link was created. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 46e223a06626d8c..abf65bb4f6cd580 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -6646,7 +6646,7 @@ os_unshare(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject * #if (defined(HAVE_READLINK) || defined(MS_WINDOWS)) PyDoc_STRVAR(os_readlink__doc__, -"readlink($module, /, path, *, dir_fd=None)\n" +"readlink($module, /, path, *, dir_fd=None, printname=False)\n" "--\n" "\n" "Return a string representing the path to which the symbolic link points.\n" @@ -6656,13 +6656,19 @@ PyDoc_STRVAR(os_readlink__doc__, "that directory.\n" "\n" "dir_fd may not be implemented on your platform. If it is unavailable,\n" -"using it will raise a NotImplementedError."); +"using it will raise a NotImplementedError.\n" +"\n" +"On Windows, if printname is true, return the print name of the link --\n" +"the target path as it was specified when the link was created -- instead\n" +"of the substitute name used by the system to resolve the link.\n" +"\n" +"printname is ignored on non-Windows platforms."); #define OS_READLINK_METHODDEF \ {"readlink", _PyCFunction_CAST(os_readlink), METH_FASTCALL|METH_KEYWORDS, os_readlink__doc__}, static PyObject * -os_readlink_impl(PyObject *module, path_t *path, int dir_fd); +os_readlink_impl(PyObject *module, path_t *path, int dir_fd, int printname); static PyObject * os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -6670,7 +6676,7 @@ os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 2 + #define NUM_KEYWORDS 3 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -6679,7 +6685,7 @@ os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(path), &_Py_ID(dir_fd), }, + .ob_item = { &_Py_ID(path), &_Py_ID(dir_fd), &_Py_ID(printname), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -6688,17 +6694,18 @@ os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"path", "dir_fd", NULL}; + static const char * const _keywords[] = {"path", "dir_fd", "printname", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "readlink", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[2]; + PyObject *argsbuf[3]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; path_t path = PATH_T_INITIALIZE_P("readlink", "path", 0, 0, 0, 0); int dir_fd = DEFAULT_DIR_FD; + int printname = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -6711,11 +6718,20 @@ os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject if (!noptargs) { goto skip_optional_kwonly; } - if (!READLINKAT_DIR_FD_CONVERTER(args[1], &dir_fd)) { + if (args[1]) { + if (!READLINKAT_DIR_FD_CONVERTER(args[1], &dir_fd)) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + printname = PyObject_IsTrue(args[2]); + if (printname < 0) { goto exit; } skip_optional_kwonly: - return_value = os_readlink_impl(module, &path, dir_fd); + return_value = os_readlink_impl(module, &path, dir_fd, printname); exit: /* Cleanup for path */ @@ -13774,4 +13790,4 @@ os__emscripten_log(PyObject *module, PyObject *const *args, Py_ssize_t nargs, Py #ifndef OS__EMSCRIPTEN_LOG_METHODDEF #define OS__EMSCRIPTEN_LOG_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */ -/*[clinic end generated code: output=38edcd9b6f56b729 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a91a1844476f2d0f input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 0a451b8a833e67e..c6c9fca3784a866 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -10985,6 +10985,7 @@ os.readlink path: path_t * dir_fd: dir_fd(requires='readlinkat') = None + printname: bool = False Return a string representing the path to which the symbolic link points. @@ -10994,11 +10995,17 @@ that directory. dir_fd may not be implemented on your platform. If it is unavailable, using it will raise a NotImplementedError. + +On Windows, if printname is true, return the print name of the link -- +the target path as it was specified when the link was created -- instead +of the substitute name used by the system to resolve the link. + +printname is ignored on non-Windows platforms. [clinic start generated code]*/ static PyObject * -os_readlink_impl(PyObject *module, path_t *path, int dir_fd) -/*[clinic end generated code: output=d21b732a2e814030 input=03d10130870dbca8]*/ +os_readlink_impl(PyObject *module, path_t *path, int dir_fd, int printname) +/*[clinic end generated code: output=f4a4454719a32798 input=9877a2bcf1aa0726]*/ { #if defined(HAVE_READLINK) char buffer[MAXPATHLEN+1]; @@ -11074,24 +11081,41 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd) wchar_t *name = NULL; Py_ssize_t nameLen = 0; + /* The print name is optional, fall back to the substitute name. */ + int is_printname = 0; if (rdb->ReparseTag == IO_REPARSE_TAG_SYMLINK) { - name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer + - rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset); - nameLen = rdb->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(wchar_t); + USHORT offset = rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset; + USHORT length = rdb->SymbolicLinkReparseBuffer.SubstituteNameLength; + if (printname && rdb->SymbolicLinkReparseBuffer.PrintNameLength) { + offset = rdb->SymbolicLinkReparseBuffer.PrintNameOffset; + length = rdb->SymbolicLinkReparseBuffer.PrintNameLength; + is_printname = 1; + } + name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer + + offset); + nameLen = length / sizeof(wchar_t); } else if (rdb->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { - name = (wchar_t *)((char*)rdb->MountPointReparseBuffer.PathBuffer + - rdb->MountPointReparseBuffer.SubstituteNameOffset); - nameLen = rdb->MountPointReparseBuffer.SubstituteNameLength / sizeof(wchar_t); + USHORT offset = rdb->MountPointReparseBuffer.SubstituteNameOffset; + USHORT length = rdb->MountPointReparseBuffer.SubstituteNameLength; + if (printname && rdb->MountPointReparseBuffer.PrintNameLength) { + offset = rdb->MountPointReparseBuffer.PrintNameOffset; + length = rdb->MountPointReparseBuffer.PrintNameLength; + is_printname = 1; + } + name = (wchar_t *)((char*)rdb->MountPointReparseBuffer.PathBuffer + + offset); + nameLen = length / sizeof(wchar_t); } else { PyErr_SetString(PyExc_ValueError, "not a symbolic link"); } if (name) { - if (nameLen > 4 && wcsncmp(name, L"\\??\\", 4) == 0) { + /* Only the substitute name is in the NT namespace. */ + if (!is_printname && nameLen > 4 && wcsncmp(name, L"\\??\\", 4) == 0) { /* Our buffer is mutable, so this is okay */ name[1] = L'\\'; }