8000 gh-127350: Add Py_fopen() and Py_fclose() functions (#127821) · python/cpython@f89e5e2 · GitHub
[go: up one dir, main page]

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

Modules/_testcapi/object.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ call_pyobject_print(PyObject *self, PyObject * args)
1515
return NULL;
1616
}
1717

18-
fp = _Py_fopen_obj(filename, "w+");
18+
fp = Py_fopen(filename, "w+");
1919

2020
if (Py_IsTrue(print_raw)) {
2121
flags = Py_PRINT_RAW;
@@ -41,7 +41,7 @@ pyobject_print_null(PyObject *self, PyObject *args)
4141
return NULL;
4242
}
4343

44-
fp = _Py_fopen_obj(filename, "w+");
44+
fp = Py_fopen(filename, "w+");
4545

4646
if (PyObject_Print(NULL, fp, 0) < 0) {
4747
fclose(fp);
@@ -72,7 +72,7 @@ pyobject_print_noref_object(PyObject *self, PyObject *args)
7272
return NULL;
7373
}
7474

75-
fp = _Py_fopen_obj(filename, "w+");
75+
fp = Py_fopen(filename, "w+");
7676

7777
if (PyObject_Print(test_string, fp, 0) < 0){
7878
fclose(fp);
@@ -103,7 +103,7 @@ pyobject_print_os_error(PyObject *self, PyObject *args)
103103
}
104104

105105
// open file in read mode to induce OSError
106-
fp = _Py_fopen_obj(filename, "r");
106+
fp = Py_fopen(filename, "r");
107107

108108
if (PyObject_Print(test_string, fp, 0) < 0) {
109109
fclose(fp);

Modules/_testcapimodule.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,7 +1744,7 @@ pymarshal_write_long_to_file(PyObject* self, PyObject *args)
17441744
&value, &filename, &version))
17451745
return NULL;
17461746

1747-
fp = _Py_fopen_obj(filename, "wb");
1747+
fp = Py_fopen(filename, "wb");
17481748
if (fp == NULL) {
17491749
PyErr_SetFromErrno(PyExc_OSError);
17501750
return NULL;
@@ -1769,7 +1769,7 @@ pymarshal_write_object_to_file(PyObject* self, PyObject *args)
17691769
&obj, &filename, &version))
17701770
return NULL;
17711771

1772-
fp = _Py_fopen_obj(filename, "wb");
1772+
fp = Py_fopen(filename, "wb");
17731773
if (fp == NULL) {
17741774
PyErr_SetFromErrno(PyExc_OSError);
17751775
return NULL;
@@ -1793,7 +1793,7 @@ pymarshal_read_short_from_file(PyObject* self, PyObject *args)
17931793
if (!PyArg_ParseTuple(args, "O:pymarshal_read_short_from_file", &filename))
17941794
return NULL;
17951795

1796-
fp = _Py_fopen_obj(filename, "rb");
1796+
fp = Py_fopen(filename, "rb");
17971797
if (fp == NULL) {
17981798
PyErr_SetFromErrno(PyExc_OSError);
17991799
return NULL;
@@ -1818,7 +1818,7 @@ pymarshal_read_long_from_file(PyObject* self, PyObject *args)
18181818
if (!PyArg_ParseTuple(args, "O:pymarshal_read_long_from_file", &filename))
18191819
return NULL;
18201820

1821-
fp = _Py_fopen_obj(filename, "rb");
1821+
fp = Py_fopen(filename, "rb");
18221822
if (fp == NULL) {
18231823
PyErr_SetFromErrno(PyExc_OSError);
18241824
return NULL;
@@ -1840,7 +1840,7 @@ pymarshal_read_last_object_from_file(PyObject* self, PyObject *args)
18401840
if (!PyArg_ParseTuple(args, "O:pymarshal_read_last_object_from_file", &filename))
18411841
return NULL;
18421842

1843-
FILE *fp = _Py_fopen_obj(filename, "rb");
1843+
FILE *fp = Py_fopen(filename, "rb");
18441844
if (fp == NULL) {
18451845
PyErr_SetFromErrno(PyExc_OSError);
18461846
return NULL;
@@ -1863,7 +1863,7 @@ pymarshal_read_object_from_file(PyObject* self, PyObject *args)
18631863
if (!PyArg_ParseTuple(args, "O:pymarshal_read_object_from_file", &filename))
18641864
return NULL;
18651865

1866-
FILE *fp = _Py_fopen_obj(filename, "rb");
1866+
FILE *fp = Py_fopen(filename, "rb");
18671867
if (fp == NULL) {
18681868
PyErr_SetFromErrno(PyExc_OSError);
18691869
return NULL;

Modules/main.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename,
370370
return pymain_exit_err_print();
371371
}
372372

373-
FILE *fp = _Py_fopen_obj(filename, "rb");
373+
FILE *fp = Py_fopen(filename, "rb");
374374
if (fp == NULL) {
375375
// Ignore the OSError
376376
PyErr_Clear();
@@ -465,7 +465,7 @@ pymain_run_startup(PyConfig *config, int *exitcode)
465465
goto error;
466466
}
467467

468-
FILE *fp = _Py_fopen_obj(startup, "r");
468+
FILE *fp = Py_fopen(startup, "r");
469469
if (fp == NULL) {
470470
int save_errno = errno;
471471
PyErr_Clear();

Python/errors.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1981,7 +1981,7 @@ _PyErr_ProgramDecodedTextObject(PyObject *filename, int lineno, const char* enco
19811981
return NULL;
19821982
}
19831983

1984-
FILE *fp = _Py_fopen_obj(filename, "r" PY_STDIOTEXTMODE);
1984+
FILE *fp = Py_fopen(filename, "r" PY_STDIOTEXTMODE);
19851985
if (fp == NULL) {
19861986
PyErr_Clear();
19871987
return NULL;

0 commit comments

Comments
 (0)
0