Skip to content

Commit

Permalink
gh-127350: Add Py_fopen() function
Browse files Browse the repository at this point in the history
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().
  • Loading branch information
vstinner committed Dec 11, 2024
1 parent d5d84c3 commit 773c4d9
Show file tree
Hide file tree
Showing 17 changed files with 197 additions and 75 deletions.
16 changes: 16 additions & 0 deletions Doc/c-api/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
3 changes: 1 addition & 2 deletions Include/cpython/fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
23 changes: 23 additions & 0 deletions Lib/test/test_capi/test_file.py
Original file line number Diff line number Diff line change
@@ -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()
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,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.
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() 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},
};

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 @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 773c4d9

Please sign in to comment.