Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
11 changes: 6 additions & 5 deletions Doc/c-api/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,17 @@ Operating System Utilities

.. c:function:: FILE* Py_fopen(PyObject *path, const char *mode)

Similar to the :c:func:`!fopen` function, but *path* is a Python object and
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 or a :class:`bytes` object.
*path* must be a :class:`str` object, a :class:`bytes` object,
or a :term:`path-like object`.

On success, return the new file 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
``fclose()``.
:c:func:`!fclose`.

The file descriptor is created non-inheritable (:pep:`446`).

Expand All @@ -238,7 +239,7 @@ Operating System Utilities

.. c:function:: int Py_fclose(FILE *file)

Closes files that were opened by :c:func:`Py_fopen`.
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.
Expand Down
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1227,12 +1227,23 @@ New features
* Add :c:func:`PyUnstable_Object_EnableDeferredRefcount` for enabling
deferred reference counting, as outlined in :pep:`703`.

* The :ref:`Unicode Exception Objects <unicodeexceptions>` C API
now raises a :exc:`TypeError` if its exception argument is not
a :exc:`UnicodeError` object.
(Contributed by Bénédikt Tran in :gh:`127691`.)

* Add :c:func:`PyMonitoring_FireBranchLeftEvent` and
:c:func:`PyMonitoring_FireBranchRightEvent` for generating
: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
5 changes: 5 additions & 0 deletions Include/cpython/fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ 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);
55 changes: 50 additions & 5 deletions Lib/test/test_capi/test_file.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,67 @@
import os
import unittest
from test.support import import_helper
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):
content = _testcapi.py_fopen(filename, "rb")
with open(filename, "rb") as fp:
self.assertEqual(fp.read(256), content)
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, "r")
_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__":
Expand Down
14 changes: 14 additions & 0 deletions Python/fileutils.c
Original file line number Diff line number Diff line change
Expand Up @@ -1776,6 +1776,12 @@ Py_fopen(PyObject *path, const char *mode)
int async_err = 0;
int saved_errno;
#ifdef MS_WINDOWS
PyObject *fspath = PyOS_FSPath(path);
if (fspath == NULL) {
return NULL;
}
Py_SETREF(path, fspath);

if (PyUnicode_Check(path)) {
wchar_t *wpath = PyUnicode_AsWideCharString(path, NULL);
if (wpath == NULL) {
Expand Down Expand Up @@ -1837,6 +1843,14 @@ Py_fopen(PyObject *path, const char *mode)
}


// Deprecated alias to Py_fopen() kept for backward compatibility
FILE*
_Py_fopen_obj(PyObject *path, const char *mode)
{
return Py_fopen(path, mode);
}


// Call fclose().
//
// On Windows, files opened by Py_fopen() in the Python DLL must be closed by
Expand Down
Loading