Skip to content

Commit

Permalink
gh-127350: Add Py_fopen() and Py_fclose() functions (#127821)
Browse files Browse the repository at this point in the history
  • Loading branch information
vstinner authored Jan 6, 2025
1 parent 7e8c571 commit f89e5e2
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 53 deletions.
32 changes: 32 additions & 0 deletions Doc/c-api/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,38 @@ 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 :c:func:`!fopen`, but *path* is a Python object and
an exception is set on error.
*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``.
The file must be closed by :c:func:`Py_fclose` rather than calling directly
:c:func:`!fclose`.
The file descriptor is created non-inheritable (:pep:`446`).
The caller must hold the GIL.
.. versionadded:: next
.. c:function:: int Py_fclose(FILE *file)
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.
In either case, any further access (including another call to
:c:func:`Py_fclose`) to the stream results in undefined behavior.
.. versionadded:: next
.. _systemfunctions:
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,12 @@ New features
:monitoring-event:`BRANCH_LEFT` and :monitoring-event:`BRANCH_RIGHT`
events, respectively.

* 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.
(Contributed by Victor Stinner in :gh:`127350`.)


Porting to Python 3.14
----------------------
Expand Down
10 changes: 8 additions & 2 deletions Include/cpython/fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
# 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);

// 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);
67 changes: 67 additions & 0 deletions Lib/test/test_capi/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import unittest
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()

with open(__file__, "rb") as fp:
source = fp.read()

for filename in (__file__, os.fsencode(__file__)):
with self.subTest(filename=filename):
data = _testcapi.py_fopen(filename, "rb")
self.assertEqual(data, source[:256])

data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb")
self.assertEqual(data, source[:256])

filenames = [
os_helper.TESTFN,
os.fsencode(os_helper.TESTFN),
]
# 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:
fp.write(source)

data = _testcapi.py_fopen(filename, "rb")
self.assertEqual(data, source[: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")

# 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, "rb")

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")

# CRASHES py_fopen(__file__, None)


if __name__ == "__main__":
unittest.main()
3 changes: 1 addition & 2 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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. Add also :c:func:`Py_fclose` function to close a file, function
needed for Windows support.
Patch by Victor Stinner.
2 changes: 1 addition & 1 deletion Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions Modules/_ssl/debughelpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
48 changes: 48 additions & 0 deletions Modules/_testcapi/clinic/file.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions Modules/_testcapi/file.c
Original file line number Diff line number Diff line change
@@ -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(), fread(256) and Py_fclose(). Return read bytes.
[clinic start generated code]*/

static PyObject *
_testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode)
/*[clinic end generated code: output=5a900af000f759de input=d7e7b8f0fd151953]*/
{
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);
Py_fclose(fp);

return PyBytes_FromStringAndSize(buffer, size);
}

static PyMethodDef test_methods[] = {
_TESTCAPI_PY_FOPEN_METHODDEF
{NULL},
};

Expand Down
8 changes: 4 additions & 4 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions Modules/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -465,7 +465,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();
Expand Down
2 changes: 1 addition & 1 deletion Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -1981,7 +1981,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;
Expand Down
Loading

0 comments on commit f89e5e2

Please sign in to comment.