Skip to content

Commit f89e5e2

Browse files
authored
gh-127350: Add Py_fopen() and Py_fclose() functions (#127821)
1 parent 7e8c571 commit f89e5e2

File tree

18 files changed

+270
-53
lines changed

18 files changed

+270
-53
lines changed

Doc/c-api/sys.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,38 @@ Operating System Utilities
216216
The function now uses the UTF-8 encoding on Windows if
217217
:c:member:`PyPreConfig.legacy_windows_fs_encoding` is zero.
218218
219+
.. c:function:: FILE* Py_fopen(PyObject *path, const char *mode)
220+
221+
Similar to :c:func:`!fopen`, but *path* is a Python object and
222+
an exception is set on error.
223+
224+
*path* must be a :class:`str` object, a :class:`bytes` object,
225+
or a :term:`path-like object`.
226+
227+
On success, return the new file pointer.
228+
On error, set an exception and return ``NULL``.
229+
230+
The file must be closed by :c:func:`Py_fclose` rather than calling directly
231+
:c:func:`!fclose`.
232+
233+
The file descriptor is created non-inheritable (:pep:`446`).
234+
235+
The caller must hold the GIL.
236+
237+
.. versionadded:: next
238+
239+
240+
.. c:function:: int Py_fclose(FILE *file)
241+
242+
Close a file that was opened by :c:func:`Py_fopen`.
243+
244+
On success, return ``0``.
245+
On error, return ``EOF`` and ``errno`` is set to indicate the error.
246+
In either case, any further access (including another call to
247+
:c:func:`Py_fclose`) to the stream results in undefined behavior.
248+
249+
.. versionadded:: next
250+
219251
220252
.. _systemfunctions:
221253

Doc/whatsnew/3.14.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,12 @@ New features
12371237
:monitoring-event:`BRANCH_LEFT` and :monitoring-event:`BRANCH_RIGHT`
12381238
events, respectively.
12391239

1240+
* Add :c:func:`Py_fopen` function to open a file. Similar to the
1241+
:c:func:`!fopen` function, but the *path* parameter is a Python object and an
1242+
exception is set on error. Add also :c:func:`Py_fclose` function to close a
1243+
file.
1244+
(Contributed by Victor Stinner in :gh:`127350`.)
1245+
12401246

12411247
Porting to Python 3.14
12421248
----------------------

Include/cpython/fileutils.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
# error "this header file must not be included directly"
33
#endif
44

5-
// Used by _testcapi which must not use the internal C API
6-
PyAPI_FUNC(FILE*) _Py_fopen_obj(
5+
PyAPI_FUNC(FILE*) Py_fopen(
76
PyObject *path,
87
const char *mode);
8+
9+
// Deprecated alias to Py_fopen() kept for backward compatibility
10+
Py_DEPRECATED(3.14) PyAPI_FUNC(FILE*) _Py_fopen_obj(
11+
PyObject *path,
12+
const char *mode);
13+
14+
PyAPI_FUNC(int) Py_fclose(FILE *file);

Lib/test/test_capi/test_file.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
import unittest
3+
from test import support
4+
from test.support import import_helper, os_helper
5+
6+
_testcapi = import_helper.import_module('_testcapi')
7+
8+
9+
class CAPIFileTest(unittest.TestCase):
10+
def test_py_fopen(self):
11+
# Test Py_fopen() and Py_fclose()
12+
13+
with open(__file__, "rb") as fp:
14+
source = fp.read()
15+
16+
for filename in (__file__, os.fsencode(__file__)):
17+
with self.subTest(filename=filename):
18+
data = _testcapi.py_fopen(filename, "rb")
19+
self.assertEqual(data, source[:256])
20+
21+
data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb")
22+
self.assertEqual(data, source[:256])
23+
24+
filenames = [
25+
os_helper.TESTFN,
26+
os.fsencode(os_helper.TESTFN),
27+
]
28+
# TESTFN_UNDECODABLE cannot be used to create a file on macOS/WASI.
29+
if os_helper.TESTFN_UNENCODABLE is not None:
30+
filenames.append(os_helper.TESTFN_UNENCODABLE)
31+
for filename in filenames:
32+
with self.subTest(filename=filename):
33+
try:
34+
with open(filename, "wb") as fp:
35+
fp.write(source)
36+
37+
data = _testcapi.py_fopen(filename, "rb")
38+
self.assertEqual(data, source[:256])
39+
finally:
40+
os_helper.unlink(filename)
41+
42+
# embedded null character/byte in the filename
43+
with self.assertRaises(ValueError):
44+
_testcapi.py_fopen("a\x00b", "rb")
45+
with self.assertRaises(ValueError):
46+
_testcapi.py_fopen(b"a\x00b", "rb")
47+
48+
# non-ASCII mode failing with "Invalid argument"
49+
with self.assertRaises(OSError):
50+
_testcapi.py_fopen(__file__, "\xe9")
51+
52+
# invalid filename type
53+
for invalid_type in (123, object()):
54+
with self.subTest(filename=invalid_type):
55+
with self.assertRaises(TypeError):
56+
_testcapi.py_fopen(invalid_type, "rb")
57+
58+
if support.MS_WINDOWS:
59+
with self.assertRaises(OSError):
60+
# On Windows, the file mode is limited to 10 characters
61+
_testcapi.py_fopen(__file__, "rt+, ccs=UTF-8")
62+
63+
# CRASHES py_fopen(__file__, None)
64+
65+
66+
if __name__ == "__main__":
67+
unittest.main()

Lib/test/test_ssl.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,7 @@ def test_load_verify_cadata(self):
13251325
def test_load_dh_params(self):
13261326
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
13271327
ctx.load_dh_params(DHFILE)
1328-
if os.name != 'nt':
1329-
ctx.load_dh_params(BYTES_DHFILE)
1328+
ctx.load_dh_params(BYTES_DHFILE)
13301329
self.assertRaises(TypeError, ctx.load_dh_params)
13311330
self.assertRaises(TypeError, ctx.load_dh_params, None)
13321331
with self.assertRaises(FileNotFoundError) as cm:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add :c:func:`Py_fopen` function to open a file. Similar to the :c:func:`!fopen`
2+
function, but the *path* parameter is a Python object and an exception is set
3+
on error. Add also :c:func:`Py_fclose` function to close a file, function
4+
needed for Windows support.
5+
Patch by Victor Stinner.

Modules/_ssl.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4377,7 +4377,7 @@ _ssl__SSLContext_load_dh_params_impl(PySSLContext *self, PyObject *filepath)
43774377
FILE *f;
43784378
DH *dh;
43794379

4380-
f = _Py_fopen_obj(filepath, "rb");
4380+
f = Py_fopen(filepath, "rb");
43814381
if (f == NULL)
43824382
return NULL;
43834383

Modules/_ssl/debughelpers.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ _PySSLContext_set_keylog_filename(PySSLContext *self, PyObject *arg, void *c) {
180180
return 0;
181181
}
182182

183-
/* _Py_fopen_obj() also checks that arg is of proper type. */
184-
fp = _Py_fopen_obj(arg, "a" PY_STDIOTEXTMODE);
183+
/* Py_fopen() also checks that arg is of proper type. */
184+
fp = Py_fopen(arg, "a" PY_STDIOTEXTMODE);
185185
if (fp == NULL)
186186
return -1;
187187

Modules/_testcapi/clinic/file.c.h

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/file.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1+
// clinic/file.c.h uses internal pycore_modsupport.h API
2+
#define PYTESTCAPI_NEED_INTERNAL_API
3+
14
#include "parts.h"
25
#include "util.h"
6+
#include "clinic/file.c.h"
7+
8+
/*[clinic input]
9+
module _testcapi
10+
[clinic start generated code]*/
11+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6361033e795369fc]*/
12+
13+
/*[clinic input]
14+
_testcapi.py_fopen
15+
16+
path: object
17+
mode: str
18+
/
19+
20+
Call Py_fopen(), fread(256) and Py_fclose(). Return read bytes.
21+
[clinic start generated code]*/
322

23+
static PyObject *
24+
_testcapi_py_fopen_impl(PyObject *module, PyObject *path, const char *mode)
25+
/*[clinic end generated code: output=5a900af000f759de input=d7e7b8f0fd151953]*/
26+
{
27+
FILE *fp = Py_fopen(path, mode);
28+
if (fp == NULL) {
29+
return NULL;
30+
}
31+
32+
char buffer[256];
33+
size_t size = fread(buffer, 1, Py_ARRAY_LENGTH(buffer), fp);
34+
Py_fclose(fp);
35+
36+
return PyBytes_FromStringAndSize(buffer, size);
37+
}
438

539
static PyMethodDef test_methods[] = {
40+
_TESTCAPI_PY_FOPEN_METHODDEF
641
{NULL},
742
};
843

0 commit comments

Comments
 (0)