diff --git a/Lib/ntpath.py b/Lib/ntpath.py index e810b655e5ac85..45d379dc8d5212 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -910,13 +910,14 @@ def commonpath(paths): try: - # The isdir(), isfile(), islink() and exists() implementations in - # genericpath use os.stat(). This is overkill on Windows. Use simpler + # The isdir(), isfile(), islink(), exists() and lexists() implementations + # in genericpath use os.stat(). This is overkill on Windows. Use simpler # builtin functions if they are available. from nt import _path_isdir as isdir 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..bf04b3fecf7057 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -135,6 +135,9 @@ def test_exists(self): self.assertIs(self.pathmodule.exists(filename), False) self.assertIs(self.pathmodule.exists(bfilename), False) + self.assertIs(self.pathmodule.lexists(filename), False) + self.assertIs(self.pathmodule.lexists(bfilename), False) + create_file(filename) self.assertIs(self.pathmodule.exists(filename), True) @@ -145,14 +148,13 @@ def test_exists(self): 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) + self.assertIs(self.pathmodule.lexists(filename), True) + self.assertIs(self.pathmodule.lexists(bfilename), True) - 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) + 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) @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 7f91bf1c2b837a..c22ce1cfa49f9b 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1116,6 +1116,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-13-11-30-09.gh-issue-117841.eW4w_y.rst b/Misc/NEWS.d/next/Core and Builtins/2024-04-13-11-30-09.gh-issue-117841.eW4w_y.rst new file mode 100644 index 00000000000000..3a562884283b1c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-04-13-11-30-09.gh-issue-117841.eW4w_y.rst @@ -0,0 +1 @@ +Speedup :func:`os.path.lexists` on Windows with a native implementation. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index a0d1f3238a6733..4ede117b5aff10 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -2133,59 +2133,26 @@ 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, /)\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."); #define OS__PATH_EXISTS_METHODDEF \ - {"_path_exists", _PyCFunction_CAST(os__path_exists), METH_FASTCALL|METH_KEYWORDS, os__path_exists__doc__}, + {"_path_exists", (PyCFunction)os__path_exists, METH_O, os__path_exists__doc__}, -static PyObject * -os__path_exists_impl(PyObject *module, PyObject *path); - -static PyObject * -os__path_exists(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; - PyObject_VAR_HEAD - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_item = { &_Py_ID(path), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE +#endif /* defined(MS_WINDOWS) */ - static const char * const _keywords[] = {"path", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "_path_exists", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - PyObject *path; +#if defined(MS_WINDOWS) - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf); - if (!args) { - goto exit; - } - path = args[0]; - return_value = os__path_exists_impl(module, path); +PyDoc_STRVAR(os__path_lexists__doc__, +"_path_lexists($module, path, /)\n" +"--\n" +"\n" +"Test whether a path exists. Returns True for broken symbolic links."); -exit: - return return_value; -} +#define OS__PATH_LEXISTS_METHODDEF \ + {"_path_lexists", (PyCFunction)os__path_lexists, METH_O, os__path_lexists__doc__}, #endif /* defined(MS_WINDOWS) */ @@ -12109,6 +12076,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) */ @@ -12660,4 +12631,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=c4698b47007cd6eb input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b97f5f5319f45e29 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index c9d67ccbb8c908..5aa6de79e08e51 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -5280,25 +5280,13 @@ os__path_isfile_impl(PyObject *module, PyObject *path) } -/*[clinic input] -os._path_exists - - path: 'O' - -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]*/ +nt_exists(PyObject *path, int follow_symlinks) { - 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; + BOOL traverse = follow_symlinks; + int result = 0; if (!path_converter(path, &_path)) { path_cleanup(&_path); @@ -5310,49 +5298,95 @@ 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 { + // reparse point but not name-surrogate + traverse = TRUE; } } - else { - 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; + else if (_Py_GetFileInformationByName_ErrorIsTrustworthy( + GetLastError())) + { + slow_path = FALSE; + } + if (slow_path) { + BOOL traverse = follow_symlinks; + if (!traverse) { + hfile = CreateFileW(_path.wide, FILE_READ_ATTRIBUTES, 0, NULL, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (hfile != INVALID_HANDLE_VALUE) { + FILE_ATTRIBUTE_TAG_INFO info; + if (GetFileInformationByHandleEx(hfile, + FileAttributeTagInfo, &info, sizeof(info))) + { + if (!(info.FileAttributes & + FILE_ATTRIBUTE_REPARSE_POINT) || + IsReparseTagNameSurrogate(info.ReparseTag)) + { + result = 1; + } + else { + // reparse point but not name-surrogate + traverse = TRUE; + } + } + else { + // device or legacy filesystem + result = 1; + } + CloseHandle(hfile); } else { + STRUCT_STAT st; + switch (GetLastError()) { + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_CANT_ACCESS_FILE: + case ERROR_INVALID_PARAMETER: + if (!LSTAT(_path.wide, &st)) { + result = 1; + } + } + } + } + if (traverse) { + 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 { + 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 = 1; + } + } + } } } } @@ -5366,6 +5400,42 @@ os__path_exists_impl(PyObject *module, PyObject *path) } +/*[clinic input] +os._path_exists + + path: object + / + +Test whether a path exists. Returns False for broken symbolic links. + +[clinic start generated code]*/ + +static PyObject * +os__path_exists(PyObject *module, PyObject *path) +/*[clinic end generated code: output=617b7575ba0644bc input=242708cabb67c407]*/ +{ + return nt_exists(path, 1); +} + + +/*[clinic input] +os._path_lexists + + path: object + / + +Test whether a path exists. Returns True for broken symbolic links. + +[clinic start generated code]*/ + +static PyObject * +os__path_lexists(PyObject *module, PyObject *path) +/*[clinic end generated code: output=c7c89aa6d6e341df input=536ed4b0a7d4f723]*/ +{ + return nt_exists(path, 0); +} + + /*[clinic input] os._path_islink @@ -16887,6 +16957,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 */