From 773c4d96d890d083c8f159ad99c82ba65cf6e9ae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 11 Dec 2024 12:57:59 +0100 Subject: [PATCH 01/18] gh-127350: Add Py_fopen() function Rename _Py_fopen_obj() to Py_fopen(). The function now also accepts bytes path on Windows. Remove the private, undocumented, and untested function _Py_fopen_obj(). --- Doc/c-api/sys.rst | 16 +++ Doc/whatsnew/3.14.rst | 5 + Include/cpython/fileutils.h | 3 +- Lib/test/test_capi/test_file.py | 23 ++++ Lib/test/test_ssl.py | 3 +- ...-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst | 3 + Modules/_ssl.c | 2 +- Modules/_ssl/debughelpers.c | 4 +- Modules/_testcapi/clinic/file.c.h | 48 +++++++++ Modules/_testcapi/file.c | 35 ++++++ Modules/_testcapi/object.c | 8 +- Modules/_testcapimodule.c | 12 +-- Modules/main.c | 4 +- Python/errors.c | 2 +- Python/fileutils.c | 100 ++++++++---------- Python/import.c | 2 +- Python/pythonrun.c | 2 +- 17 files changed, 197 insertions(+), 75 deletions(-) create mode 100644 Lib/test/test_capi/test_file.py create mode 100644 Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst create mode 100644 Modules/_testcapi/clinic/file.c.h diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index c688afdca8231d..d51e66d45d22b3 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -216,6 +216,22 @@ Operating System Utilities The function now uses the UTF-8 encoding on Windows if :c:member:`PyPreConfig.legacy_windows_fs_encoding` is zero. +.. c:function:: FILE* Py_fopen(PyObject *path, const char *mode) + + Similar to the :c:func:`!fopen` function, but *path* is a Python object and + an exception is set on error. + + *path* must be a :class:`str` object or a :class:`bytes` object. + + On success, return the new file object. + On error, set an exception and return ``NULL``. + + The file descriptor is created non-inheritable (:pep:`446`). + + The caller must hold the GIL. + + .. versionadded:: next + .. _systemfunctions: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index b71d31f9742fe0..2ce1bcdad75fbc 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1034,6 +1034,11 @@ New features * Add :c:func:`PyUnstable_Object_EnableDeferredRefcount` for enabling deferred reference counting, as outlined in :pep:`703`. +* Add :c:func:`Py_fopen` function to open a file. Similar to the + :c:func:`!fopen` function, but the *path* parameter is a Python object and an + exception is set on error. + (Contributed by Victor Stinner in :gh:`127350`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/cpython/fileutils.h b/Include/cpython/fileutils.h index b386ad107bde1f..3a2bfae1add88e 100644 --- a/Include/cpython/fileutils.h +++ b/Include/cpython/fileutils.h @@ -2,7 +2,6 @@ # error "this header file must not be included directly" #endif -// Used by _testcapi which must not use the internal C API -PyAPI_FUNC(FILE*) _Py_fopen_obj( +PyAPI_FUNC(FILE*) Py_fopen( PyObject *path, const char *mode); diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py new file mode 100644 index 00000000000000..68e63ad2a3e490 --- /dev/null +++ b/Lib/test/test_capi/test_file.py @@ -0,0 +1,23 @@ +import os +import unittest +from test.support import import_helper + +_testcapi = import_helper.import_module('_testcapi') + + +class CAPIFileTest(unittest.TestCase): + def test_py_fopen(self): + for filename in (__file__, os.fsencode(__file__)): + with self.subTest(filename=filename): + content = _testcapi.py_fopen(filename, "rb") + with open(filename, "rb") as fp: + self.assertEqual(fp.read(256), content) + + for invalid_type in (123, object()): + with self.subTest(filename=invalid_type): + with self.assertRaises(TypeError): + _testcapi.py_fopen(invalid_type, "r") + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 59f37b3f9a7575..122c1e7882864f 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1325,8 +1325,7 @@ def test_load_verify_cadata(self): def test_load_dh_params(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_dh_params(DHFILE) - if os.name != 'nt': - ctx.load_dh_params(BYTES_DHFILE) + ctx.load_dh_params(BYTES_DHFILE) self.assertRaises(TypeError, ctx.load_dh_params) self.assertRaises(TypeError, ctx.load_dh_params, None) with self.assertRaises(FileNotFoundError) as cm: diff --git a/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst b/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst new file mode 100644 index 00000000000000..e3a914188ca680 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst @@ -0,0 +1,3 @@ +Add :c:func:`Py_fopen` function to open a file. Similar to the :c:func:`!fopen` +function, but the *path* parameter is a Python object and an exception is set +on error. Patch by Victor Stinner. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index e7df132869fee6..accc9b7a3ac481 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -4377,7 +4377,7 @@ _ssl__SSLContext_load_dh_params_impl(PySSLContext *self, PyObject *filepath) FILE *f; DH *dh; - f = _Py_fopen_obj(filepath, "rb"); + f = Py_fopen(filepath, "rb"); if (f == NULL) return NULL; diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index 9c87f8b4d21e68..318c045a0eec3c 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -180,8 +180,8 @@ _PySSLContext_set_keylog_filename(PySSLContext *self, PyObject *arg, void *c) { return 0; } - /* _Py_fopen_obj() also checks that arg is of proper type. */ - fp = _Py_fopen_obj(arg, "a" PY_STDIOTEXTMODE); + /* Py_fopen() also checks that arg is of proper type. */ + fp = Py_fopen(arg, "a" PY_STDIOTEXTMODE); if (fp == NULL) return -1; diff --git a/Modules/_testcapi/clinic/file.c.h b/Modules/_testcapi/clinic/file.c.h new file mode 100644 index 00000000000000..9856b6161cdfa0 --- /dev/null +++ b/Modules/_testcapi/clinic/file.c.h @@ -0,0 +1,48 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#include "pycore_modsupport.h" // _PyArg_CheckPositional() + +PyDoc_STRVAR(_testcapi_py_fopen__doc__, +"py_fopen($module, path, mode, /)\n" +"--\n" +"\n" +"Call Py_fopen() and return fread(256)."); + +#define _TESTCAPI_PY_FOPEN_METHODDEF \ + {"py_fopen", _PyCFunction_CAST(_testcapi_py_fopen), METH_FASTCALL, _testcapi_py_fopen__doc__}, + +static PyObject * +_testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode); + +static PyObject * +_testcapi_py_fopen(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *path; + const char *mode; + + if (!_PyArg_CheckPositional("py_fopen", nargs, 2, 2)) { + goto exit; + } + path = args[0]; + if (!PyUnicode_Check(args[1])) { + _PyArg_BadArgument("py_fopen", "argument 2", "str", args[1]); + goto exit; + } + Py_ssize_t mode_length; + mode = PyUnicode_AsUTF8AndSize(args[1], &mode_length); + if (mode == NULL) { + goto exit; + } + if (strlen(mode) != (size_t)mode_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + return_value = _testcapi_py_fopen_impl(module, path, mode); + +exit: + return return_value; +} +/*[clinic end generated code: output=968f4f4a620f13cf input=a9049054013a1b77]*/ diff --git a/Modules/_testcapi/file.c b/Modules/_testcapi/file.c index 634563f6ea12cb..b163ec0efcb78b 100644 --- a/Modules/_testcapi/file.c +++ b/Modules/_testcapi/file.c @@ -1,8 +1,43 @@ +// clinic/file.c.h uses internal pycore_modsupport.h API +#define PYTESTCAPI_NEED_INTERNAL_API + #include "parts.h" #include "util.h" +#include "clinic/file.c.h" + +/*[clinic input] +module _testcapi +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6361033e795369fc]*/ + +/*[clinic input] +_testcapi.py_fopen + + path: object + mode: str + / + +Call Py_fopen() and return fread(256). +[clinic start generated code]*/ +static PyObject * +_testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode) +/*[clinic end generated code: output=5a900af000f759de input=0878c2f9333abd60]*/ +{ + FILE *fp = Py_fopen(path, mode); + if (fp == NULL) { + return NULL; + } + + char buffer[256]; + size_t size = fread(buffer, 1, Py_ARRAY_LENGTH(buffer), fp); + fclose(fp); + + return PyBytes_FromStringAndSize(buffer, size); +} static PyMethodDef test_methods[] = { + _TESTCAPI_PY_FOPEN_METHODDEF {NULL}, }; diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 3af5429ef00985..841410c52b3ce2 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -15,7 +15,7 @@ call_pyobject_print(PyObject *self, PyObject * args) return NULL; } - fp = _Py_fopen_obj(filename, "w+"); + fp = Py_fopen(filename, "w+"); if (Py_IsTrue(print_raw)) { flags = Py_PRINT_RAW; @@ -41,7 +41,7 @@ pyobject_print_null(PyObject *self, PyObject *args) return NULL; } - fp = _Py_fopen_obj(filename, "w+"); + fp = Py_fopen(filename, "w+"); if (PyObject_Print(NULL, fp, 0) < 0) { fclose(fp); @@ -72,7 +72,7 @@ pyobject_print_noref_object(PyObject *self, PyObject *args) return NULL; } - fp = _Py_fopen_obj(filename, "w+"); + fp = Py_fopen(filename, "w+"); if (PyObject_Print(test_string, fp, 0) < 0){ fclose(fp); @@ -103,7 +103,7 @@ pyobject_print_os_error(PyObject *self, PyObject *args) } // open file in read mode to induce OSError - fp = _Py_fopen_obj(filename, "r"); + fp = Py_fopen(filename, "r"); if (PyObject_Print(test_string, fp, 0) < 0) { fclose(fp); diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 8d86b535effb9a..994785191c4bb5 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -1744,7 +1744,7 @@ pymarshal_write_long_to_file(PyObject* self, PyObject *args) &value, &filename, &version)) return NULL; - fp = _Py_fopen_obj(filename, "wb"); + fp = Py_fopen(filename, "wb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -1769,7 +1769,7 @@ pymarshal_write_object_to_file(PyObject* self, PyObject *args) &obj, &filename, &version)) return NULL; - fp = _Py_fopen_obj(filename, "wb"); + fp = Py_fopen(filename, "wb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -1793,7 +1793,7 @@ pymarshal_read_short_from_file(PyObject* self, PyObject *args) if (!PyArg_ParseTuple(args, "O:pymarshal_read_short_from_file", &filename)) return NULL; - fp = _Py_fopen_obj(filename, "rb"); + fp = Py_fopen(filename, "rb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -1818,7 +1818,7 @@ pymarshal_read_long_from_file(PyObject* self, PyObject *args) if (!PyArg_ParseTuple(args, "O:pymarshal_read_long_from_file", &filename)) return NULL; - fp = _Py_fopen_obj(filename, "rb"); + fp = Py_fopen(filename, "rb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -1840,7 +1840,7 @@ pymarshal_read_last_object_from_file(PyObject* self, PyObject *args) if (!PyArg_ParseTuple(args, "O:pymarshal_read_last_object_from_file", &filename)) return NULL; - FILE *fp = _Py_fopen_obj(filename, "rb"); + FILE *fp = Py_fopen(filename, "rb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -1863,7 +1863,7 @@ pymarshal_read_object_from_file(PyObject* self, PyObject *args) if (!PyArg_ParseTuple(args, "O:pymarshal_read_object_from_file", &filename)) return NULL; - FILE *fp = _Py_fopen_obj(filename, "rb"); + FILE *fp = Py_fopen(filename, "rb"); if (fp == NULL) { PyErr_SetFromErrno(PyExc_OSError); return NULL; diff --git a/Modules/main.c b/Modules/main.c index 15ea49a1bad19e..29b944aab84bec 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -370,7 +370,7 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, return pymain_exit_err_print(); } - FILE *fp = _Py_fopen_obj(filename, "rb"); + FILE *fp = Py_fopen(filename, "rb"); if (fp == NULL) { // Ignore the OSError PyErr_Clear(); @@ -464,7 +464,7 @@ pymain_run_startup(PyConfig *config, int *exitcode) goto error; } - FILE *fp = _Py_fopen_obj(startup, "r"); + FILE *fp = Py_fopen(startup, "r"); if (fp == NULL) { int save_errno = errno; PyErr_Clear(); diff --git a/Python/errors.c b/Python/errors.c index 7f3b4aabc432d7..3376012dca4d01 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1972,7 +1972,7 @@ _PyErr_ProgramDecodedTextObject(PyObject *filename, int lineno, const char* enco return NULL; } - FILE *fp = _Py_fopen_obj(filename, "r" PY_STDIOTEXTMODE); + FILE *fp = Py_fopen(filename, "r" PY_STDIOTEXTMODE); if (fp == NULL) { PyErr_Clear(); return NULL; diff --git a/Python/fileutils.c b/Python/fileutils.c index 9529b14d377c60..c039fdc8f3309e 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1748,8 +1748,10 @@ _Py_wfopen(const wchar_t *path, const wchar_t *mode) } -/* Open a file. Call _wfopen() on Windows, or encode the path to the filesystem - encoding and call fopen() otherwise. +/* Open a file. + + On Windows, if 'path' is a Unicode string, call _wfopen(). Otherwise, encode + the path to the filesystem encoding and call fopen(). Return the new file object on success. Raise an exception and return NULL on error. @@ -1762,72 +1764,64 @@ _Py_wfopen(const wchar_t *path, const wchar_t *mode) Release the GIL to call _wfopen() or fopen(). The caller must hold the GIL. */ FILE* -_Py_fopen_obj(PyObject *path, const char *mode) +Py_fopen(PyObject *path, const char *mode) { - FILE *f; - int async_err = 0; -#ifdef MS_WINDOWS - wchar_t wmode[10]; - int usize; - assert(PyGILState_Check()); if (PySys_Audit("open", "Osi", path, mode, 0) < 0) { return NULL; } - if (!PyUnicode_Check(path)) { - PyErr_Format(PyExc_TypeError, - "str file path expected under Windows, got %R", - Py_TYPE(path)); - return NULL; - } - wchar_t *wpath = PyUnicode_AsWideCharString(path, NULL); - if (wpath == NULL) - return NULL; + FILE *f; + int async_err = 0; + int saved_errno; +#ifdef MS_WINDOWS + if (PyUnicode_Check(path)) { + wchar_t *wpath = PyUnicode_AsWideCharString(path, NULL); + if (wpath == NULL) { + return NULL; + } - usize = MultiByteToWideChar(CP_ACP, 0, mode, -1, - wmode, Py_ARRAY_LENGTH(wmode)); - if (usize == 0) { - PyErr_SetFromWindowsErr(0); + wchar_t wmode[10]; + int usize = MultiByteToWideChar(CP_ACP, 0, mode, -1, + wmode, Py_ARRAY_LENGTH(wmode)); + if (usize == 0) { + PyErr_SetFromWindowsErr(0); + PyMem_Free(wpath); + return NULL; + } + + do { + Py_BEGIN_ALLOW_THREADS + f = _wfopen(wpath, wmode); + Py_END_ALLOW_THREADS + } while (f == NULL + && errno == EINTR && !(async_err = PyErr_CheckSignals())); + saved_errno = errno; PyMem_Free(wpath); - return NULL; } + else +#endif + { + PyObject *bytes; + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } + const char *path_bytes = PyBytes_AS_STRING(bytes); - do { - Py_BEGIN_ALLOW_THREADS - f = _wfopen(wpath, wmode); - Py_END_ALLOW_THREADS - } while (f == NULL - && errno == EINTR && !(async_err = PyErr_CheckSignals())); - int saved_errno = errno; - PyMem_Free(wpath); -#else - PyObject *bytes; - const char *path_bytes; - - assert(PyGILState_Check()); - - if (!PyUnicode_FSConverter(path, &bytes)) - return NULL; - path_bytes = PyBytes_AS_STRING(bytes); - - if (PySys_Audit("open", "Osi", path, mode, 0) < 0) { + do { + Py_BEGIN_ALLOW_THREADS + f = fopen(path_bytes, mode); + Py_END_ALLOW_THREADS + } while (f == NULL + && errno == EINTR && !(async_err = PyErr_CheckSignals())); + saved_errno = errno; Py_DECREF(bytes); - return NULL; } - do { - Py_BEGIN_ALLOW_THREADS - f = fopen(path_bytes, mode); - Py_END_ALLOW_THREADS - } while (f == NULL - && errno == EINTR && !(async_err = PyErr_CheckSignals())); - int saved_errno = errno; - Py_DECREF(bytes); -#endif - if (async_err) + if (async_err) { return NULL; + } if (f == NULL) { errno = saved_errno; diff --git a/Python/import.c b/Python/import.c index b3c384c27718ce..9566b596703884 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4687,7 +4687,7 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) * code relies on fp still being open. */ FILE *fp; if (file != NULL) { - fp = _Py_fopen_obj(info.filename, "r"); + fp = Py_fopen(info.filename, "r"); if (fp == NULL) { goto finally; } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 8b57018321c070..8b8a9e72a9ac61 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -467,7 +467,7 @@ _PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, fclose(fp); } - pyc_fp = _Py_fopen_obj(filename, "rb"); + pyc_fp = Py_fopen(filename, "rb"); if (pyc_fp == NULL) { fprintf(stderr, "python: Can't reopen .pyc file\n"); goto done; From b80f4245e4635306c6c7e3fda08323d5e859390d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 13 Dec 2024 14:13:59 +0100 Subject: [PATCH 02/18] Add Py_fclose() function --- Doc/c-api/sys.rst | 15 +++++++++++++++ Doc/whatsnew/3.14.rst | 3 ++- Include/cpython/fileutils.h | 2 ++ ...2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst | 4 +++- Modules/_testcapi/file.c | 4 ++-- Python/fileutils.c | 13 +++++++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index d51e66d45d22b3..21f5c813c5dcdd 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -226,6 +226,9 @@ Operating System Utilities On success, return the new file object. On error, set an exception and return ``NULL``. + The file must be closed by :c:func:`Py_fclose` rather than calling directly + ``fclose()``. + The file descriptor is created non-inheritable (:pep:`446`). The caller must hold the GIL. @@ -233,6 +236,18 @@ Operating System Utilities .. versionadded:: next +.. c:function:: int Py_fclose(FILE *file) + + Call ``fclose(file)``. + + This function is needed on Windows: ``FILE*`` files opened by + :c:func:`Py_fopen` in the Python DLL must be closed by the Python DLL to use + the same C runtime version. Otherwise, calling ``fclose()`` directly can + cause undefined behavior. + + .. versionadded:: next + + .. _systemfunctions: System Functions diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 2ce1bcdad75fbc..22221cc51675ec 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1036,7 +1036,8 @@ New features * Add :c:func:`Py_fopen` function to open a file. Similar to the :c:func:`!fopen` function, but the *path* parameter is a Python object and an - exception is set on error. + exception is set on error. Add also :c:func:`Py_fclose` function to close a + file, function needed for Windows support. (Contributed by Victor Stinner in :gh:`127350`.) Porting to Python 3.14 diff --git a/Include/cpython/fileutils.h b/Include/cpython/fileutils.h index 3a2bfae1add88e..c65e5d807266d6 100644 --- a/Include/cpython/fileutils.h +++ b/Include/cpython/fileutils.h @@ -5,3 +5,5 @@ PyAPI_FUNC(FILE*) Py_fopen( PyObject *path, const char *mode); + +PyAPI_FUNC(int) Py_fclose(FILE *file); diff --git a/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst b/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst index e3a914188ca680..d1b528c673442f 100644 --- a/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst +++ b/Misc/NEWS.d/next/C_API/2024-12-11-13-01-26.gh-issue-127350.uEBZZ4.rst @@ -1,3 +1,5 @@ Add :c:func:`Py_fopen` function to open a file. Similar to the :c:func:`!fopen` function, but the *path* parameter is a Python object and an exception is set -on error. Patch by Victor Stinner. +on error. Add also :c:func:`Py_fclose` function to close a file, function +needed for Windows support. +Patch by Victor Stinner. diff --git a/Modules/_testcapi/file.c b/Modules/_testcapi/file.c index b163ec0efcb78b..0ce0780623b9cd 100644 --- a/Modules/_testcapi/file.c +++ b/Modules/_testcapi/file.c @@ -17,7 +17,7 @@ _testcapi.py_fopen mode: str / -Call Py_fopen() and return fread(256). +Call Py_fopen(), fread(256) and Py_fclose(). Return read bytes. [clinic start generated code]*/ static PyObject * @@ -31,7 +31,7 @@ _testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode) char buffer[256]; size_t size = fread(buffer, 1, Py_ARRAY_LENGTH(buffer), fp); - fclose(fp); + Py_fclose(fp); return PyBytes_FromStringAndSize(buffer, size); } diff --git a/Python/fileutils.c b/Python/fileutils.c index c039fdc8f3309e..7075e6a6938efe 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1836,6 +1836,19 @@ Py_fopen(PyObject *path, const char *mode) return f; } + +// Call fclose(). +// +// This function is needed on Windows: FILE* files opened by Py_fopen() in the +// Python DLL must be closed by the Python DLL to use the same C runtime +// version. Otherwise, calling fclose() directly can cause undefined behavior. +int +Py_fclose(FILE *file) +{ + return fclose(file); +} + + /* Read count bytes from fd into buf. On success, return the number of read bytes, it can be lower than count. From 7390c5e6ff85f713b2ba1728f058eac3a8496889 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 13 Dec 2024 14:37:55 +0100 Subject: [PATCH 03/18] Replace _Py_fopen_obj() with Py_fopen() in sysmodule.c --- Python/sysmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index d6719f9bb0af91..887591a681b25c 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2356,7 +2356,7 @@ static PyObject * sys__dump_tracelets_impl(PyObject *module, PyObject *outpath) /*[clinic end generated code: output=a7fe265e2bc3b674 input=5bff6880cd28ffd1]*/ { - FILE *out = _Py_fopen_obj(outpath, "wb"); + FILE *out = Py_fopen(outpath, "wb"); if (out == NULL) { return NULL; } From 26a5a5698e43419f52ad831cd3706916fc525907 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 13 Dec 2024 15:44:50 +0100 Subject: [PATCH 04/18] Simplify Py_fclose() documentation --- Doc/c-api/sys.rst | 5 ----- Doc/whatsnew/3.14.rst | 2 +- Python/fileutils.c | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index 21f5c813c5dcdd..b132988aeac875 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -240,11 +240,6 @@ Operating System Utilities Call ``fclose(file)``. - This function is needed on Windows: ``FILE*`` files opened by - :c:func:`Py_fopen` in the Python DLL must be closed by the Python DLL to use - the same C runtime version. Otherwise, calling ``fclose()`` directly can - cause undefined behavior. - .. versionadded:: next diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index c12dd3aa13f936..918d34b3ed40b6 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1048,7 +1048,7 @@ New features * Add :c:func:`Py_fopen` function to open a file. Similar to the :c:func:`!fopen` function, but the *path* parameter is a Python object and an exception is set on error. Add also :c:func:`Py_fclose` function to close a - file, function needed for Windows support. + file. (Contributed by Victor Stinner in :gh:`127350`.) Porting to Python 3.14 diff --git a/Python/fileutils.c b/Python/fileutils.c index 7075e6a6938efe..a3f889cb8a65e7 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1839,9 +1839,9 @@ Py_fopen(PyObject *path, const char *mode) // Call fclose(). // -// This function is needed on Windows: FILE* files opened by Py_fopen() in the -// Python DLL must be closed by the Python DLL to use the same C runtime -// version. Otherwise, calling fclose() directly can cause undefined behavior. +// On Windows, files opened by Py_fopen() in the Python DLL must be closed by +// the Python DLL to use the same C runtime version. Otherwise, calling +// fclose() directly can cause undefined behavior. int Py_fclose(FILE *file) { From 4375566a429ad288331a7f92e2462d7bace887b7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 13 Dec 2024 16:09:37 +0100 Subject: [PATCH 05/18] Run make clinic --- Modules/_testcapi/clinic/file.c.h | 4 ++-- Modules/_testcapi/file.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_testcapi/clinic/file.c.h b/Modules/_testcapi/clinic/file.c.h index 9856b6161cdfa0..2ca21fffcef680 100644 --- a/Modules/_testcapi/clinic/file.c.h +++ b/Modules/_testcapi/clinic/file.c.h @@ -8,7 +8,7 @@ PyDoc_STRVAR(_testcapi_py_fopen__doc__, "py_fopen($module, path, mode, /)\n" "--\n" "\n" -"Call Py_fopen() and return fread(256)."); +"Call Py_fopen(), fread(256) and Py_fclose(). Return read bytes."); #define _TESTCAPI_PY_FOPEN_METHODDEF \ {"py_fopen", _PyCFunction_CAST(_testcapi_py_fopen), METH_FASTCALL, _testcapi_py_fopen__doc__}, @@ -45,4 +45,4 @@ _testcapi_py_fopen(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=968f4f4a620f13cf input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c9fe964c3e5a0c32 input=a9049054013a1b77]*/ diff --git a/Modules/_testcapi/file.c b/Modules/_testcapi/file.c index 0ce0780623b9cd..4bad43010fd440 100644 --- a/Modules/_testcapi/file.c +++ b/Modules/_testcapi/file.c @@ -22,7 +22,7 @@ Call Py_fopen(), fread(256) and Py_fclose(). Return read bytes. static PyObject * _testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode) -/*[clinic end generated code: output=5a900af000f759de input=0878c2f9333abd60]*/ +/*[clinic end generated code: output=5a900af000f759de input=d7e7b8f0fd151953]*/ { FILE *fp = Py_fopen(path, mode); if (fp == NULL) { From a7793215dfce7819ab82f726815b29ebdfef0236 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 13 Dec 2024 19:11:13 +0100 Subject: [PATCH 06/18] Update Doc/c-api/sys.rst Co-authored-by: Steve Dower --- Doc/c-api/sys.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index b132988aeac875..10178bbd42cbb6 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -238,7 +238,7 @@ Operating System Utilities .. c:function:: int Py_fclose(FILE *file) - Call ``fclose(file)``. + Closes files that were opened by :c:func:`Py_fopen`. .. versionadded:: next From e6133740e832018ea35888e43a88d009c35a2782 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 16 Dec 2024 13:41:42 +0100 Subject: [PATCH 07/18] Document Py_fclose() return value --- Doc/c-api/sys.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index 10178bbd42cbb6..2d044dbb30afcd 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -240,6 +240,11 @@ Operating System Utilities Closes files that were opened by :c:func:`Py_fopen`. + On success, return ``0``. + On error, return ``EOF`` and ``errno`` is set to indicate the error. + In either case, any further access (including another call to + :c:func:`Py_fclose`) to the stream results in undefined behavior. + .. versionadded:: next From 2af4d2b3c24e73d0ecafed22e9fddf9745c1f4e5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 11:31:37 +0100 Subject: [PATCH 08/18] Address some of Serhiy's review --- Doc/c-api/sys.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index 2d044dbb30afcd..daa4ab06e647b6 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -223,7 +223,7 @@ Operating System Utilities *path* must be a :class:`str` object or a :class:`bytes` object. - On success, return the new file object. + On success, return the new file pointer. On error, set an exception and return ``NULL``. The file must be closed by :c:func:`Py_fclose` rather than calling directly From 8e9170ea2c1ee50fc68b3113b6829b0b8aa13e2a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 11:32:56 +0100 Subject: [PATCH 09/18] Address Erlend's review --- Doc/c-api/sys.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index daa4ab06e647b6..ef62849505bf2a 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -218,7 +218,7 @@ Operating System Utilities .. c:function:: FILE* Py_fopen(PyObject *path, const char *mode) - Similar to the :c:func:`!fopen` function, but *path* is a Python object and + Similar to :c:func:`!fopen`, but *path* is a Python object and an exception is set on error. *path* must be a :class:`str` object or a :class:`bytes` object. @@ -227,7 +227,7 @@ Operating System Utilities On error, set an exception and return ``NULL``. The file must be closed by :c:func:`Py_fclose` rather than calling directly - ``fclose()``. + :c:func:`!fclose`. The file descriptor is created non-inheritable (:pep:`446`). @@ -238,7 +238,7 @@ Operating System Utilities .. c:function:: int Py_fclose(FILE *file) - Closes files that were opened by :c:func:`Py_fopen`. + Close a file that was opened by :c:func:`Py_fopen`. On success, return ``0``. On error, return ``EOF`` and ``errno`` is set to indicate the error. From a8c26b2707e74c6b1ca593d4123cc7f283e2d5c5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 11:51:03 +0100 Subject: [PATCH 10/18] Add more tests --- Lib/test/test_capi/test_file.py | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index 68e63ad2a3e490..bae4bce3dcc0a9 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -1,23 +1,55 @@ import os import unittest -from test.support import import_helper +from test import support +from test.support import import_helper, os_helper _testcapi = import_helper.import_module('_testcapi') class CAPIFileTest(unittest.TestCase): def test_py_fopen(self): + # Test Py_fopen() and Py_fclose() for filename in (__file__, os.fsencode(__file__)): with self.subTest(filename=filename): content = _testcapi.py_fopen(filename, "rb") with open(filename, "rb") as fp: self.assertEqual(fp.read(256), content) + with open(__file__, "rb") as fp: + content = fp.read() + for filename in ( + os_helper.TESTFN, + os.fsencode(os_helper.TESTFN), + os_helper.TESTFN_UNDECODABLE, + os_helper.TESTFN_UNENCODABLE, + ): + with self.subTest(filename=filename): + try: + with open(filename, "wb") as fp: + fp.write(content) + + content = _testcapi.py_fopen(filename, "rb") + with open(filename, "rb") as fp: + self.assertEqual(fp.read(256), content[:256]) + finally: + os_helper.unlink(filename) + + # embedded null character/byte in the filename + with self.assertRaises(ValueError): + _testcapi.py_fopen("a\x00b", "rb") + with self.assertRaises(ValueError): + _testcapi.py_fopen(b"a\x00b", "rb") + for invalid_type in (123, object()): with self.subTest(filename=invalid_type): with self.assertRaises(TypeError): _testcapi.py_fopen(invalid_type, "r") + if support.MS_WINDOWS: + with self.assertRaises(OSError): + # On Windows, the file mode is limited to 10 characters + _testcapi.py_fopen(__file__, "rt+, ccs=UTF-8") + if __name__ == "__main__": unittest.main() From 5b52a4e63dcd5ca445bdb721feb44f3d622c6e70 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 12:08:40 +0100 Subject: [PATCH 11/18] Support __fspath__() protocol on Windows Test also non-ASCII mode. --- Lib/test/test_capi/test_file.py | 33 +++++++++++++++++++++++---------- Python/fileutils.c | 6 ++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index bae4bce3dcc0a9..66a7e18f59a108 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -9,14 +9,23 @@ class CAPIFileTest(unittest.TestCase): def test_py_fopen(self): # Test Py_fopen() and Py_fclose() + class FSPath: + def __init__(self, path): + self.path = path + def __fspath__(self): + return self.path + + with open(__file__, "rb") as fp: + source = fp.read() + for filename in (__file__, os.fsencode(__file__)): with self.subTest(filename=filename): - content = _testcapi.py_fopen(filename, "rb") - with open(filename, "rb") as fp: - self.assertEqual(fp.read(256), content) + data = _testcapi.py_fopen(filename, "rb") + self.assertEqual(data, source[:256]) + + data = _testcapi.py_fopen(FSPath(filename), "rb") + self.assertEqual(data, source[:256]) - with open(__file__, "rb") as fp: - content = fp.read() for filename in ( os_helper.TESTFN, os.fsencode(os_helper.TESTFN), @@ -26,11 +35,10 @@ def test_py_fopen(self): with self.subTest(filename=filename): try: with open(filename, "wb") as fp: - fp.write(content) + fp.write(source) - content = _testcapi.py_fopen(filename, "rb") - with open(filename, "rb") as fp: - self.assertEqual(fp.read(256), content[:256]) + data = _testcapi.py_fopen(filename, "rb") + self.assertEqual(data, source[:256]) finally: os_helper.unlink(filename) @@ -40,10 +48,15 @@ def test_py_fopen(self): with self.assertRaises(ValueError): _testcapi.py_fopen(b"a\x00b", "rb") + # non-ASCII mode failing with "Invalid argument" + with self.assertRaises(OSError): + _testcapi.py_fopen(__file__, "\xe9") + + # invalid filename type for invalid_type in (123, object()): with self.subTest(filename=invalid_type): with self.assertRaises(TypeError): - _testcapi.py_fopen(invalid_type, "r") + _testcapi.py_fopen(invalid_type, "rb") if support.MS_WINDOWS: with self.assertRaises(OSError): diff --git a/Python/fileutils.c b/Python/fileutils.c index 746f86aeaeab54..0d3baeccdf4013 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1776,6 +1776,12 @@ Py_fopen(PyObject *path, const char *mode) int async_err = 0; int saved_errno; #ifdef MS_WINDOWS + PyObject *fspath = PyOS_FSPath(arg); + if (fspath == NULL) { + return NULL; + } + Py_SETREF(path, fspath); + if (PyUnicode_Check(path)) { wchar_t *wpath = PyUnicode_AsWideCharString(path, NULL); if (wpath == NULL) { From e6a53feda01b5d47675de112c596e24773562585 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 12:10:27 +0100 Subject: [PATCH 12/18] Update doc for path-like object --- Doc/c-api/sys.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index ef62849505bf2a..7a7d39aea20baf 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -221,7 +221,8 @@ Operating System Utilities Similar to :c:func:`!fopen`, but *path* is a Python object and an exception is set on error. - *path* must be a :class:`str` object or a :class:`bytes` object. + *path* must be a :class:`str` object, a :class:`bytes` object, + or a :term:`path-like object`. On success, return the new file pointer. On error, set an exception and return ``NULL``. From 1d1e7f4985ae0dcd177e5bccc41b174192f44ed5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 12:15:45 +0100 Subject: [PATCH 13/18] Keep _Py_fopen_obj() function --- Include/cpython/fileutils.h | 5 +++++ Python/fileutils.c | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/Include/cpython/fileutils.h b/Include/cpython/fileutils.h index c65e5d807266d6..702f89aca324c5 100644 --- a/Include/cpython/fileutils.h +++ b/Include/cpython/fileutils.h @@ -6,4 +6,9 @@ PyAPI_FUNC(FILE*) Py_fopen( PyObject *path, const char *mode); +// Deprecated alias to Py_fopen() kept for backward compatibility +Py_DEPRECATED(3.14) PyAPI_FUNC(FILE*) _Py_fopen_obj( + PyObject *path, + const char *mode); + PyAPI_FUNC(int) Py_fclose(FILE *file); diff --git a/Python/fileutils.c b/Python/fileutils.c index 0d3baeccdf4013..e05eda7c6108b2 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1843,6 +1843,14 @@ Py_fopen(PyObject *path, const char *mode) } +// Deprecated alias to Py_fopen() kept for backward compatibility +FILE* +_Py_fopen_obj(PyObject *path, const char *mode) +{ + return Py_fopen(path, mode); +} + + // Call fclose(). // // On Windows, files opened by Py_fopen() in the Python DLL must be closed by From 335c1bc5e2ea677b79b0a1e751b73f9f508174e3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 12:16:53 +0100 Subject: [PATCH 14/18] Fix typo --- Python/fileutils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/fileutils.c b/Python/fileutils.c index e05eda7c6108b2..246ea27227717d 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1776,7 +1776,7 @@ Py_fopen(PyObject *path, const char *mode) int async_err = 0; int saved_errno; #ifdef MS_WINDOWS - PyObject *fspath = PyOS_FSPath(arg); + PyObject *fspath = PyOS_FSPath(path); if (fspath == NULL) { return NULL; } From 82414221f5b0386a0464173c5cb48a4c880a8d73 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 12:34:36 +0100 Subject: [PATCH 15/18] Fix test on macOS and WASI * Skip TESTFN_UNENCODABLE if it's None. * Remove TESTFN_UNDECODABLE test. --- Lib/test/test_capi/test_file.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index 66a7e18f59a108..d0a7b3d3d75441 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -26,12 +26,14 @@ def __fspath__(self): data = _testcapi.py_fopen(FSPath(filename), "rb") self.assertEqual(data, source[:256]) - for filename in ( + filenames = [ os_helper.TESTFN, os.fsencode(os_helper.TESTFN), - os_helper.TESTFN_UNDECODABLE, - os_helper.TESTFN_UNENCODABLE, - ): + ] + # TESTFN_UNDECODABLE cannot be used to create a file on macOS/WASI. + if os_helper.TESTFN_UNENCODABLE is not None: + filenames.append(os_helper.TESTFN_UNENCODABLE) + for filename in filenames: with self.subTest(filename=filename): try: with open(filename, "wb") as fp: From c91aa82dc4a059126c9730d5c03ee3ba6dd883ef Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 13:13:43 +0100 Subject: [PATCH 16/18] Rewrite the Windows implementation --- Python/fileutils.c | 78 ++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/Python/fileutils.c b/Python/fileutils.c index 246ea27227717d..6bc3a44c3c1313 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1776,54 +1776,52 @@ Py_fopen(PyObject *path, const char *mode) int async_err = 0; int saved_errno; #ifdef MS_WINDOWS - PyObject *fspath = PyOS_FSPath(path); - if (fspath == NULL) { + PyObject *unicode; + if (!PyUnicode_FSDecoder(path, &unicode)) { return NULL; } - Py_SETREF(path, fspath); - if (PyUnicode_Check(path)) { - wchar_t *wpath = PyUnicode_AsWideCharString(path, NULL); - if (wpath == NULL) { - return NULL; - } - - wchar_t wmode[10]; - int usize = MultiByteToWideChar(CP_ACP, 0, mode, -1, - wmode, Py_ARRAY_LENGTH(wmode)); - if (usize == 0) { - PyErr_SetFromWindowsErr(0); - PyMem_Free(wpath); - return NULL; - } + wchar_t *wpath = PyUnicode_AsWideCharString(unicode, NULL); + Py_DECREF(unicode); + if (wpath == NULL) { + return NULL; + } - do { - Py_BEGIN_ALLOW_THREADS - f = _wfopen(wpath, wmode); - Py_END_ALLOW_THREADS - } while (f == NULL - && errno == EINTR && !(async_err = PyErr_CheckSignals())); - saved_errno = errno; + wchar_t wmode[10]; + int usize = MultiByteToWideChar(CP_ACP, 0, mode, -1, + wmode, Py_ARRAY_LENGTH(wmode)); + if (usize == 0) { + PyErr_SetFromWindowsErr(0); PyMem_Free(wpath); + return NULL; } - else -#endif - { - PyObject *bytes; - if (!PyUnicode_FSConverter(path, &bytes)) { - return NULL; - } - const char *path_bytes = PyBytes_AS_STRING(bytes); - do { - Py_BEGIN_ALLOW_THREADS - f = fopen(path_bytes, mode); - Py_END_ALLOW_THREADS - } while (f == NULL - && errno == EINTR && !(async_err = PyErr_CheckSignals())); - saved_errno = errno; - Py_DECREF(bytes); + do { + Py_BEGIN_ALLOW_THREADS + _Py_BEGIN_SUPPRESS_IPH + f = _wfopen(wpath, wmode); + _Py_END_SUPPRESS_IPH + Py_END_ALLOW_THREADS + } while (f == NULL + && errno == EINTR && !(async_err = PyErr_CheckSignals())); + saved_errno = errno; + PyMem_Free(wpath); +#else + PyObject *bytes; + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; } + const char *path_bytes = PyBytes_AS_STRING(bytes); + + do { + Py_BEGIN_ALLOW_THREADS + f = fopen(path_bytes, mode); + Py_END_ALLOW_THREADS + } while (f == NULL + && errno == EINTR && !(async_err = PyErr_CheckSignals())); + saved_errno = errno; + Py_DECREF(bytes); +#endif if (async_err) { return NULL; From 0fc6dc10d767f93ead306cb06cb06fdedeacaf98 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 13:17:03 +0100 Subject: [PATCH 17/18] Use FakePath --- Lib/test/test_capi/test_file.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index d0a7b3d3d75441..3f25a1e7fe0588 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -9,11 +9,6 @@ class CAPIFileTest(unittest.TestCase): def test_py_fopen(self): # Test Py_fopen() and Py_fclose() - class FSPath: - def __init__(self, path): - self.path = path - def __fspath__(self): - return self.path with open(__file__, "rb") as fp: source = fp.read() @@ -23,7 +18,7 @@ def __fspath__(self): data = _testcapi.py_fopen(filename, "rb") self.assertEqual(data, source[:256]) - data = _testcapi.py_fopen(FSPath(filename), "rb") + data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb") self.assertEqual(data, source[:256]) filenames = [ From dda433569a6ecdae30495cf90fd6c24ef26db2f0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 13:19:18 +0100 Subject: [PATCH 18/18] NULL mode does crash --- Lib/test/test_capi/test_file.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index 3f25a1e7fe0588..8a08a0a93eb8b7 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -60,6 +60,8 @@ def test_py_fopen(self): # On Windows, the file mode is limited to 10 characters _testcapi.py_fopen(__file__, "rt+, ccs=UTF-8") + # CRASHES py_fopen(__file__, None) + if __name__ == "__main__": unittest.main()