Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
68 changes: 68 additions & 0 deletions Lib/test/test_capi/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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()
class FSPath:
def __init__(self, path):
self.path = path
def __fspath__(self):
return self.path

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(FSPath(filename), "rb")
self.assertEqual(data, source[:256])

for filename in (
os_helper.TESTFN,
os.fsencode(os_helper.TESTFN),
os_helper.TESTFN_UNDECODABLE,
os_helper.TESTFN_UNENCODABLE,
):
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")
Copy link
Member

@serhiy-storchaka serhiy-storchaka Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"\xe9" is encoded to b'\xc3\xa9'. Please test also with non-UTF-8 bytes. You may get different error on Windows. Actually, it may depend on the locale.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to pass non-UTF-8 bytes, PySys_Audit() decodes the mode from UTF-8 in strict mode:

    if (PySys_Audit("open", "Osi", path, mode, 0) < 0) {
        return NULL;
    }

I don't think that it's worth to "support" non-UTF-8 just for the test, whereas it's rejected anyway by fopen().


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


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
Loading