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;