diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index ebeb3bb50b8b1f..afd69606cc831d 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -127,10 +127,11 @@ the :mod:`glob` module.) Accepts a :term:`path-like object`. -.. function:: exists(path) +.. function:: exists(path, *, follow_symlinks=True) Return ``True`` if *path* refers to an existing path or an open - file descriptor. Returns ``False`` for broken symbolic links. On + file descriptor. Returns ``False`` for broken symbolic links if + *follow_symlinks* is set, otherwise ``True``. On some platforms, this function may return ``False`` if permission is not granted to execute :func:`os.stat` on the requested file, even if the *path* physically exists. @@ -142,6 +143,8 @@ the :mod:`glob` module.) .. versionchanged:: 3.6 Accepts a :term:`path-like object`. + .. versionchanged:: 3.13 + Added the *follow_symlinks* parameter. .. function:: lexists(path) diff --git a/Lib/genericpath.py b/Lib/genericpath.py index ba7b0a13c7f81d..f183dbf24c4363 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -13,10 +13,11 @@ # Does a path exist? # This is false for dangling symbolic links on systems that support them. -def exists(path): - """Test whether a path exists. Returns False for broken symbolic links""" +def exists(path, *, follow_symlinks=True): + """Test whether a path exists. Returns False for broken symbolic links + if follow_symlinks is set, otherwise True""" try: - os.stat(path) + os.stat(path, follow_symlinks=follow_symlinks) except (OSError, ValueError): return False return True diff --git a/Lib/ntpath.py b/Lib/ntpath.py index f5d1a2195dd633..d8da88993e9a38 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -899,6 +899,7 @@ def commonpath(paths): from nt import _path_isfile as isfile from nt import _path_islink as islink from nt import _path_exists as exists + from nt import _path_lexists as lexists except ImportError: # Use genericpath.* as imported above pass diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index f407ee3caf154c..08209be3f6890f 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -128,31 +128,42 @@ def test_filetime(self): ) def test_exists(self): + def check_exists(filename, expected): + bfilename = os.fsencode(filename) + self.assertIs(self.pathmodule.exists(filename), expected) + self.assertIs(self.pathmodule.exists(bfilename), expected) + self.assertIs(self.pathmodule.exists(filename, follow_symlinks=True), + expected) + self.assertIs(self.pathmodule.exists(bfilename, follow_symlinks=True), + expected) + + def check_lexists(filename, expected): + bfilename = os.fsencode(filename) + self.assertIs(self.pathmodule.lexists(filename), expected) + self.assertIs(self.pathmodule.lexists(bfilename), expected) + self.assertIs(self.pathmodule.exists(filename, follow_symlinks=False), + expected) + self.assertIs(self.pathmodule.exists(bfilename, follow_symlinks=False), + expected) + filename = os_helper.TESTFN bfilename = os.fsencode(filename) self.addCleanup(os_helper.unlink, filename) - self.assertIs(self.pathmodule.exists(filename), False) - self.assertIs(self.pathmodule.exists(bfilename), False) + check_exists(filename, False) + check_lexists(filename, False) create_file(filename) - self.assertIs(self.pathmodule.exists(filename), True) - self.assertIs(self.pathmodule.exists(bfilename), True) - + check_exists(filename, True) self.assertIs(self.pathmodule.exists(filename + '\udfff'), False) self.assertIs(self.pathmodule.exists(bfilename + b'\xff'), False) - self.assertIs(self.pathmodule.exists(filename + '\x00'), False) - self.assertIs(self.pathmodule.exists(bfilename + b'\x00'), False) - - if self.pathmodule is not genericpath: - self.assertIs(self.pathmodule.lexists(filename), True) - self.assertIs(self.pathmodule.lexists(bfilename), True) + check_exists(filename + '\x00', False) - self.assertIs(self.pathmodule.lexists(filename + '\udfff'), False) - self.assertIs(self.pathmodule.lexists(bfilename + b'\xff'), False) - self.assertIs(self.pathmodule.lexists(filename + '\x00'), False) - self.assertIs(self.pathmodule.lexists(bfilename + b'\x00'), False) + check_lexists(filename, True) + self.assertIs(self.pathmodule.lexists(filename + '\udfff'), False) + self.assertIs(self.pathmodule.lexists(bfilename + b'\xff'), False) + check_lexists(filename + '\x00', False) @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") @unittest.skipIf(is_emscripten, "Emscripten pipe fds have no stat") diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 31156130fcc747..28706298618e04 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1115,6 +1115,8 @@ def test_fast_paths_in_use(self): self.assertFalse(inspect.isfunction(os.path.islink)) self.assertTrue(os.path.exists is nt._path_exists) self.assertFalse(inspect.isfunction(os.path.exists)) + self.assertTrue(os.path.lexists is nt._path_lexists) + self.assertFalse(inspect.isfunction(os.path.lexists)) @unittest.skipIf(os.name != 'nt', "Dev Drives only exist on Win32") def test_isdevdrive(self): diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-04-10-20-41-15.gh-issue-117705.2pDs7H.rst b/Misc/NEWS.d/next/Core and Builtins/2024-04-10-20-41-15.gh-issue-117705.2pDs7H.rst new file mode 100644 index 00000000000000..130bcb61c388d1 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-04-10-20-41-15.gh-issue-117705.2pDs7H.rst @@ -0,0 +1 @@ +Added the *follow_symlinks* parameter to :func:`os.path.exists`. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 0398629e3c10ce..a0d9af2f61ee79 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -2133,16 +2133,23 @@ os__path_isfile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj #if defined(MS_WINDOWS) PyDoc_STRVAR(os__path_exists__doc__, -"_path_exists($module, /, path)\n" +"_path_exists($module, /, path, follow_symlinks=True)\n" "--\n" "\n" -"Test whether a path exists. Returns False for broken symbolic links"); +"Test whether a path exists. Returns False for broken symbolic links\n" +"\n" +" path\n" +" Path to be tested; can be a string, bytes object, a path-like object,\n" +" or an integer that references an open file descriptor.\n" +" follow_symlinks\n" +" If False, and the last element of the path is a symbolic link, do not\n" +" test the target of the symbolic link."); #define OS__PATH_EXISTS_METHODDEF \ {"_path_exists", _PyCFunction_CAST(os__path_exists), METH_FASTCALL|METH_KEYWORDS, os__path_exists__doc__}, static PyObject * -os__path_exists_impl(PyObject *module, PyObject *path); +os__path_exists_impl(PyObject *module, PyObject *path, int follow_symlinks); static PyObject * os__path_exists(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -2150,6 +2157,79 @@ os__path_exists(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(path), &_Py_ID(follow_symlinks), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"path", "follow_symlinks", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_path_exists", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + PyObject *path; + int follow_symlinks = 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 2, 0, argsbuf); + if (!args) { + goto exit; + } + path = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + follow_symlinks = PyObject_IsTrue(args[1]); + if (follow_symlinks < 0) { + goto exit; + } +skip_optional_pos: + return_value = os__path_exists_impl(module, path, follow_symlinks); + +exit: + return return_value; +} + +#endif /* defined(MS_WINDOWS) */ + +#if defined(MS_WINDOWS) + +PyDoc_STRVAR(os__path_lexists__doc__, +"_path_lexists($module, /, path)\n" +"--\n" +"\n" +"Test whether a path exists. Returns True for broken symbolic links\n" +"\n" +" path\n" +" Path to be tested; can be a string, bytes object, a path-like object,\n" +" or an integer that references an open file descriptor."); + +#define OS__PATH_LEXISTS_METHODDEF \ + {"_path_lexists", _PyCFunction_CAST(os__path_lexists), METH_FASTCALL|METH_KEYWORDS, os__path_lexists__doc__}, + +static PyObject * +os__path_lexists_impl(PyObject *module, PyObject *path); + +static PyObject * +os__path_lexists(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + #define NUM_KEYWORDS 1 static struct { PyGC_Head _this_is_not_used; @@ -2169,7 +2249,7 @@ os__path_exists(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj static const char * const _keywords[] = {"path", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, - .fname = "_path_exists", + .fname = "_path_lexists", .kwtuple = KWTUPLE, }; #undef KWTUPLE @@ -2181,7 +2261,7 @@ os__path_exists(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj goto exit; } path = args[0]; - return_value = os__path_exists_impl(module, path); + return_value = os__path_lexists_impl(module, path); exit: return return_value; @@ -12051,6 +12131,10 @@ os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored)) #define OS__PATH_EXISTS_METHODDEF #endif /* !defined(OS__PATH_EXISTS_METHODDEF) */ +#ifndef OS__PATH_LEXISTS_METHODDEF + #define OS__PATH_LEXISTS_METHODDEF +#endif /* !defined(OS__PATH_LEXISTS_METHODDEF) */ + #ifndef OS__PATH_ISLINK_METHODDEF #define OS__PATH_ISLINK_METHODDEF #endif /* !defined(OS__PATH_ISLINK_METHODDEF) */ @@ -12602,4 +12686,4 @@ os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */ -/*[clinic end generated code: output=511f0788a6b90db0 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3cfe2a1ba37c496d input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 5e54cf64cd563e..fa1fc51d55f836 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -5284,21 +5284,24 @@ os__path_isfile_impl(PyObject *module, PyObject *path) os._path_exists path: 'O' + Path to be tested; can be a string, bytes object, a path-like object, + or an integer that references an open file descriptor. + + follow_symlinks: bool = True + If False, and the last element of the path is a symbolic link, do not + test the target of the symbolic link. Test whether a path exists. Returns False for broken symbolic links [clinic start generated code]*/ static PyObject * -os__path_exists_impl(PyObject *module, PyObject *path) -/*[clinic end generated code: output=f508c3b35e13a249 input=380f77cdfa0f7ae8]*/ +os__path_exists_impl(PyObject *module, PyObject *path, int follow_symlinks) +/*[clinic end generated code: output=f31130f636d45dcc input=26779f82ae9d4f5a]*/ { - HANDLE hfile; - BOOL close_file = TRUE; path_t _path = PATH_T_INITIALIZE("exists", "path", 0, 1); - int result; - BOOL slow_path = TRUE; - FILE_STAT_BASIC_INFORMATION statInfo; + HANDLE hfile; + int result = 0; if (!path_converter(path, &_path)) { path_cleanup(&_path); @@ -5310,49 +5313,56 @@ os__path_exists_impl(PyObject *module, PyObject *path) } Py_BEGIN_ALLOW_THREADS - if (_path.wide) { + if (_path.fd != -1) { + hfile = _Py_get_osfhandle_noraise(_path.fd); + if (hfile != INVALID_HANDLE_VALUE) { + result = 1; + } + } + else if (_path.wide) { + BOOL slow_path = TRUE; + FILE_STAT_BASIC_INFORMATION statInfo; if (_Py_GetFileInformationByName(_path.wide, FileStatBasicByNameInfo, - &statInfo, sizeof(statInfo))) { - if (!(statInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { + &statInfo, sizeof(statInfo))) + { + if (!(statInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || + !follow_symlinks && + IsReparseTagNameSurrogate(statInfo.ReparseTag)) + { slow_path = FALSE; result = 1; } - } else if (_Py_GetFileInformationByName_ErrorIsTrustworthy(GetLastError())) { - slow_path = FALSE; - result = 0; - } - } - if (slow_path) { - if (_path.fd != -1) { - hfile = _Py_get_osfhandle_noraise(_path.fd); - close_file = FALSE; - } - else { - hfile = CreateFileW(_path.wide, FILE_READ_ATTRIBUTES, 0, NULL, - OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); - } - if (hfile != INVALID_HANDLE_VALUE) { - result = 1; - if (close_file) { - CloseHandle(hfile); - } + } else if (_Py_GetFileInformationByName_ErrorIsTrustworthy( + GetLastError())) + { + slow_path = FALSE; } - else { + if (slow_path) { STRUCT_STAT st; - switch (GetLastError()) { - case ERROR_ACCESS_DENIED: - case ERROR_SHARING_VIOLATION: - case ERROR_CANT_ACCESS_FILE: - case ERROR_INVALID_PARAMETER: - if (STAT(_path.wide, &st)) { - result = 0; + if (!follow_symlinks) { + if (!LSTAT(_path.wide, &st)) { + result = 1; } - else { + } + else { + hfile = CreateFileW(_path.wide, FILE_READ_ATTRIBUTES, 0, NULL, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hfile != INVALID_HANDLE_VALUE) { + CloseHandle(hfile); result = 1; } - break; - default: - result = 0; + else { + switch (GetLastError()) { + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_CANT_ACCESS_FILE: + case ERROR_INVALID_PARAMETER: + if (!STAT(_path.wide, &st)) { + result = 1; + } + } + } } } } @@ -5366,6 +5376,25 @@ os__path_exists_impl(PyObject *module, PyObject *path) } +/*[clinic input] +os._path_lexists + + path: 'O' + Path to be tested; can be a string, bytes object, a path-like object, + or an integer that references an open file descriptor. + +Test whether a path exists. Returns True for broken symbolic links + +[clinic start generated code]*/ + +static PyObject * +os__path_lexists_impl(PyObject *module, PyObject *path) +/*[clinic end generated code: output=b9a42a50b1df6651 input=96bc06e253ad0cd7]*/ +{ + return os__path_exists_impl(module, path, 0); +} + + /*[clinic input] os._path_islink @@ -16843,6 +16872,7 @@ static PyMethodDef posix_methods[] = { OS__PATH_ISFILE_METHODDEF OS__PATH_ISLINK_METHODDEF OS__PATH_EXISTS_METHODDEF + OS__PATH_LEXISTS_METHODDEF OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF {NULL, NULL} /* Sentinel */